Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js (revision 14588) @@ -1,1214 +1,1216 @@ var AEGIS = function(m) { /* This is an attack plan (despite the name, it's a relic of older times). * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units rae built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. * These should be extremely flexible with only minimal work. * There is a basic support for naval expeditions here. */ m.CityAttack = function CityAttack(gameState, HQ, Config, uniqueID, targetEnemy, type , targetFinder) { this.Config = Config; //This is the list of IDs of the units in the plan this.idList=[]; this.state = "unexecuted"; this.targetPlayer = targetEnemy; if (this.targetPlayer === -1 || this.targetPlayer === undefined) { // let's find our prefered target, basically counting our enemies units. // TODO: improve this. var enemyCount = {}; for (var i = 1; i <=8; i++) enemyCount[i] = 0; gameState.getEntities().forEach(function(ent) { if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) { enemyCount[ent.owner()]++; } }); var max = 0; for (var i in enemyCount) if (enemyCount[i] > max && +i !== PlayerID) { this.targetPlayer = +i; max = enemyCount[i]; } } if (this.targetPlayer === undefined || this.targetPlayer === -1) { this.failed = true; return false; } var CCs = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); if (CCs.length === 0) { this.failed = true; return false; } m.debug ("Target (" + PlayerID +") = " +this.targetPlayer); this.targetFinder = targetFinder || this.defaultTargetFinder; this.type = type || "normal"; this.name = uniqueID; this.healthRecord = []; this.timeOfPlanStart = gameState.getTimeElapsed(); // we get the time at which we decided to start the attack this.maxPreparationTime = 210*1000; this.pausingStart = 0; this.totalPausingTime = 0; this.paused = false; this.onArrivalReaction = "proceedOnTargets"; // priority of the queues we'll create. var priority = 70; // priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 18, "batchSize" : 3, "classes" : ["Infantry","Ranged"], "interests" : [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 18, "batchSize" : 3, "classes" : ["Infantry","Melee"], "interests" : [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ], "templates" : [] }; var ats = Math.random() * 15000 - 15000; // attack time shuffle: move the exact attack time around a bit. // in this case we want to have the attack ready by the 14th minute. Countdown. Minimum 2 minutes. if (this.Config.difficulty >= 1) this.maxPreparationTime = (800000+ats) - gameState.getTimeElapsed() < 120000 ? 120000 : 800000 + ats - gameState.getTimeElapsed(); if (type === "Rush") { // we have 3 minutes to train infantry. delete this.unitStat["RangedInfantry"]; delete this.unitStat["MeleeInfantry"]; this.unitStat["Infantry"] = { "priority" : 1, "minSize" : 10, "targetSize" : 30, "batchSize" : 2, "classes" : ["Infantry"], "interests" : [ ["strength",1], ["cost",1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ], "templates" : [] }; this.maxPreparationTime = (540000+ats) - gameState.getTimeElapsed() < 120000 ? 120000 : 540000 + ats - gameState.getTimeElapsed(); priority = 250; } else if (type === "superSized") { // our first attack has started worst case at the 14th minute, we want to attack another time by the 21th minute, so we rock 6.5 minutes this.maxPreparationTime = 480000; // 8 minutes // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat["RangedInfantry"] = { "priority" : 0.7, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Ranged", "CitizenSoldier"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 0.7, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampRangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 25, "batchSize" : 5, "classes" : ["Infantry","Ranged", "Champion"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 20, "batchSize" : 5, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 0.7, "minSize" : 3, "targetSize" : 15, "batchSize" : 3, "classes" : ["Cavalry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 0.7, "minSize" : 3, "targetSize" : 15, "batchSize" : 3, "classes" : ["Cavalry","Ranged", "CitizenSoldier"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18, "batchSize" : 3, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18, "batchSize" : 3, "classes" : ["Cavalry","Melee", "Champion" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; priority = 90; } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); this.queue = gameState.ai.queues["plan_" + this.name]; gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; /* this.unitStat["Siege"]["filter"] = function (ent) { var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]]; return (strength[0] > 15 || strength[1] > 15); };*/ var filter = API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID)); this.unitCollection = gameState.getOwnUnits().filter(filter); this.unitCollection.registerUpdates(); this.unitCollection.length; this.unit = {}; // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrder = []; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (var unitCat in this.unitStat) { var cat = unitCat; var Unit = this.unitStat[cat]; filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); this.unit[cat] = gameState.getOwnUnits().filter(filter); this.unit[cat].registerUpdates(); this.unit[cat].length; this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]); } /*if (gameState.getTimeElapsed() > 900000) // 15 minutes { this.unitStat.Cavalry.Ranged["minSize"] = 5; this.unitStat.Cavalry.Melee["minSize"] = 5; this.unitStat.Infantry.Ranged["minSize"] = 10; this.unitStat.Infantry.Melee["minSize"] = 10; this.unitStat.Cavalry.Ranged["targetSize"] = 10; this.unitStat.Cavalry.Melee["targetSize"] = 10; this.unitStat.Infantry.Ranged["targetSize"] = 20; this.unitStat.Infantry.Melee["targetSize"] = 20; this.unitStat.Siege["targetSize"] = 5; this.unitStat.Siege["minSize"] = 2; } else { this.maxPreparationTime = 180000; }*/ // todo: REACTIVATE (in all caps) if (type === "harass_raid" && 0 == 1) { this.targetFinder = this.raidingTargetFinder; this.onArrivalReaction = "huntVillagers"; this.type = "harass_raid"; // This is a Cavalry raid against villagers. A Cavalry Swordsman has a bonus against these. Only build these this.maxPreparationTime = 180000; // 3 minutes. if (gameState.playerData.civ === "hele") // hellenes have an ealry Cavalry Swordsman { this.unitCount.Cavalry.Melee = { "subCat" : ["Swordsman"] , "usesSubcategories" : true, "Swordsman" : undefined, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 0, "preferedAmount" : 0 }; this.unitCount.Cavalry.Melee.Swordsman = { "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7, "fallback" : "abort" }; } else { this.unitCount.Cavalry.Melee = { "subCat" : undefined , "usesSubcategories" : false, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7 }; } this.unitCount.Cavalry.Ranged["minimalAmount"] = 0; this.unitCount.Cavalry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Ranged["minimalAmount"] = 0; this.unitCount.Infantry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Melee["minimalAmount"] = 0; this.unitCount.Infantry.Melee["preferedAmount"] = 0; this.unitCount.Siege["preferedAmount"] = 0; } this.anyNotMinimal = true; // used for support plans var myFortresses = gameState.getOwnTrainingFacilities().filter(API3.Filters.byClass("GarrisonFortress")); if (myFortresses.length !== 0) { // make this our rallypoint for (var i in myFortresses._entities) { if (myFortresses._entities[i].position()) { this.rallyPoint = myFortresses._entities[i].position(); break; } } } else { if(gameState.ai.pathsToMe.length > 1) var position = [(gameState.ai.pathsToMe[0][0]+gameState.ai.pathsToMe[1][0])/2.0,(gameState.ai.pathsToMe[0][1]+gameState.ai.pathsToMe[1][1])/2.0]; else if (gameState.ai.pathsToMe.length !== 0) var position = [gameState.ai.pathsToMe[0][0],gameState.ai.pathsToMe[0][1]]; else var position = [-1,-1]; if (gameState.ai.accessibility.getAccessValue(position) !== gameState.ai.myIndex) var position = [-1,-1]; var nearestCCArray = CCs.filterNearest(position, 1).toEntityArray(); var CCpos = nearestCCArray[0].position(); this.rallyPoint = [0,0]; if (position[0] !== -1) { this.rallyPoint[0] = position[0]; this.rallyPoint[1] = position[1]; } else { this.rallyPoint[0] = CCpos[0]; this.rallyPoint[1] = CCpos[1]; } if (type == 'harass_raid') { this.rallyPoint[0] = (position[0]*3.9 + 0.1 * CCpos[0]) / 4.0; this.rallyPoint[1] = (position[1]*3.9 + 0.1 * CCpos[1]) / 4.0; } } // some variables for during the attack this.position5TurnsAgo = [0,0]; this.lastPosition = [0,0]; this.position = [0,0]; this.threatList = []; // sounds so FBI this.tactics = undefined; this.assignUnits(gameState); //m.debug ("Before"); //Engine.DumpHeap(); // get a good path to an estimated target. this.pathFinder = new API3.aStarPath(gameState,false,false, this.targetPlayer); //Engine.DumpImage("widthmap.png", this.pathFinder.widthMap, this.pathFinder.width,this.pathFinder.height,255); this.pathWidth = 6; // prefer a path far from entities. This will avoid units getting stuck in trees and also results in less straight paths. this.pathSampling = 2; this.onBoat = false; // tells us if our units are loaded on boats. this.needsShip = false; //m.debug ("after"); //Engine.DumpHeap(); return true; }; m.CityAttack.prototype.getName = function(){ return this.name; }; m.CityAttack.prototype.getType = function(){ return this.type; }; // Returns true if the attack can be executed at the current time // Basically his checks we have enough units. // We run a count of our units. m.CityAttack.prototype.canStart = function(gameState){ for (var unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit["minSize"]) { m.debug(unitCat + " doesn't have enough units : " + this.unit[unitCat].length); return false; } } return true; // TODO: check if our target is valid and a few other stuffs (good moment to attack?) }; m.CityAttack.prototype.isStarted = function(){ if ((this.state !== "unexecuted")) m.debug ("Attack plan already started"); return !(this.state == "unexecuted"); }; m.CityAttack.prototype.isPaused = function(){ return this.paused; }; m.CityAttack.prototype.setPaused = function(gameState, boolValue){ if (!this.paused && boolValue === true) { this.pausingStart = gameState.getTimeElapsed(); this.paused = true; m.debug ("Pausing attack plan " +this.name); } else if (this.paused && boolValue === false) { this.totalPausingTime += gameState.getTimeElapsed() - this.pausingStart; this.paused = false; m.debug ("Unpausing attack plan " +this.name); } }; m.CityAttack.prototype.mustStart = function(gameState){ if (this.isPaused() || this.path === undefined) return false; var MaxReachedEverywhere = true; for (var unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit["targetSize"]) { MaxReachedEverywhere = false; } } if (MaxReachedEverywhere || (gameState.getPopulationMax() - gameState.getPopulation() < 10 && this.canStart(gameState))) return true; return (this.maxPreparationTime + this.timeOfPlanStart + this.totalPausingTime < gameState.getTimeElapsed()); }; // Adds a build order. If resetQueue is true, this will reset the queue. m.CityAttack.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { m.debug ("Adding a build order for " + name); // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; var Unit = this.unitStat[name]; var filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); this.unit[name] = gameState.getOwnUnits().filter(filter); this.unit[name].registerUpdates(); this.buildOrder.push([0, Unit["classes"], this.unit[name], Unit, name]); if (resetQueue) { this.queue.empty(); this.queueChamp.empty(); } } }; // Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start" // 3 is a special case: no valid path returned. Right now I stop attacking alltogether. m.CityAttack.prototype.updatePreparation = function(gameState, HQ,events) { var self = this; if (this.path == undefined || this.target == undefined || this.path === "toBeContinued") { // find our target if (this.target == undefined) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0) targets = this.defaultTargetFinder(gameState, HQ); if (targets.length !== 0) { m.debug ("Aiming for " + targets); // picking a target var maxDist = -1; var index = 0; for (var i in targets._entities) { // we're sure it has a position has TargetFinder already checks that. var dist = API3.SquareVectorDistance(targets._entities[i].position(), this.rallyPoint); if (dist < maxDist || maxDist === -1) { maxDist = dist; index = i; } } this.target = targets._entities[index]; this.targetPos = this.target.position(); } } // when we have a target, we path to it. // I'd like a good high width sampling first. // Thus I will not do everything at once. // It will probably carry over a few turns but that's no issue. if (this.path === undefined) this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, this.pathSampling, this.pathWidth,175);//,gameState); else if (this.path === "toBeContinued") this.path = this.pathFinder.continuePath();//gameState); if (this.path === undefined) { if (this.pathWidth == 6) { this.pathWidth = 2; delete this.path; } else { delete this.pathFinder; return 3; // no path. } } else if (this.path === "toBeContinued") { // carry on. } else if (this.path[1] === true && this.pathWidth == 2) { // okay so we need a ship. // Basically we'll add it as a new class to train compulsorily, and we'll recompute our path. if (!gameState.ai.HQ.waterMap) { m.debug ("This is actually a water map."); gameState.ai.HQ.waterMap = true; return 0; } m.debug ("We need a ship."); this.needsShip = true; this.pathWidth = 3; this.pathSampling = 3; this.path = this.path[0].reverse(); delete this.pathFinder; // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) for (var i = 0; i < this.path.length; ++i) { // my pathfinder returns arrays in arrays in arrays. var waypointPos = this.path[i][0]; var territory = m.createTerritoryMap(gameState); if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) { // if we're suddenly out of our territory or this is the point where we change transportation method. if (i !== 0) this.rallyPoint = this.path[i-1][0]; else this.rallyPoint = this.path[0][0]; if (i >= 1) this.path.splice(0,i-1); break; } } } else if (this.path[1] === true && this.pathWidth == 6) { // retry with a smaller pathwidth: this.pathWidth = 2; delete this.path; } else { this.path = this.path[0].reverse(); delete this.pathFinder; // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) for (var i = 0; i < this.path.length; ++i) { // my pathfinder returns arrays in arrays in arrays. var waypointPos = this.path[i][0]; var territory = m.createTerritoryMap(gameState); if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) { // if we're suddenly out of our territory or this is the point where we change transportation method. if (i !== 0) { this.rallyPoint = this.path[i-1][0]; } else this.rallyPoint = this.path[0][0]; if (i >= 1) this.path.splice(0,i-1); break; } } } } Engine.ProfileStart("Update Preparation"); // special case: if we're reached max pop, and we can start the plan, start it. if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) && this.canStart()) { this.assignUnits(gameState); this.queue.empty(); this.queueChamp.empty(); if ( gameState.ai.playedTurn % 5 == 0) this.AllToRallyPoint(gameState, true); } else if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)) { // keep on while the units finish being trained, then we'll start this.assignUnits(gameState); this.queue.empty(); this.queueChamp.empty(); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, true); // TODO: should use this time to let gatherers deposit resources. } Engine.ProfileStop(); return 1; } else if (!this.mustStart(gameState)) { // We still have time left to recruit units and do stuffs. // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. this.buildOrder.sort(function (a,b) { //}) { var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); a[0] = (a[2].length + aQueued)/a[3]["targetSize"]; var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); b[0] = (b[2].length + bQueued)/b[3]["targetSize"]; a[0] -= a[3]["priority"]; b[0] -= b[3]["priority"]; return (a[0]) - (b[0]); }); this.assignUnits(gameState); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, false); this.unitCollection.setStance("standground"); // make sure units won't disperse out of control } Engine.ProfileStart("Creating units."); // gets the number in training of the same kind as the first one. var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); var queued = this.queue.countQueuedUnitsWithMetadata("special",specialData) + this.queueChamp.countQueuedUnitsWithMetadata("special",specialData) if (queued + inTraining + this.buildOrder[0][2].length <= this.buildOrder[0][3]["targetSize"]) { // find the actual queue we want var queue = this.queue; if (this.buildOrder[0][3]["classes"].indexOf("Champion") !== -1) queue = this.queueChamp; if (this.buildOrder[0][0] < 1 && queue.length() <= 5) { var template = HQ.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); //m.debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan. if (template === undefined) { // TODO: this is a complete hack. delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. this.buildOrder.splice(0,1); } else { var max = this.buildOrder[0][3]["batchSize"]; // TODO: this should be plan dependant. if (gameState.getTimeElapsed() > 1800000) max *= 2; if (gameState.getTemplate(template).hasClass("CitizenSoldier")) queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); else queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); } } } /* if (!this.startedPathing && this.path === undefined) { // find our target var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); var target = targets.toEntityArray()[rand]; this.targetPos = target.position(); count++; if (count > 1000){ m.debug("No target with a valid position found"); + Engine.ProfileStop(); + Engine.ProfileStop(); return false; } } this.startedPathing = true; // Start pathfinding using the optimized version, with a minimal sampling of 2 this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2, gameState); } } else if (this.startedPathing) { var path = this.pathFinder.continuePath(gameState); if (path !== "toBeContinued") { this.startedPathing = false; this.path = path; m.debug("Pathing ended"); } } */ Engine.ProfileStop(); Engine.ProfileStop(); // can happen for now if (this.buildOrder.length === 0) { m.debug ("Ending plan: no build orders"); return 0; // will abort the plan, should return something else } return 1; } this.unitCollection.forEach(function (entity) { entity.setMetadata(PlayerID, "role","attack"); }); Engine.ProfileStop(); // if we're here, it means we must start (and have no units in training left). // if we can, do, else, abort. if (this.canStart(gameState)) return 2; else return 0; return 0; }; m.CityAttack.prototype.assignUnits = function(gameState){ var self = this; // TODO: assign myself units that fit only, right now I'm getting anything. // Assign all no-roles that fit (after a plan aborts, for example). var NoRole = gameState.getOwnEntitiesByRole(undefined, false); if (this.type === "rush") NoRole = gameState.getOwnEntitiesByRole("worker", true); NoRole.forEach(function(ent) { if (ent.hasClass("Unit") && ent.attackTypes() !== undefined) { if (ent.hasClasses(["CitizenSoldier", "Infantry"])) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "plan", self.name); } }); }; // this sends a unit by ID back to the "rally point" m.CityAttack.prototype.ToRallyPoint = function(gameState,id) { // Move back to nearest rallypoint gameState.getEntityById(id).move(this.rallyPoint[0],this.rallyPoint[1]); } // this sends all units back to the "rally point" by entity collections. // It doesn't disturb ones that could be currently defending, even if the plan is not (yet) paused. m.CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { var self = this; if (evenWorkers) { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "defence") { ent.setMetadata(PlayerID,"role", "attack"); ent.move(self.rallyPoint[0],self.rallyPoint[1]); } }); } } else { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence") ent.move(self.rallyPoint[0],self.rallyPoint[1]); }); } } } // Default target finder aims for conquest critical targets m.CityAttack.prototype.defaultTargetFinder = function(gameState, HQ){ var targets = undefined; targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("CivCentre")); if (targets.length == 0) { targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("ConquestCritical")); } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("Town")); } if (targets.length == 0) { targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("Village")); } // no buildings, attack anything conquest critical, even units (it's assuming it won't move). if (targets.length == 0) { targets = gameState.getEnemyEntities(this.targetPlayer).filter(API3.Filters.byClass("ConquestCritical")); } return targets; }; // tupdate m.CityAttack.prototype.raidingTargetFinder = function(gameState, HQ, Target){ var targets = undefined; if (Target == "villager") { // let's aim for any resource dropsite. We assume villagers are in the neighborhood (note: the human player could certainly troll us... small (scouting) TODO here.) targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.resourceDropsiteTypes() !== undefined && !ent.hasClass("CivCentre") && ent.owner() === this.targetPlayer && ent.position()); }); if (targets.length == 0) { targets = gameState.entities.filter(function(ent) { return (ent.hasClass("CivCentre") && ent.resourceDropsiteTypes() !== undefined && ent.owner() === this.targetPlayer && ent.position()); }); } if (targets.length == 0) { // if we're here, it means they also don't have no CC... So I'll just take any building at this point. targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.owner() === this.targetPlayer && ent.position()); }); } return targets; } else { return this.defaultTargetFinder(gameState, HQ); } }; // Executes the attack plan, after this is executed the update function will be run every turn // If we're here, it's because we have in our IDlist enough units. // now the IDlist units are treated turn by turn m.CityAttack.prototype.StartAttack = function(gameState, HQ){ // check we have a target and a path. if (this.targetPos && this.path !== undefined) { // erase our queue. This will stop any leftover unit from being trained. gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); var curPos = this.unitCollection.getCentrePosition(); this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "subrole", "walking"); ent.setMetadata(PlayerID, "role", "attack") ;}); // optimize our collection now. this.unitCollection.freeze(); this.unitCollection.allowQuickIter(); this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); this.unitCollection.setStance("aggressive"); this.unitCollection.filter(API3.Filters.byClass("Siege")).setStance("defensive"); this.state = "walking"; } else { gameState.ai.gameFinished = true; m.debug ("I do not have any target. So I'll just assume I won the game."); return false; } return true; }; // Runs every turn after the attack is executed m.CityAttack.prototype.update = function(gameState, HQ, events){ var self = this; Engine.ProfileStart("Update Attack"); // we're marching towards the target // Check for attacked units in our band. var bool_attacked = false; // raids don't care about attacks much if (this.unitCollection.length === 0) { Engine.ProfileStop(); return 0; } this.position = this.unitCollection.getCentrePosition(); var IDs = this.unitCollection.toIdArray(); // this actually doesn't do anything right now. if (this.state === "walking") { var attackedNB = 0; var toProcess = {}; var armyToProcess = {}; // Let's check if any of our unit has been attacked. In case yes, we'll determine if we're simply off against an enemy army, a lone unit/builing // or if we reached the enemy base. Different plans may react differently. var attackedEvents = events["Attacked"]; for (var key in attackedEvents) { var e = attackedEvents[key]; if (IDs.indexOf(e.target) !== -1) { var attacker = gameState.getEntityById(e.attacker); var ourUnit = gameState.getEntityById(e.target); if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { attackedNB++; //if (HQ.enemyWatchers[attacker.owner()]) { //toProcess[attacker.id()] = attacker; //var armyID = HQ.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id()); //armyToProcess[armyID[0]] = armyID[1]; //} } // if we're being attacked by a building, flee. if (attacker && ourUnit && attacker.hasClass("Structure")) { ourUnit.flee(attacker); } } } var territoryMap = m.createTerritoryMap(gameState); if ((territoryMap.getOwner(this.position) === this.targetPlayer && attackedNB > 1) || attackedNB > 4) { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } /* }&& this.type !== "harass_raid"){ // walking toward the target var sumAttackerPos = [0,0]; var numAttackers = 0; // let's check if one of our unit is not under attack, by any chance. for (var key in events){ var e = events[key]; if (e.type === "Attacked" && e.msg){ if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){ var attacker = HeadQuarters.entity(e.msg.attacker); if (attacker && attacker.position()){ sumAttackerPos[0] += attacker.position()[0]; sumAttackerPos[1] += attacker.position()[1]; numAttackers += 1; bool_attacked = true; // todo: differentiate depending on attacker type... If it's a ship, let's not do anythin, a building, depends on the attack type/ if (this.threatList.indexOf(e.msg.attacker) === -1) { var enemySoldiers = HeadQuarters.getEnemySoldiers().toEntityArray(); for (var j in enemySoldiers) { var enemy = enemySoldiers[j]; if (enemy.position() === undefined) // likely garrisoned continue; if (m.inRange(enemy.position(), attacker.position(), 1000) && this.threatList.indexOf(enemy.id()) === -1) this.threatList.push(enemy.id()); } this.threatList.push(e.msg.attacker); } } } } } if (bool_attacked > 0){ var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers]; units.move(avgAttackerPos[0], avgAttackerPos[1]); // let's run towards it. this.tactics = new Tactics(gameState,HeadQuarters, this.idList,this.threatList,true); this.state = "attacking_threat"; } }else if (this.state === "attacking_threat"){ this.tactics.eventMetadataCleanup(events,HeadQuarters); var removeList = this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); for (var i in removeList){ this.threatList.splice(this.threatList.indexOf(removeList[i]),1); } if (this.threatList.length <= 0) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = "walking"; units.move(this.path[0][0], this.path[0][1]); }else { this.tactics.reassignAttacks(HeadQuarters); } }*/ } if (this.state === "walking"){ this.position = this.unitCollection.getCentrePosition(); // probably not too good. if (!this.position) { Engine.ProfileStop(); return undefined; // should spawn an error. } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units var sieges = this.unitCollection.filter(API3.Filters.byClass("Siege")); var farthest = 0; var farthestEnt = -1; sieges.forEach (function (ent) { if (API3.SquareVectorDistance(ent.position(),self.position) > farthest) { farthest = API3.SquareVectorDistance(ent.position(),self.position); farthestEnt = ent; } }); if (farthestEnt !== -1) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. var walls = gameState.getEnemyEntities().filter(API3.Filters.and(API3.Filters.byOwner(this.targetPlayer), API3.Filters.byClass("StoneWall"))); var nexttoWalls = false; walls.forEach( function (ent) { if (!nexttoWalls && API3.SquareVectorDistance(self.position, ent.position()) < 800) nexttoWalls = true; }); // there are walls but we can attack if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).length !== 0) { m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy."); this.state = "arrived"; } else if (nexttoWalls) { // abort plan. m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and gives up."); Engine.ProfileStop(); return 0; } } // check if our land units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 9000 || API3.SquareVectorDistance(this.position, this.path[0][0]) < 650) { if (this.unitCollection.filter(API3.Filters.byClass("Siege")).length !== 0 && API3.SquareVectorDistance(this.position, this.targetPos) >= 9000 && API3.SquareVectorDistance(this.unitCollection.filter(API3.Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) { } else { // okay so here basically two cases. First case is "we've arrived" // Second case is "either we need a boat, or we need to unload" if (this.path[0][1] !== true) { this.path.shift(); if (this.path.length > 0){ this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); } else { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } } else { // TODO: make this require an escort later on. this.path.shift(); if (this.path.length === 0) { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } else { /* var plan = new m.TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); this.tpPlanID = plan.ID; HQ.navalManager.transportPlans.push(plan); m.debug ("Transporting over sea"); this.state = "transporting"; */ // TODO: fix this above //right now we'll abort. Engine.ProfileStop(); return 0; } } } } } else if (this.state === "transporting") { // check that we haven't finished transporting, ie the plan if (!HQ.navalManager.checkActivePlan(this.tpPlanID)) { this.state = "walking"; } } // todo: re-implement raiding if (this.state === "arrived"){ // let's proceed on with whatever happens now. // There's a ton of TODOs on this part. if (this.onArrivalReaction == "proceedOnTargets") { this.state = ""; this.unitCollection.forEach( function (ent) { //}) { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); } else if (this.onArrivalReaction == "huntVillagers") { // let's get any villager and target them with a tactics manager var enemyCitizens = gameState.entities.filter(function(ent) { return (gameState.isEntityEnemy(ent) && ent.hasClass("Support") && ent.owner() !== 0 && ent.position()); }); var targetList = []; enemyCitizens.forEach( function (enemy) { if (m.inRange(enemy.position(), units.getCentrePosition(), 2500) && targetList.indexOf(enemy.id()) === -1) targetList.push(enemy.id()); }); if (targetList.length > 0) { this.tactics = new Tactics(gameState,HeadQuarters, this.idList,targetList); this.state = "huntVillagers"; var arrivedthisTurn = true; } else { this.state = ""; var arrivedthisTurn = true; } } } // basic state of attacking. if (this.state === "") { // events watch: if siege units are attacked, we'll send some units to deal with enemies. var attackedEvents = events["Attacked"]; for (var key in attackedEvents) { var e = attackedEvents[key]; if (IDs.indexOf(e.target) !== -1) { var attacker = gameState.getEntityById(e.attacker); var ourUnit = gameState.getEntityById(e.target); if (!attacker || !attacker.position() || !attacker.hasClass("Unit") || attacker.owner() === 0 || attacker.owner() === PlayerID) continue; if (!ourUnit.hasClass("Siege")) continue; var collec = this.unitCollection.filter(Filters.not(Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5).toEntityArray(); if (collec.length === 0) continue; collec.attack(attacker.id()) } } var enemyUnits = gameState.getEnemyUnits(this.targetPlayer); var enemyStructures = gameState.getEnemyStructures(this.targetPlayer); if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0) { this.unitCollUpdateArray = this.unitCollection.toIdArray(); } else { // some stuffs for locality and speed var territoryMap = m.createTerritoryMap(gameState); var timeElapsed = gameState.getTimeElapsed(); // Let's check a few units each time we update. Currently 10 var lgth = Math.min(this.unitCollUpdateArray.length,10); for (var check = 0; check < lgth; check++) { var ent = gameState.getEntityById(this.unitCollUpdateArray[0]); if (!ent) continue; // if the unit is in my territory, make it move towards the target. if (territoryMap.point(ent.position()) - 64 === PlayerID) { ent.move(this.targetPos[0],this.targetPos[1]); continue; } var orderData = ent.unitAIOrderData(); if (orderData.length !== 0) orderData = orderData[0]; else orderData = undefined; // update it. var needsUpdate = false; if (ent.isIdle()) needsUpdate = true; else if (ent.hasClass("Siege") && (!orderData || !orderData["target"] || !gameState.getEntityById(orderData["target"]) || !gameState.getEntityById(orderData["target"]).hasClass("ConquestCritical")) ) needsUpdate = true; else if (!ent.hasClass("Siege") && orderData && orderData["target"] && gameState.getEntityById(orderData["target"]) && gameState.getEntityById(orderData["target"]).hasClass("Structure")) needsUpdate = true; // try to make it attack a unit instead // don't update too soon. if (timeElapsed - ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") < 10000) continue; if (needsUpdate === false && !arrivedthisTurn) continue; ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed); // let's filter targets further based on this unit. var mStruct = enemyStructures.filter(function (enemy) { //}){ if (!enemy.position() || (enemy.hasClass("StoneWall") && ent.canAttackClass("StoneWall"))) { return false; } if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 3000) { return false; } return true; }); var mUnit = enemyUnits.filter(function (enemy) { if (!enemy.position()) return false; if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 10000) return false; return true; }); // Checking for gates if we're a siege unit. var isGate = false; mUnit = mUnit.toEntityArray(); mStruct = mStruct.toEntityArray(); if (ent.hasClass("Siege")) { mStruct.sort(function (structa,structb) { var vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) { // we hate gates isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 200; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) { // we hate gates isGate = true; valb += 10000; } else if (structb.hasClass("ConquestCritical")) valb += 200; //warn ("Structure " +structa.genericName() + " is worth " +vala); //warn ("Structure " +structb.genericName() + " is worth " +valb); return (valb - vala); }); // TODO: handle ballistas here if (mStruct.length !== 0) { if (isGate) ent.attack(mStruct[0].id()); else { var rand = Math.floor(Math.random() * mStruct.length*0.2); ent.attack(mStruct[+rand].id()); //m.debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ) { //m.debug ("Siege units moving to " + uneval(self.targetPos)); ent.move(self.targetPos[0],self.targetPos[1]); } } else { if (mUnit.length !== 0) { mUnit.sort(function (unitA,unitB) { var vala = unitA.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitA.classes())) vala += 100; var valb = unitB.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitB.classes())) valb += 100; return valb - vala; }); var rand = Math.floor(Math.random() * mUnit.length*0.1); ent.attack(mUnit[(+rand)].id()); //m.debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ //m.debug ("Units moving to " + uneval(self.targetPos)); ent.move(self.targetPos[0],self.targetPos[1]); } else if (mStruct.length !== 0) { mStruct.sort(function (structa,structb) { //}){ var vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 100; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; valb += 10000; } else if (structb.hasClass("ConquestCritical")) valb += 100; return (valb - vala); }); if (isGate) ent.attack(mStruct[0].id()); else { var rand = Math.floor(Math.random() * mStruct.length*0.1); ent.attack(mStruct[+rand].id()); //m.debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } } } } this.unitCollUpdateArray.splice(0,10); } // updating targets. if (!gameState.getEntityById(this.target.id())) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { m.debug ("Seems like our target has been destroyed. Switching."); m.debug ("Aiming for " + targets); // picking a target this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); this.target = targets.toEntityArray()[rand]; this.targetPos = this.target.position(); count++; if (count > 1000){ m.debug("No target with a valid position found"); Engine.ProfileStop(); return false; } } } } // regularly update the target position in case it's a unit. if (this.target.hasClass("Unit")) this.targetPos = this.target.position(); } /* if (this.state === "huntVillagers") { this.tactics.eventMetadataCleanup(events,HeadQuarters); this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); if (this.tactics.isBattleOver()) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = ""; return 0; // assume over } else this.tactics.reassignAttacks(HeadQuarters); }*/ this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; m.CityAttack.prototype.totalCountUnits = function(gameState){ var totalcount = 0; for (var i in this.idList) { totalcount++; } return totalcount; }; // reset any units m.CityAttack.prototype.Abort = function(gameState){ this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "role",undefined); ent.setMetadata(PlayerID, "subrole",undefined); ent.setMetadata(PlayerID, "plan",undefined); }); for (var unitCat in this.unitStat) { delete this.unitStat[unitCat]; delete this.unit[unitCat]; } delete this.unitCollection; gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); }; return m; }(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/base-manager.js (revision 14588) @@ -1,1034 +1,1030 @@ var AEGIS = function(m) { /* Base Manager * Handles lower level economic stuffs. * Some tasks: -tasking workers: gathering/hunting/building/repairing?/scouting/plans. -giving feedback/estimates on GR -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans, if I ever get any. -getting good spots for dropsites -managing dropsite use in the base > warning HQ if we'll need more space -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ m.BaseManager = function(Config) { this.Config = Config; this.farmingFields = false; this.ID = m.playerGlobals[PlayerID].uniqueIDBases++; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; // list of IDs of buildings in our base that have a "territory pusher" function. this.territoryBuildings = []; // will tell if we should be considered as a source of X. this.willGather = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.isFarming = false; this.isHunting = true; this.constructing = false; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; }; m.BaseManager.prototype.init = function(gameState, unconstructed){ this.constructing = unconstructed; // entitycollections this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID,"role","worker")); this.workers.allowQuickIter(); this.buildings.allowQuickIter(); this.units.allowQuickIter(); this.units.registerUpdates(); this.buildings.registerUpdates(); this.workers.registerUpdates(); // array of entity IDs, with each being // { "food" : [close entities, semi-close entities, faraway entities, closeAmount, medianAmount, assignedWorkers collection ] … } (one per resource) // note that "median amount" also counts the closeAmount. this.dropsites = { }; // TODO: difficulty levels for this? // smallRadius is the distance necessary to mark a resource as linked to a dropsite. this.smallRadius = { 'food':40*40,'wood':50*50,'stone':40*40,'metal':40*40 }; // medRadius is the maximal distance for a link, albeit one that would still make us want to build a new dropsite. this.medRadius = { 'food':70*70,'wood':55*55,'stone':80*80,'metal':80*80 }; // bigRadius is the distance for a weak link, mainly for optimizing search for resources when a DP is depleted. this.bigRadius = { 'food':70*70,'wood':200*200,'stone':200*200,'metal':200*200 }; }; m.BaseManager.prototype.assignEntity = function(unit){ unit.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(unit); this.workers.updateEnt(unit); this.buildings.updateEnt(unit); // TODO: immediately assign it some task? if (unit.hasClass("Structure") && unit.hasTerritoryInfluence() && this.territoryBuildings.indexOf(unit.id()) === -1) this.territoryBuildings.push(unit.id()); }; m.BaseManager.prototype.setAnchor = function(anchorEntity) { if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence()) { warn("Error: Aegis' base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.") return false; } this.anchor = anchorEntity; this.anchor.setMetadata(PlayerID, "base", this.ID); this.anchor.setMetadata(PlayerID, "baseAnchor", true); this.buildings.updateEnt(this.anchor); if (this.territoryBuildings.indexOf(this.anchor.id()) === -1) this.territoryBuildings.push(this.anchor.id()); return true; } // affects the HQ map. m.BaseManager.prototype.initTerritory = function(HQ, gameState) { if (!this.anchor) warn ("Error: Aegis tried to initialize the territory of base " + this.ID + " without assigning it an anchor building first"); var radius = Math.round((this.anchor.territoryInfluenceRadius() / 4.0) * 1.25); var LandSize = gameState.sharedScript.accessibility.getRegionSize(this.anchor.position()); this.accessIndex = gameState.sharedScript.accessibility.getAccessValue(this.anchor.position()); if (LandSize < 6500) { // We're on a small land, we'll assign all territories in the vicinity. // there's a slight chance we're on an elongated weird stuff, we'll just pump up a little the radius radius = Math.round(radius*1.2); } var x = Math.round(this.anchor.position()[0]/gameState.cellSize); var y = Math.round(this.anchor.position()[1]/gameState.cellSize); this.territoryIndices = []; var width = gameState.getMap().width; for (var xi = -radius; xi <= radius; ++xi) for (var yi = -radius; yi <= radius; ++yi) { if (x+xi >= width || y+yi >= width) continue; if (xi*xi+yi*yi < radius*radius && HQ.basesMap.map[(x+xi) + (y+yi)*width] === 0) { if (this.accessIndex == gameState.sharedScript.accessibility.landPassMap[x+xi + width*(y+yi)]) { this.territoryIndices.push((x+xi) + (y+yi)*width); HQ.basesMap.map[(x+xi) + (y+yi)*width] = this.ID; } } } } m.BaseManager.prototype.initGatheringFunctions = function(HQ, gameState, specTypes) { // init our gathering functions. var types = ["food","wood","stone","metal"]; if (specTypes !== undefined) type = specTypes; var self = this; var count = 0; for (var i in types) { var type = types[i]; // TODO: set us as "X" gatherer this.buildings.filter(API3.Filters.isDropsite(type)).forEach(function(ent) { self.initializeDropsite(gameState, ent,type) }); if (this.getResourceLevel(gameState, type, "all") > 1000) this.willGather[type] = 1; } if (this.willGather["food"] === 0) { var needFarm = true; // Let's check again for food for (var base in HQ.baseManagers) if (HQ.baseManagers[base].willGather["food"] === 1) needFarm = false; if (needFarm) this.willGather["food"] = 1; } m.debug ("food" + this.willGather["food"]); m.debug (this.willGather["wood"]); m.debug (this.willGather["stone"]); m.debug (this.willGather["metal"]); } m.BaseManager.prototype.checkEvents = function (gameState, events, queues) { var destEvents = events["Destroy"]; var createEvents = events["Create"]; var cFinishedEvents = events["ConstructionFinished"]; for (var i in destEvents) { var evt = destEvents[i]; // let's check we haven't lost an important building here. if (evt != undefined && !evt.SuccessfulFoundation && evt.entityObj != undefined && evt.metadata !== undefined && evt.metadata[PlayerID] && evt.metadata[PlayerID]["base"] !== undefined && evt.metadata[PlayerID]["base"] == this.ID) { var ent = evt.entityObj; if (ent.hasTerritoryInfluence()) this.territoryBuildings.splice(this.territoryBuildings.indexOf(ent.id()),1); if (ent.resourceDropsiteTypes()) this.scrapDropsite(gameState, ent); if (evt.metadata[PlayerID]["baseAnchor"] && evt.metadata[PlayerID]["baseAnchor"] == true) { // sounds like we lost our anchor. Let's try rebuilding it. // TODO: currently the HQ manager sets us as initgathering, we probably ouht to do it this.anchor = undefined; this.constructing = true; // let's switch mode. this.workers.forEach( function (worker) { worker.stopMoving(); }); if (ent.hasClass("CivCentre")) { // TODO: might want to tell the queue manager to pause other stuffs if we are the only base. queues.civilCentre.addItem(gameState, new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, ent.position())); } else { // TODO queues.civilCentre.addItem(gameState, new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base" : this.ID, "baseAnchor" : true }, ent.position())); } } } } for (var i in cFinishedEvents) { var evt = cFinishedEvents[i]; if (evt && evt.newentity) { // TODO: we ought to add new resources or do something. var ent = gameState.getEntityById(evt.newentity); if (ent === undefined) continue; if (ent.getMetadata(PlayerID,"base") == this.ID) { if(ent.hasTerritoryInfluence()) this.territoryBuildings.push(ent.id()); if (ent.resourceDropsiteTypes()) for (var ress in ent.resourceDropsiteTypes()) this.initializeDropsite(gameState, ent, ent.resourceDropsiteTypes()[ress]); if (ent.resourceSupplyAmount() && ent.resourceSupplyType()["specific"] == "grain") this.assignResourceToDP(gameState,ent); } } } for (var i in createEvents) { var evt = createEvents[i]; // Checking for resources. var evt = events[i]; if (evt && evt.entity) { var ent = gameState.getEntityById(evt.entity); if (ent === undefined) continue; if (ent.resourceSupplyAmount() && ent.owner() == 0) this.assignResourceToDP(gameState,ent); } } }; // If no specific dropsite, it'll assign to the closest m.BaseManager.prototype.assignResourceToDP = function (gameState, supply, specificDP) { var type = supply.resourceSupplyType()["generic"]; if (type == "treasure") type = supply.resourceSupplyType()["specific"]; if (!specificDP) { var closest = -1; var dist = Math.min(); for (var i in this.dropsites) { var dp = gameState.getEntityById(i); var distance = API3.SquareVectorDistance(supply.position(), dp.position()); if (distance < dist && distance < this.bigRadius[type]) { closest = dp.id(); dist = distance; } } if (closest !== -1) { supply.setMetadata(PlayerID, "linked-dropsite-close", (dist < this.smallRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite-nearby", (dist < this.medRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite", closest ); supply.setMetadata(PlayerID, "linked-dropsite-dist", +dist); } } // TODO: ought to recount immediatly. } m.BaseManager.prototype.initializeDropsite = function (gameState, ent, type) { var count = 0, farCount = 0; var self = this; var resources = gameState.getResourceSupplies(type); // TODO: if we're initing, we should probably remove them anyway. if (self.dropsites[ent.id()] === undefined || self.dropsites[ent.id()][type] === undefined) { resources.filter( function (supply) { //}){ if (!supply.position() || !ent.position()) return; var distance = API3.SquareVectorDistance(supply.position(), ent.position()); if (supply.getMetadata(PlayerID, "linked-dropsite") == undefined || supply.getMetadata(PlayerID, "linked-dropsite-dist") > distance) { if (distance < self.bigRadius[type]) { supply.setMetadata(PlayerID, "linked-dropsite-close", (distance < self.smallRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite-nearby", (distance < self.medRadius[type]) ); supply.setMetadata(PlayerID, "linked-dropsite", ent.id() ); supply.setMetadata(PlayerID, "linked-dropsite-dist", +distance); if(distance < self.smallRadius[type]) count += supply.resourceSupplyAmount(); if (distance < self.medRadius[type]) farCount += supply.resourceSupplyAmount(); } } }); // This one is both for the nearby and the linked var filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite", ent.id()); var collection = resources.filter(filter); collection.registerUpdates(); filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-close",true); var collection2 = collection.filter(filter); collection2.registerUpdates(); filter = API3.Filters.byMetadata(PlayerID, "linked-dropsite-nearby",true); var collection3 = collection.filter(filter); collection3.registerUpdates(); filter = API3.Filters.byMetadata(PlayerID, "linked-to-dropsite", ent.id()); var WkCollection = this.workers.filter(filter); WkCollection.registerUpdates(); if (!self.dropsites[ent.id()]) self.dropsites[ent.id()] = {}; self.dropsites[ent.id()][type] = [collection2,collection3, collection, count, farCount, WkCollection]; // TODO: flag us on the SharedScript "type" map. // TODO: get workers on those resources and do something with them. } if (m.DebugEnabled()) { // Make resources glow wildly if (type == "food") { self.dropsites[ent.id()][type][2].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,0,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]}); }); } if (type == "wood") { self.dropsites[ent.id()][type][2].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]}); }); } if (type == "stone") { self.dropsites[ent.id()][type][2].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0.5,0.5,0]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,10,0]}); }); } if (type == "metal") { self.dropsites[ent.id()][type][2].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,0.5]}); }); self.dropsites[ent.id()][type][1].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,2,2]}); }); self.dropsites[ent.id()][type][0].forEach(function(ent){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,10]}); }); } } }; // completely and "safely" remove a dropsite from our list. // this also removes any linked resource and so on. // TODO: should re-add the resources to another dropsite. m.BaseManager.prototype.scrapDropsite = function (gameState, ent) { if (this.dropsites[ent.id()] === undefined) return true; for (var i in this.dropsites[ent.id()]) { var type = i; var dp = this.dropsites[ent.id()][i]; dp[2].forEach(function (supply) { //}){ supply.deleteMetadata(PlayerID,"linked-dropsite-nearby"); supply.deleteMetadata(PlayerID,"linked-dropsite-close"); supply.deleteMetadata(PlayerID,"linked-dropsite"); supply.deleteMetadata(PlayerID,"linked-dropsite-dist"); }); dp[5].forEach(function (worker) { worker.deleteMetadata(PlayerID,"linked-to-dropsite"); // TODO: should probably stop the worker or something. }); dp = [undefined, undefined, undefined, 0, 0, undefined]; delete this.dropsites[ent.id()][i]; } this.dropsites[ent.id()] = undefined; delete this.dropsites[ent.id()]; return true; }; // Returns the position of the best place to build a new dropsite for the specified resource m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource){ var storeHousePlate = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse")); // 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. var territory = m.createTerritoryMap(gameState); var obstructions = m.createObstructionMap(gameState,this.accessIndex,storeHousePlate); obstructions.expandInfluences(); // copy the resource map as initialization. var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.resourceMaps[resource].map, true); var DPFoundations = gameState.getOwnFoundations().filter(API3.Filters.byType(gameState.applyCiv("foundation|structures/{civ}_storehouse"))); // TODO: might be better to check dropsites someplace else. // loop over this in this.terrytoryindices. It's usually a little too much, but it's always enough. for (var p = 0; p < this.territoryIndices.length; ++p) { var j = this.territoryIndices[p]; friendlyTiles.map[j] *= 1.5; // only add where the map is currently not null, ie in our territory and some "Resource" would be close. // This makes the placement go from "OK" to "human-like". for (var i in gameState.sharedScript.resourceMaps) if (friendlyTiles.map[j] !== 0 && i !== "food") friendlyTiles.map[j] += gameState.sharedScript.resourceMaps[i].map[j]; for (var i in this.dropsites) { var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; var dpPos = gameState.getEntityById(i).position(); if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) { friendlyTiles.map[j] = 0; continue; } else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) friendlyTiles.map[j] /= 2; } for (var i in DPFoundations._entities) { var pos = [j%friendlyTiles.width, Math.floor(j/friendlyTiles.width)]; var dpPos = gameState.getEntityById(i).position(); if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 250) friendlyTiles.map[j] = 0; else if (dpPos && API3.SquareVectorDistance(friendlyTiles.gamePosToMapPos(dpPos), pos) < 450) friendlyTiles.map[j] /= 2; } } if (m.DebugEnabled()) friendlyTiles.dumpIm("DP_" + resource + "_" + gameState.getTimeElapsed() + ".png"); var best = friendlyTiles.findBestTile(2, obstructions); // try to find a spot to place a DP. var bestIdx = best[0]; m.debug ("for dropsite best is " +best[1] + " at " + gameState.getTimeElapsed()); // tell the dropsite builder we haven't found anything satisfactory. if (best[1] < 60) return false; var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; return [x,z]; }; // update the resource level of a dropsite. m.BaseManager.prototype.updateDropsite = function (gameState, ent, type) { if (this.dropsites[ent.id()] === undefined || this.dropsites[ent.id()][type] === undefined) return undefined; // should initialize it first. var count = 0, farCount = 0; var resources = gameState.getResourceSupplies(type); this.dropsites[ent.id()][type][1].forEach( function (supply) { //}){ farCount += supply.resourceSupplyAmount(); }); this.dropsites[ent.id()][type][0].forEach( function (supply) { //}){ count += supply.resourceSupplyAmount(); }); this.dropsites[ent.id()][type][3] = count; this.dropsites[ent.id()][type][4] = farCount; return true; }; // Updates dropsites. m.BaseManager.prototype.updateDropsites = function (gameState) { // for each dropsite, recalculate for (var i in this.dropsites) { for (var type in this.dropsites[i]) { this.updateDropsite(gameState,gameState.getEntityById(i),type); } } }; // TODO: ought to be cached or something probably // Returns the number of slots available for workers here. // we're assuming Max - 3 for metal/stone mines, and 20 for any dropsite that has wood. // TODO: for wood might want to count the trees too. // TODO: this returns "future" worker capacity, might want to have a current one. m.BaseManager.prototype.getWorkerCapacity = function (gameState, type) { var count = 0; if (type == "food") return 1000000; // TODO: perhaps return something sensible here. if (type === "stone" || type === "metal") { for (var id in this.dropsites) if (this.dropsites[id][type]) this.dropsites[id][type][1].forEach(function (ent) {// }){ if (ent.resourceSupplyAmount() > 500) count += ent.maxGatherers() - 3; }); } else if (type === "wood") { for (var id in this.dropsites) if (this.dropsites[id][type] && (this.dropsites[id][type][4]) > 1000) count += Math.min(15, this.dropsites[id][type][4] / 200); } return count; }; // TODO: ought to be cached or something probably // Returns the amount of resource left m.BaseManager.prototype.getResourceLevel = function (gameState, type, searchType, threshold) { var count = 0; if (searchType == "all") { // return all resources in the base area. gameState.getResourceSupplies(type).filter(API3.Filters.byTerritory(gameState.ai.HQ.basesMap, this.ID)).forEach( function (ent) { //}){ count += ent.resourceSupplyAmount(); }); return count; } if (searchType == "dropsites") { // for each dropsite, recalculate for (var i in this.dropsites) if (this.dropsites[i][type] !== undefined) count += this.dropsites[i][type][4]; return count; } if (searchType == "dropsitesClose") { // for each dropsite, recalculate for (var i in this.dropsites) if (this.dropsites[i][type] !== undefined) count += this.dropsites[i][type][3]; return count; } if (searchType == "dropsites-dpcount") { var seuil = 800; if (threshold) seuil = threshold; // for each dropsite, recalculate for (var i in this.dropsites) if (this.dropsites[i][type] !== undefined) { if (this.dropsites[i][type][4] > seuil) count++; } return count; } return 0; }; // check our resource levels and react accordingly m.BaseManager.prototype.checkResourceLevels = function (gameState,queues) { for (var type in this.willGather) { if (this.willGather[type] === 0) continue; if (type !== "food" && gameState.ai.playedTurn % 10 === 4 && this.getResourceLevel(gameState,type, "all") < 200) this.willGather[type] = 0; // won't gather at all if (this.willGather[type] === 2) continue; var count = this.getResourceLevel(gameState,type, "dropsites"); if (type == "food") { if (!this.isFarming && count < 1600 && queues.field.length === 0) { // tell the queue manager we'll be trying to build fields shortly. for (var i = 0; i < this.Config.Economy.initialFields;++i) { var plan = new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID }); plan.isGo = function() { return false; }; // don't start right away. queues.field.addItem(plan); } } else if (!this.isFarming && count < 400) { for (var i in queues.field.queue) queues.field.queue[i].isGo = function() { return true; }; // start them this.isFarming = true; } if (this.isFarming) { var numFarms = 0; this.buildings.filter(API3.Filters.byClass("Field")).forEach(function (field) { if (field.resourceSupplyAmount() > 400) numFarms++; }); var numFd = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_field"), true); numFarms += numFd; numFarms += queues.field.countQueuedUnits(); // let's see if we need to push new farms. if (numFd < 2) if (numFarms < Math.round(this.gatherersByType(gameState, "food").length / 4.6) || numFarms < Math.round(this.workers.length / 15.0)) queues.field.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base" : this.ID })); // TODO: refine count to only count my base. } } else if (queues.dropsites.length() === 0 && gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_storehouse"), true) === 0) { var wantedDPs = Math.ceil(this.gatherersByType(gameState, type).length / 12.0); var need = wantedDPs - this.getResourceLevel(gameState,type, "dropsites-dpcount",2000); if (need > 0) { var pos = this.findBestDropsiteLocation(gameState, type); if (!pos) { m.debug ("Found no right position for a " + type + " dropsite, going into \"noSpot\" mode"); this.willGather[type] = 2; // won't build // TODO: tell the HQ we'll be needing a new base for this resource, or tell it we've ran out of resource Z. } else { m.debug ("planning new dropsite for " + type); queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : this.ID }, pos)); } } } } }; // let's return the estimated gather rates. m.BaseManager.prototype.getGatherRates = function(gameState, currentRates) { for (var i in currentRates) { // I calculate the exact gathering rate for each unit. // I must then lower that to account for travel time. // Given that the faster you gather, the more travel time matters, // I use some logarithms. // TODO: this should take into account for unit speed and/or distance to target var units = this.gatherersByType(gameState, i); units.forEach(function (ent) { var gRate = ent.currentGatherRate() if (gRate !== undefined) currentRates[i] += Math.log(1+gRate)/1.1; }); if (i === "food") { units = this.workers.filter(API3.Filters.byMetadata(PlayerID, "subrole", "hunter")); units.forEach(function (ent) { var gRate = ent.currentGatherRate() if (gRate !== undefined) currentRates[i] += Math.log(1+gRate)/1.1; }); } } }; m.BaseManager.prototype.assignRolelessUnits = function(gameState) { // TODO: make this cleverer. var roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))); var self = this; roleless.forEach(function(ent) { if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier")) { if (ent.hasClass("Cavalry") && !self.isHunting) return; ent.setMetadata(PlayerID, "role", "worker"); } }); }; // If the numbers of workers on the resources is unbalanced then set some of workers to idle so // they can be reassigned by reassignIdleWorkers. // TODO: actually this probably should be in the HQ. m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState){ var self = this; if (gameState.currentPhase() < 2 && gameState.getTimeElapsed() < 360000) return; // not in the first phase or the first 6 minutes. var types = gameState.ai.queueManager.getAvailableResources(gameState); var bestType = ""; var avgOverdraft = 0; for (var i in types.types) avgOverdraft += types[types.types[i]]; avgOverdraft /= 4; for (var i in types.types) if (types[types.types[i]] > avgOverdraft + 200 || (types[types.types[i]] > avgOverdraft && avgOverdraft > 200)) if (this.gatherersByType(gameState,types.types[i]).length > 0) { // TODO: perhaps change this? var nb = 2; this.gatherersByType(gameState,types.types[i]).forEach( function (ent) { //}){ if (nb > 0) { //m.debug ("Moving " +ent.id() + " from " + types.types[i]); nb--; // TODO: might want to direct assign. ent.stopMoving(); ent.setMetadata(PlayerID, "subrole","idle"); } }); } //m.debug (currentRates); }; // TODO: work on this. m.BaseManager.prototype.reassignIdleWorkers = function(gameState) { var self = this; // Search for idle workers, and tell them to gather resources based on demand var filter = API3.Filters.or(API3.Filters.byMetadata(PlayerID,"subrole","idle"), API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"subrole"))); var idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers); if (idleWorkers.length) { idleWorkers.forEach(function(ent) { // Check that the worker isn't garrisoned if (ent.position() === undefined){ return; } if (ent.hasClass("Worker")) { var types = gameState.ai.HQ.pickMostNeededResources(gameState); //m.debug ("assigning " +ent.id() + " to " + types[0]); ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", types[0]); if (gameState.turnCache["gathererAssignementCache-" + types[0]]) gameState.turnCache["gathererAssignementCache-" + types[0]]++; else gameState.turnCache["gathererAssignementCache-" + types[0]] = 1; // Okay let's now check we can actually remain here for that if (self.willGather[types[0]] !== 1) { // TODO: if fail, we should probably pick the second most needed resource. gameState.ai.HQ.switchWorkerBase(gameState, ent, types[0]); } } else { ent.setMetadata(PlayerID, "subrole", "hunter"); } }); } }; m.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers, true); }; m.BaseManager.prototype.gatherersByType = function(gameState, type) { return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); }; // returns an entity collection of workers. // They are idled immediatly and their subrole set to idle. m.BaseManager.prototype.pickBuilders = function(gameState, number) { var collec = new API3.EntityCollection(gameState.sharedScript); // TODO: choose better. var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))).toEntityArray(); workers.sort(function (a,b) { var vala = 0, valb = 0; if (a.getMetadata(PlayerID,"subrole") == "builder") vala = 100; if (b.getMetadata(PlayerID,"subrole") == "builder") valb = 100; if (a.getMetadata(PlayerID,"plan") != undefined) vala = -100; if (b.getMetadata(PlayerID,"plan") != undefined) valb = -100; return a < b }); for (var i = 0; i < number; ++i) { workers[i].stopMoving(); workers[i].setMetadata(PlayerID, "subrole","idle"); collec.addEnt(workers[i]); } return collec; } m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { // If we have some foundations, and we don't have enough builder-workers, // try reassigning some other workers who are nearby // AI tries to use builders sensibly, not completely stopping its econ. var self = this; // TODO: this is not perfect performance-wise. var foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(),API3.Filters.not(API3.Filters.byClass("Field")))).toEntityArray(); var damagedBuildings = this.buildings.filter(function (ent) { if (ent.foundationProgress() === undefined && ent.needsRepair()) return true; return false; }).toEntityArray(); // Check if nothing to build if (!foundations.length && !damagedBuildings.length){ return; } var workers = this.workers.filter(API3.Filters.not(API3.Filters.byClass("Cavalry"))); var builderWorkers = this.workersBySubrole(gameState, "builder"); var idleBuilderWorkers = this.workersBySubrole(gameState, "builder").filter(API3.Filters.isIdle()); // if we're constructing and we have the foundations to our base anchor, only try building that. if (this.constructing == true && this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.byMetadata(PlayerID, "baseAnchor", true))).length !== 0) { foundations = this.buildings.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray(); var tID = foundations[0].id(); workers.forEach(function (ent) { //}){ var target = ent.getMetadata(PlayerID, "target-foundation"); if (target && target != tID) { ent.stopMoving(); ent.setMetadata(PlayerID, "target-foundation", tID); } }); } if (workers.length < 2) { var noobs = gameState.ai.HQ.bulkPickWorkers(gameState, this.ID, 2); if(noobs) { noobs.forEach(function (worker) { //}){ worker.setMetadata(PlayerID,"base", self.ID); worker.setMetadata(PlayerID,"subrole", "builder"); workers.updateEnt(worker); builderWorkers.updateEnt(worker); idleBuilderWorkers.updateEnt(worker); }); } } var addedWorkers = 0; var maxTotalBuilders = Math.ceil(workers.length * 0.15); if (this.constructing == true && maxTotalBuilders < 15) maxTotalBuilders = 15; for (var i in foundations) { var target = foundations[i]; if (target.hasClass("Field")) continue; // we do not build fields var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; var targetNB = this.Config.Economy.targetNumBuilders; // TODO: dynamic that. if (target.hasClass("House")) targetNB *= 2; else if (target.hasClass("Barracks")) targetNB = 4; else if (target.hasClass("Fortress")) targetNB = 7; if (target.getMetadata(PlayerID, "baseAnchor") == true) targetNB = 15; if (assigned < targetNB) { if (builderWorkers.length - idleBuilderWorkers.length + addedWorkers < maxTotalBuilders) { var addedToThis = 0; idleBuilderWorkers.forEach(function(ent) { if (ent.position() && API3.SquareVectorDistance(ent.position(), target.position()) < 10000 && assigned + addedToThis < targetNB) { addedWorkers++; addedToThis++; ent.setMetadata(PlayerID, "target-foundation", target.id()); } }); if (assigned + addedToThis < targetNB) { var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB - assigned - addedToThis); nearestNonBuilders.forEach(function(ent) { addedWorkers++; addedToThis++; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } } } } - // auras/techs are buggy and the AI tries to repair healthy buildings. - // TODO: reimplement once that's fixed. - return; - // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed. for (var i in damagedBuildings) { var target = damagedBuildings[i]; if (gameState.defcon() < 5) { if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) { continue; } } else if (noRepair && !target.hasClass("CivCentre")) continue; var territory = m.createTerritoryMap(gameState); if (territory.getOwner(target.position()) !== PlayerID || territory.getOwner([target.position()[0] + 5, target.position()[1]]) !== PlayerID) continue; var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; if (assigned < targetNB/3) { if (builderWorkers.length + addedWorkers < targetNB*2) { var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.position() !== undefined); }); if (gameState.defcon() < 5) nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata(PlayerID, "subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); }); var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), targetNB/3 - assigned); nearestNonBuilders.forEach(function(ent) { ent.stopMoving(); addedWorkers++; ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } } } }; m.BaseManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Base update - base " + this.ID); var self = this; this.updateDropsites(gameState); this.checkResourceLevels(gameState, queues); Engine.ProfileStart("Assign builders"); this.assignToFoundations(gameState); - Engine.ProfileStop() + Engine.ProfileStop(); if (this.constructing && this.anchor) { var terrMap = m.createTerritoryMap(gameState); if(terrMap.getOwner(this.anchor.position()) !== 0 && terrMap.getOwner(this.anchor.position()) !== PlayerID) { // we're in enemy territory. If we're too close from the enemy, destroy us. var eEnts = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); for (var i in eEnts) { var entPos = eEnts[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; if (API3.SquareVectorDistance(entPos, pos) < 500) this.anchor.destroy(); } } } // if (!this.constructing) // { if (gameState.ai.playedTurn % 2 === 0) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); /*Engine.ProfileStart("Swap Workers"); var gathererGroups = {}; gameState.getOwnEntitiesByRole("worker", true).forEach(function(ent){ }){ if (ent.hasClass("Cavalry")) return; var key = uneval(ent.resourceGatherRates()); if (!gathererGroups[key]){ gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []}; } if (ent.getMetadata(PlayerID, "gather-type") in gathererGroups[key]){ gathererGroups[key][ent.getMetadata(PlayerID, "gather-type")].push(ent); } }); for (var i in gathererGroups){ for (var j in gathererGroups){ var a = eval(i); var b = eval(j); if (a !== undefined && b !== undefined) if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){ for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){ gathererGroups[i]["wood"][k].setMetadata(PlayerID, "gather-type", "food"); gathererGroups[j]["food"][k].setMetadata(PlayerID, "gather-type", "wood"); } } } } Engine.ProfileStop();*/ // should probably be last to avoid reallocations of units that would have done stuffs otherwise. Engine.ProfileStart("Assigning Workers"); this.reassignIdleWorkers(gameState); Engine.ProfileStop(); // } // TODO: do this incrementally a la defence.js Engine.ProfileStart("Run Workers"); this.workers.forEach(function(ent) { if (!ent.getMetadata(PlayerID, "worker-object")) ent.setMetadata(PlayerID, "worker-object", new m.Worker(ent)); ent.getMetadata(PlayerID, "worker-object").update(self, gameState); }); Engine.ProfileStop(); Engine.ProfileStop(); }; return m; }(AEGIS); Index: ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/aegis/headquarters.js (revision 14588) @@ -1,1322 +1,1321 @@ var AEGIS = 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.targetNumBuilders = this.Config.Economy.targetNumBuilders; // number of workers we want building stuff this.dockStartTime = this.Config.Economy.dockStartTime * 1000; this.techStartTime = this.Config.Economy.techStartTime * 1000; this.dockFailed = false; // sanity check this.waterMap = false; // set by the aegis.js file. this.econState = "growth"; // existing values: growth, townPhasing. // tell if we can't gather from a resource type for sanity checks. this.outOf = { "food" : false, "wood" : false, "stone" : false, "metal" : false }; this.baseManagers = {}; // cache the rates. this.wantedRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRates = { "food": 0, "wood": 0, "stone":0, "metal": 0 }; this.currentRateLastUpdateTime = 0; // this means we'll have about a big third of women, and thus we can maximize resource gathering rates. this.femaleRatio = this.Config.Economy.femaleRatio; this.fortressStartTime = 0; this.fortressLapseTime = this.Config.Military.fortressLapseTime * 1000; this.defenceBuildingTime = this.Config.Military.defenceBuildingTime * 1000; this.attackPlansStartTime = this.Config.Military.attackPlansStartTime * 1000; this.defenceManager = new m.Defence(this.Config); this.navalManager = new m.NavalManager(); this.TotalAttackNumber = 0; this.upcomingAttacks = { "CityAttack" : [], "Rush" : [] }; this.startedAttacks = { "CityAttack" : [], "Rush" : [] }; }; // More initialisation for stuff that needs the gameState m.HQ.prototype.init = function(gameState, queues){ // initialize base map. Each pixel is a base ID, or 0 if none this.basesMap = new API3.Map(gameState.sharedScript, new Uint8Array(gameState.getMap().data.length)); this.basesMap.setMaxVal(255); if (this.Config.Economy.targetNumWorkers) this.targetNumWorkers = this.Config.Economy.targetNumWorkers; else if (this.targetNumWorkers === undefined) this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*(0.2 + Math.min(+(this.Config.difficulty)*0.125,0.3))), 1); // Let's get our initial situation here. // TODO: improve on this. // TODO: aknowledge bases, assign workers already. var ents = gameState.getEntities().filter(API3.Filters.byOwner(PlayerID)); var workersNB = 0; var hasScout = false; var treasureAmount = { 'food': 0, 'wood': 0, 'stone': 0, 'metal': 0 }; var hasCC = false; if (ents.filter(API3.Filters.byClass("CivCentre")).length > 0) hasCC = true; workersNB = ents.filter(API3.Filters.byClass("Worker")).length; if (ents.filter(API3.Filters.byClass("Cavalry")).length > 0) hasScout = true; // TODO: take multiple CCs into account. if (hasCC) { var CC = ents.filter(API3.Filters.byClass("CivCentre")).toEntityArray()[0]; for (var i in treasureAmount) gameState.getResourceSupplies(i).forEach( function (ent) { if (ent.resourceSupplyType().generic === "treasure" && API3.SquareVectorDistance(ent.position(), CC.position()) < 5000) treasureAmount[i] += ent.resourceSupplyMax(); }); this.baseManagers[1] = new m.BaseManager(this.Config); this.baseManagers[1].init(gameState); this.baseManagers[1].setAnchor(CC); this.baseManagers[1].initTerritory(this, gameState); this.baseManagers[1].initGatheringFunctions(this, gameState); if (m.DebugEnabled()) this.basesMap.dumpIm("basesMap.png"); var self = this; ents.forEach( function (ent) { //}){ self.baseManagers[1].assignEntity(ent); }); } // we now have enough data to decide on a few things. // TODO: here would be where we pick our initial strategy. // immediatly build a wood dropsite if possible. if (this.baseManagers[1]) { if (gameState.ai.queueManager.getAvailableResources(gameState)["wood"] >= 250) { var pos = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (pos) { queues.dropsites.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse",{ "base" : 1 }, pos)); queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_capacity_wheelbarrow")); } } } var map = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps["wood"].map); if (m.DebugEnabled()) map.dumpIm("map_CC_Wood.png"); //this.reassignIdleWorkers(gameState); this.navalManager.init(gameState, queues); this.defenceManager.init(gameState); // TODO: change that to something dynamic. var civ = gameState.playerData.civ; // load units and buildings from the config files if (civ in this.Config.buildings.moderate){ this.bModerate = this.Config.buildings.moderate[civ]; }else{ this.bModerate = this.Config.buildings.moderate['default']; } if (civ in this.Config.buildings.advanced){ this.bAdvanced = this.Config.buildings.advanced[civ]; }else{ this.bAdvanced = this.Config.buildings.advanced['default']; } if (civ in this.Config.buildings.fort){ this.bFort = this.Config.buildings.fort[civ]; }else{ this.bFort = this.Config.buildings.fort['default']; } for (var i in this.bAdvanced){ this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); } for (var i in this.bFort){ this.bFort[i] = gameState.applyCiv(this.bFort[i]); } }; m.HQ.prototype.checkEvents = function (gameState, events, queues) { // TODO: probably check stuffs like a base destruction. var CreateEvents = events["Create"]; var ConstructionEvents = events["ConstructionFinished"]; for (var i in CreateEvents) { var evt = CreateEvents[i]; // Let's check if we have a building set to create a new base. if (evt && evt.entity) { var ent = gameState.getEntityById(evt.entity); if (ent === undefined) continue; // happens when this message is right before a "Destroy" one for the same entity. if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "base") === -1) { // Okay so let's try to create a new base around this. var bID = m.playerGlobals[PlayerID].uniqueIDBases; this.baseManagers[bID] = new m.BaseManager(this.Config); this.baseManagers[bID].init(gameState, events, true); this.baseManagers[bID].setAnchor(ent); this.baseManagers[bID].initTerritory(this, gameState); // Let's get a few units out there to build this. // TODO: select the best base, or use multiple bases. var builders = this.bulkPickWorkers(gameState, bID, 10); if (builders !== false) { builders.forEach(function (worker) { worker.setMetadata(PlayerID, "base", bID); worker.setMetadata(PlayerID, "subrole", "builder"); worker.setMetadata(PlayerID, "target-foundation", ent.id()); }); } } } } for (var i in ConstructionEvents) { var evt = ConstructionEvents[i]; // Let's check if we have a building set to create a new base. // TODO: move to the base manager. if (evt.newentity) { var ent = gameState.getEntityById(evt.newentity); if (ent === undefined) continue; // happens when this message is right before a "Destroy" one for the same entity. if (ent.isOwn(PlayerID) && ent.getMetadata(PlayerID, "baseAnchor") == true) { var base = ent.getMetadata(PlayerID, "base"); if (this.baseManagers[base].constructing) { this.baseManagers[base].constructing = false; this.baseManagers[base].initGatheringFunctions(this, gameState); } } } } }; // 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; gameState.ai.queues["villager"].empty(); gameState.ai.queues["citizenSoldier"].empty(); } } // 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) { // Get some data. // 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; }); }); var numQueuedF = queues.villager.countQueuedUnits(); var numQueuedS = queues.citizenSoldier.countQueuedUnits(); var numQueued = numQueuedS + numQueuedF; var numTotal = numWorkers + numQueued; // If we have too few, train more // should plan enough to always have females… // TODO: 15 here should be changed to something more sensible, such as nb of producing buildings. if (numTotal > this.targetNumWorkers || numQueued > 50 || (numQueuedF > 20 && numQueuedS > 20) || numInTraining > 15) return; if (numTotal >= this.Config.Economy.villagePopCap && gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) return; // default template and size var template = gameState.applyCiv("units/{civ}_support_female_citizen"); var size = Math.min(5, Math.ceil(numTotal / 10)); // Choose whether we want soldiers instead. // TODO: we might want to adjust our female ratio. if ((numFemales+numQueuedF)/numTotal > this.femaleRatio && numQueuedS < 20) { if (numTotal < 35) template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"]]); else template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["strength",1] ]); if (!template) template = gameState.applyCiv("units/{civ}_support_female_citizen"); else size = Math.min(5, Math.ceil(numTotal / 12)); } // TODO: improve that logic. /* if (numFemales/numWorkers > this.femaleRatio && numQueuedS > 0 && numWorkers > 25) queues.villager.paused = true; else queues.villager.paused = false; */ // TODO: perhaps assign them a default resource and check the base according to that. // 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 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, parameters) { var units = gameState.findTrainableUnits(classes); if (units.length === 0) return undefined; units.sort(function(a, b) {// }) { var aDivParam = 0, bDivParam = 0; var aTopParam = 0, bTopParam = 0; for (var i in parameters) { var param = parameters[i]; 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]; }; // Tries to research any available tech // Only one at once. Also does military tech (selection is completely random atm) // TODO: Lots, lots, lots here. m.HQ.prototype.tryResearchTechs = function(gameState, queues) { if (queues.minorTech.length() === 0) { var possibilities = gameState.findAvailableTech(); if (possibilities.length === 0) return; // randomly pick one. No worries about pairs in that case. var p = Math.floor((Math.random()*possibilities.length)); queues.minorTech.addItem(new m.ResearchPlan(gameState, possibilities[p][0])); } } // We're given a worker and a resource type // We'll assign the worker for the best base for that resource type. // TODO: improve choice alogrithm m.HQ.prototype.switchWorkerBase = function(gameState, worker, type) { var bestBase = 0; for (var i in this.baseManagers) { if (this.baseManagers[i].willGather[type] >= 1) { if (this.baseManagers[i].accessIndex === this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex || this.navalManager.canReach(gameState, this.baseManagers[i].accessIndex, this.baseManagers[worker.getMetadata(PlayerID,"base")].accessIndex)) { bestBase = i; break; } } } if (bestBase && bestBase !== worker.getMetadata(PlayerID,"base")) { worker.setMetadata(PlayerID,"base",bestBase); return true; } else { return false; } }; // returns an entity collection of workers through BaseManager.pickBuilders // TODO: better the choice algo. // TODO: also can't get over multiple bases right now. m.HQ.prototype.bulkPickWorkers = function(gameState, newBaseID, number) { var accessIndex = this.baseManagers[newBaseID].accessIndex; if (!accessIndex) return false; // sorting bases by whether they are on the same accessindex or not. var baseBest = m.AssocArraytoArray(this.baseManagers).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; }); for (var i in baseBest) { if (baseBest[i].workers.length > number) { return baseBest[i].pickBuilders(gameState,number); } } return false; } // 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) { var self = this; if (gameState.getTimeElapsed() - this.currentRateLastUpdateTime < 10000 && this.currentRateLastUpdateTime !== 0 && gameState.ai.playedTurn > 3) return this.currentRates; this.currentRateLastUpdateTime = gameState.getTimeElapsed(); for (var type in this.wantedRates) this.currentRates[type] = 0; for (var i in this.baseManagers) this.baseManagers[i].getGatherRates(gameState, this.currentRates); 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) { var self = this; this.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); var currentRates = {}; for (var type in this.wantedRates) currentRates[type] = 0; for (var i in this.baseManagers) { var base = this.baseManagers[i]; for (var type in this.wantedRates) { if (gameState.turnCache["gathererAssignementCache-" + type]) currentRates[type] += gameState.turnCache["gathererAssignementCache-" + type]; base.gatherersByType(gameState,type).forEach (function (ent) { //}){ var worker = ent.getMetadata(PlayerID, "worker-object"); if (worker) currentRates[type] += worker.getGatherRate(gameState); }); } } // let's get our ideal number. var types = Object.keys(this.wantedRates); types.sort(function(a, b) { var va = (Math.max(0,self.wantedRates[a] - currentRates[a]))/ (currentRates[a]+1); var vb = (Math.max(0,self.wantedRates[b] - currentRates[b]))/ (currentRates[b]+1); // If they happen to be equal (generally this means "0" aka no need), make it fair. if (va === vb) return (self.wantedRates[b]/(currentRates[b]+1)) - (self.wantedRates[a]/(currentRates[a]+1)); return vb-va; }); return types; }; // If all the CC's are destroyed then build a new one // TODO: rehabilitate. m.HQ.prototype.buildNewCC= function(gameState, queues) { var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre"), true); numCCs += queues.civilCentre.length(); // no use trying to lay foundations that will be destroyed if (gameState.defcon() > 2) for (var i = numCCs; i < 1; i++) { gameState.ai.queueManager.clear(); this.baseNeed["food"] = 0; this.baseNeed["wood"] = 50; this.baseNeed["stone"] = 50; this.baseNeed["metal"] = 50; queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre")); } return (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) == 0 && gameState.currentPhase() > 1); }; // 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.findBestEcoCCLocation = function(gameState, resource){ var CCPlate = gameState.getTemplate("structures/{civ}_civil_centre"); // 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. var territory = m.createTerritoryMap(gameState); var obstructions = m.createObstructionMap(gameState, 0); obstructions.expandInfluences(); // copy the resource map as initialization. var friendlyTiles = new API3.Map(gameState.sharedScript, gameState.sharedScript.CCResourceMaps[resource].map, true); friendlyTiles.setMaxVal(255); var ents = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); var eEnts = gameState.getEnemyStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); var dps = gameState.getOwnDropsites().toEntityArray(); for (var j = 0; j < friendlyTiles.length; ++j) { // We check for our other CCs: the distance must not be too big. Anything bigger will result in scrapping. // This ensures territorial continuity. // TODO: maybe whenever I get around to implement multi-base support (details below, requires being part of the team. If you're not, ask wraitii directly by PM). // (see www.wildfiregames.com/forum/index.php?showtopic=16702&#entry255631 ) // TODO: figure out what I was trying to say above. var canBuild = true; var canBuild2 = false; var pos = [j%friendlyTiles.width+0.5, Math.floor(j/friendlyTiles.width)+0.5]; for (var i in ents) { var entPos = ents[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; var dist = API3.SquareVectorDistance(entPos, pos); if (dist < 3500 || dist > 7900) friendlyTiles.map[j] /= 2.0; if (dist < 2120) { canBuild = false; continue; } else if (dist < 9200 || this.waterMap) canBuild2 = true; } // checking for bases. if (this.basesMap.map[j] !== 0) canBuild = false; if (!canBuild2) canBuild = false; if (canBuild) { // Checking for enemy CCs for (var i in eEnts) { var entPos = eEnts[i].position(); entPos = [entPos[0]/4.0,entPos[1]/4.0]; // 7100 works well as a limit. if (API3.SquareVectorDistance(entPos, pos) < 2500) { canBuild = false; continue; } } } if (!canBuild) { friendlyTiles.map[j] = 0; continue; } for (var i in dps) { var dpPos = dps[i].position(); if (dpPos === undefined) { // Probably a mauryan elephant, skip continue; } dpPos = [dpPos[0]/4.0,dpPos[1]/4.0]; var dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 600) { friendlyTiles.map[j] = 0; continue; } else if (dist < 1500) friendlyTiles.map[j] /= 2.0; } friendlyTiles.map[j] *= 1.5; for (var i in gameState.sharedScript.CCResourceMaps) if (friendlyTiles.map[j] !== 0 && i !== "food") { var val = friendlyTiles.map[j] + gameState.sharedScript.CCResourceMaps[i].map[j]; if (val < 255) friendlyTiles.map[j] = val; else friendlyTiles.map[j] = 255; } } var best = friendlyTiles.findBestTile(6, obstructions); var bestIdx = best[0]; if (m.DebugEnabled()) { friendlyTiles.map[bestIdx] = 270; friendlyTiles.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + ".png",301); //obstructions.dumpIm("cc_placement_base_" + gameState.getTimeElapsed() + "_" + resource + "_" + best[1] + "_obs.png", 20); } // not good enough. if (best[1] < 60) return false; var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize; var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize; m.debug ("Best for value " + best[1] + " at " + uneval([x,z])); return [x,z]; }; m.HQ.prototype.buildTemple = function(gameState, queues){ if (gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnits() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_temple"), true) === 0){ queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_temple", { "base" : 1 })); } } }; m.HQ.prototype.buildMarket = function(gameState, queues){ if (gameState.getPopulation() > this.Config.Economy.popForMarket && gameState.currentPhase() >= 2 ) { if (queues.economicBuilding.countQueuedUnitsWithClass("BarterMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) === 0){ //only ever build one storehouse/CC/market at a time queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_market", { "base" : 1 })); } } }; // Build a farmstead to go to town phase faster and prepare for research. Only really active on higher diff mode. m.HQ.prototype.buildFarmstead = function(gameState, queues){ if (gameState.getPopulation() > this.Config.Economy.popForFarmstead && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) { // achtung: "DropsiteFood" does not refer to CCs. if (queues.economicBuilding.countQueuedUnitsWithClass("DropsiteFood") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_farmstead"), true) === 0){ //only ever build one storehouse/CC/market at a time queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_farmstead", { "base" : 1 })); // add the farming plough to the research we want. queues.minorTech.addItem(new m.ResearchPlan(gameState, "gather_farming_plough")); } } }; // TODO: generic this, probably per-base m.HQ.prototype.buildDock = function(gameState, queues){ if (!this.waterMap || this.dockFailed) return; if (gameState.getTimeElapsed() > this.dockStartTime) { if (queues.economicBuilding.countQueuedUnitsWithClass("NavalMarket") === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0) { var tp = "" if (gameState.civ() == "cart" && gameState.currentPhase() > 1) tp = "structures/{civ}_super_dock"; else if (gameState.civ() !== "cart") tp = "structures/{civ}_dock"; if (tp !== "") { var remaining = this.navalManager.getUnconnectedSeas(gameState, this.baseManagers[1].accessIndex); queues.economicBuilding.addItem(new m.ConstructionPlan(gameState, tp, { "base" : 1, "sea" : remaining[0] })); } } } }; // Try to barter unneeded resources for needed resources. // once per turn because the info doesn't update between a turn and fixing isn't worth it. m.HQ.prototype.tryBartering = function(gameState) { var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market"), true).toEntityArray(); if (markets.length === 0) return false; // Available resources after account substraction var available = gameState.ai.queueManager.getAvailableResources(gameState); var rates = this.GetCurrentGatherRates(gameState) var prices = gameState.getBarterPrices(); // calculates conversion rates var getBarterRate = function (prices,buy,sell) { return Math.round(100 * prices["sell"][sell] / prices["buy"][buy]); }; // loop through each queues checking if we could barter and finish a queue quickly. for (var j in gameState.ai.queues) { var queue = gameState.ai.queues[j]; if (queue.paused || queue.length() === 0) continue; var account = gameState.ai.queueManager.accounts[j]; var elem = queue.queue[0]; var elemCost = elem.getCost(); for each (var ress in elemCost.types) { if (available[ress] >= 0) continue; // don't care if we still have available resources or our rate is good enough var need = elemCost[ress] - account[ress]; if (need <= 0 || rates[ress] >= need/50) // don't care if we don't need resources for our first item continue; if (ress == "food" && need < 400) continue; // pick the best resource to barter. var bestToBarter = ""; var bestRate = 0; for each (var otherRess in elemCost.types) { if (ress === otherRess) continue; // I wanna keep some if (available[otherRess] < 130 + need) return false; var barterRate = getBarterRate(prices, ress, otherRess); if (barterRate > bestRate) { bestRate = barterRate; bestToBarter = otherRess; } } if (bestToBarter !== "") { markets[0].barter(buy,sell,100); m.debug ("Snipe bartered " + sell +" for " + buy + ", value 100"); return true; } } } // now barter for big needs. var needs = gameState.ai.queueManager.currentNeeds(gameState); for each (var sell in needs.types) { for each (var buy in needs.types) { if (buy != sell && needs[sell] <= 0 && available[sell] > 500) { // if we don't need it and have a buffer if (needs[buy] > rates[buy]*80) { // if we need that other resource terribly. markets[0].barter(buy,sell,100); m.debug ("Gross bartered " +sell +" for " + buy + ", value 100"); return true; } } } } return false; }; // 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.getPopulationLimit() < gameState.getPopulationMax()) { var numPlanned = queues.house.length(); if (numPlanned < 3 || (numPlanned < 5 && gameState.getPopulation() > 80)) { var plan = new m.ConstructionPlan(gameState, "structures/{civ}_house", { "base" : 1 }); // make the difficulty available to the isGo function without having to pass it as argument var difficulty = this.Config.difficulty; // change the starting condition to "less than 15 slots left". plan.isGo = function (gameState) { var HouseNb = gameState.countEntitiesByType(gameState.applyCiv("foundation|structures/{civ}_house"), true); var freeSlots = 0; if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") freeSlots = gameState.getPopulationLimit() + HouseNb*5 - gameState.getPopulation(); else freeSlots = gameState.getPopulationLimit() + HouseNb*10 - gameState.getPopulation(); if (gameState.getPopulation() > 55 && difficulty > 1) return (freeSlots <= 21); else if (gameState.getPopulation() >= 30 && difficulty !== 0) return (freeSlots <= 15); else return (freeSlots <= 10); } queues.house.addItem(plan); } if (numPlanned > 0 && this.econState == "townPhasing") { var houseQueue = queues.house.queue; var count = gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length; count += queues.militaryBuilding.length(); // barracks for (var i = 0; i < numPlanned; ++i) { if (houseQueue[i].isGo(gameState)) ++count; else if (count < 5) { houseQueue[i].isGo = function () { return true; } ++count; } } } } }; // checks if we have bases for all resource types (bar food for now) or if we need to expand. m.HQ.prototype.checkBasesRessLevel = function(gameState,queues) { if (gameState.currentPhase() === 1 && !gameState.isResearching(gameState.townPhase())) return; var count = { "wood" : 0, "stone" : 0, "metal" : 0 } var capacity = { "wood" : 0, "stone" : 0, "metal" : 0 } var need = { "wood" : true, "stone" : true, "metal" : true }; var posss = []; for (var i in this.baseManagers) { var base = this.baseManagers[i]; for (var type in count) { if (base.getResourceLevel(gameState, type, "all") > 1500*Math.max(this.Config.difficulty,2)) count[type]++; capacity[type] += base.getWorkerCapacity(gameState, type); if (base.willGather[type] !== 2) need[type] = false; } } for (var type in count) { if (count[type] === 0 || need[type] || capacity[type] < gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byMetadata(PlayerID, "subrole", "gatherer"), API3.Filters.byMetadata(PlayerID, "gather-type", type))).length * 1.05) { // plan a new base. if (gameState.countFoundationsByType(gameState.applyCiv("structures/{civ}_civil_centre"), true) === 0 && queues.civilCentre.length() === 0) { if (this.outOf[type] && gameState.ai.playedTurn % 10 !== 0) continue; var pos = this.findBestEcoCCLocation(gameState, type); if (!pos) { // Okay so we'll set us as out of this. this.outOf[type] = true; } else { // base "-1" means new base. queues.civilCentre.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_civil_centre",{ "base" : -1 }, pos)); } } } } }; // Deals with building fortresses and towers. // Currently build towers next to every useful dropsites. // TODO: Fortresses are placed randomly atm. m.HQ.prototype.buildDefences = function(gameState, queues){ var workersNumber = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID,"plan"))).length; if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'), true) + queues.defenceBuilding.length() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.length() === 0 && gameState.currentPhase() > 1) { for (var i in this.baseManagers) { for (var j in this.baseManagers[i].dropsites) { var amnts = this.baseManagers[i].dropsites[j]; var dpEnt = gameState.getEntityById(j); if (dpEnt !== undefined && dpEnt.getMetadata(PlayerID, "defenseTower") !== true) if (amnts["wood"] || amnts["metal"] || amnts["stone"]) { var position = dpEnt.position(); if (position) { queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, 'structures/{civ}_defense_tower', { "base" : i }, position)); } dpEnt.setMetadata(PlayerID, "defenseTower", true); } } } } var numFortresses = 0; for (var i in this.bFort){ numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]), true); } if (queues.defenceBuilding.length() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) { if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) { if (!this.fortressStartTime) this.fortressStartTime = gameState.getTimeElapsed(); queues.defenceBuilding.addItem(new m.ConstructionPlan(gameState, this.bFort[0], { "base" : 1 })); m.debug ("Building a fortress"); } } if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i]), true) >= 1) { // let's add a siege building plan to the current attack plan if there is none currently. if (this.upcomingAttacks["CityAttack"].length !== 0) { var attack = this.upcomingAttacks["CityAttack"][0]; if (!attack.unitStat["Siege"]) { // no minsize as we don't want the plan to fail at the last minute though. var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; if (gameState.civ() == "cart" || gameState.civ() == "maur") stat["classes"] = ["Elephant"]; attack.addBuildOrder(gameState, "Siege", stat, true); } } } }; m.HQ.prototype.buildBlacksmith = function(gameState, queues){ if (gameState.getTimeElapsed() > this.Config.Military.timeForBlacksmith*1000) { if (queues.militaryBuilding.length() === 0 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_blacksmith"), true) === 0) { var tp = gameState.getTemplate(gameState.applyCiv("structures/{civ}_blacksmith")); if (tp.available(gameState)) queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, "structures/{civ}_blacksmith", { "base" : 1 })); } } }; // 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) { Engine.ProfileStart("Build buildings"); var workersNumber = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "plan"))).length; var barrackNb = gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]), true); // first barracks. if (workersNumber > this.Config.Military.popForBarracks1 || (this.econState == "townPhasing" && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)) { if (barrackNb + queues.militaryBuilding.length() < 1) { m.debug ("Trying to build barracks"); var plan = new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 }); plan.onStart = function(gameState) { gameState.ai.queueManager.changePriority("militaryBuilding", 130); }; queues.militaryBuilding.addItem(plan); } } // second barracks. if (barrackNb < 2 && workersNumber > this.Config.Military.popForBarracks2) if (queues.militaryBuilding.length() < 1) queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); // third barracks (optional 4th/5th for some civs as they rely on barracks more.) if (barrackNb === 2 && barrackNb + queues.militaryBuilding.length() < 3 && workersNumber > 125) if (queues.militaryBuilding.length() === 0) { queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bModerate[0], { "base" : 1 })); } } //build advanced military buildings if (workersNumber >= this.Config.Military.popForBarracks2 - 15 && gameState.currentPhase() > 2){ if (queues.militaryBuilding.length() === 0){ var inConst = 0; for (var i in this.bAdvanced) inConst += gameState.countFoundationsByType(gameState.applyCiv(this.bAdvanced[i])); if (inConst == 0 && this.bAdvanced && this.bAdvanced.length !== 0) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i]), true) < 1){ queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } } // build second advanced building except for some civs. if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && workersNumber > 130 && gameState.currentPhase() > 2) { var Const = 0; for (var i in this.bAdvanced) Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i]), true); if (inConst == 1) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i]), true) < 1){ queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); queues.militaryBuilding.addItem(new m.ConstructionPlan(gameState, this.bAdvanced[i], { "base" : 1 })); } } } Engine.ProfileStop(); }; // TODO: use pop(). Currently unused as this is too gameable. m.HQ.prototype.garrisonAllFemales = function(gameState) { var buildings = gameState.getOwnStructures().filter(API3.Filters.byCanGarrison()).toEntityArray(); var females = gameState.getOwnUnits().filter(API3.Filters.byClass("Support")); var cache = {}; females.forEach( function (ent) { for (var i in buildings) { if (ent.position()) { var struct = buildings[i]; if (!cache[struct.id()]) cache[struct.id()] = 0; if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) { ent.garrison(struct); cache[struct.id()]++; break; } } } }); this.hasGarrisonedFemales = true; }; m.HQ.prototype.ungarrisonAll = function(gameState) { this.hasGarrisonedFemales = false; var buildings = gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Structure"),API3.Filters.byCanGarrison())).toEntityArray(); buildings.forEach( function (struct) { if (struct.garrisoned() && struct.garrisoned().length) struct.unloadAll(); }); }; m.HQ.prototype.pausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } } m.HQ.prototype.unpausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } } m.HQ.prototype.pauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, true); } } } m.HQ.prototype.unpauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, false); } } } // Some functions are run every turn // Others once in a while m.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.checkEvents(gameState,events,queues); //this.buildMoreHouses(gameState); - //Engine.ProfileStart("Train workers and build farms, houses. Research techs."); this.trainMoreWorkers(gameState, queues); // sandbox doesn't expand. if (this.Config.difficulty !== 0) this.checkBasesRessLevel(gameState, queues); this.buildMoreHouses(gameState,queues); if (gameState.getTimeElapsed() > this.techStartTime && gameState.currentPhase() > 2 ) this.tryResearchTechs(gameState,queues); if (this.Config.difficulty > 1) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true) === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. Engine.ProfileStart("Constructing military buildings and building defences"); this.constructTrainingBuildings(gameState, queues); this.buildBlacksmith(gameState, queues); if(gameState.getTimeElapsed() > this.defenceBuildingTime) this.buildDefences(gameState, queues); Engine.ProfileStop(); for (var i in this.baseManagers) { this.baseManagers[i].checkEvents(gameState, events, queues) if ( ( (+i + gameState.ai.playedTurn) % (m.playerGlobals[PlayerID].uniqueIDBases - 1)) === 0) this.baseManagers[i].update(gameState, queues, events); } this.navalManager.update(gameState, queues, events); this.defenceManager.update(gameState, events, this); Engine.ProfileStart("Looping through attack plans"); // TODO: bump this into a function. // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. if (1) { for (var attackType in this.upcomingAttacks) { for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { var attack = this.upcomingAttacks[attackType][i]; // okay so we'll get the support plan if (!attack.isStarted()) { var updateStep = attack.updatePreparation(gameState, this,events); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' } else if (updateStep === 0 || updateStep === 3) { m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); if (updateStep === 3) { this.attackPlansEncounteredWater = true; m.debug("No attack path found. Aborting."); } attack.Abort(gameState, this); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); attack.StartAttack(gameState,this); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } else { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); m.debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } } } for (var attackType in this.startedAttacks) { for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { var attack = this.startedAttacks[attackType][i]; // okay so then we'll update the attack. if (!attack.isPaused()) { var remaining = attack.update(gameState,this,events); if (remaining == 0 || remaining == undefined) { m.debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } } // creating plans after updating because an aborted plan might be reused in that case. // TODO: remove the limitation to attacks when on water maps. if (!this.waterMap && !this.attackPlansEncounteredWater) { if (gameState.ai.aggressiveness > 0.75 && gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && gameState.getTimeElapsed() > this.attackPlansStartTime && gameState.getTimeElapsed() < 360000) { if (this.upcomingAttacks["Rush"].length === 0) { // we have a barracks and we want to rush, rush. var AttackPlan = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "Rush"); m.debug ("Headquarters: Rushing plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["Rush"].push(AttackPlan); } } // if we have a barracks, there's no water, we're at age >= 1 and we've decided to attack. else if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0]), true) >= 1 && !this.attackPlansEncounteredWater && gameState.getTimeElapsed() > this.attackPlansStartTime && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.townPhase()))) { if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock"), true) === 0 && this.waterMap) { // wait till we get a dock. } else if (this.upcomingAttacks["CityAttack"].length === 0) { // basically only the first plan, really. var Lalala = undefined; if (gameState.getTimeElapsed() < 12*60000) Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1); else if (this.Config.difficulty !== 0) Lalala = new m.CityAttack(gameState, this, this.Config, this.TotalAttackNumber, -1, "superSized"); if (Lalala.failed) this.attackPlansEncounteredWater = true; // hack else { m.debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } } } } /* // very old relic. This should be reimplemented someday so the code stays here. if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { var Lalala = new m.CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); if (!Lalala.createSupportPlans(gameState, this, )) { m.debug ("Military Manager: harrassing plan not a valid option"); this.HarassRaiding = false; } else { m.debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); this.totalStartedAttackNumber++; this.preparingRaidNumber++; this.currentAttacks.push(Lalala); } } */ Engine.ProfileStop(); /* Engine.ProfileStop(); Engine.ProfileStart("Build new Dropsites"); this.buildDropsites(gameState, queues); Engine.ProfileStop(); if (this.Config.difficulty !== 0) this.tryBartering(gameState); this.buildFarmstead(gameState, queues); this.buildMarket(gameState, queues); // Deactivated: the temple had no useful purpose for the AI now. //if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market"), true === 1) // this.buildTemple(gameState, queues); this.buildDock(gameState, queues); // not if not a water map. */ Engine.ProfileStop(); // Heaquarters update }; return m; }(AEGIS); 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 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 14588) @@ -1,780 +1,805 @@ var API3 = function(m) { -m.EntityTemplate = m.Class({ +// defines a template. +// It's completely raw data, except it's slightly cleverer now and then. +m.Template = m.Class({ - // techModifications should be the tech modifications of only one player. - // gamestates handle "GetTemplate" and should push the player's - // while entities should push the owner's - _init: function(template, techModifications) + _init: function(template) { - this._techModifications = techModifications; this._template = template; + this._tpCache = {}; }, + // 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[string]) { + return this._auraTemplateModif[string]; + } else if (this._techModif && this._techModif[string]) { + return this._techModif[string]; + } else { + if (this._tpCache[string] === undefined) + { + var args = string.split("/"); + for (var i = 0; i < args.length; ++i) + if (value[args[i]]) + value = value[args[i]]; + else + { + value = undefined; + break; + } + this._tpCache[string] = value; + } + return this._tpCache[string]; + } + }, + genericName: function() { - if (!this._template.Identity || !this._template.Identity.GenericName) + if (!this.get("Identity") || !this.get("Identity/GenericName")) return undefined; - return this._template.Identity.GenericName; + return this.get("Identity/GenericName"); }, - + rank: function() { - if (!this._template.Identity) + if (!this.get("Identity")) return undefined; - return this._template.Identity.Rank; + return this.get("Identity/Rank"); }, classes: function() { - if (!this._template.Identity || !this._template.Identity.Classes || !this._template.Identity.Classes._string) + if (!this.get("Identity") || !this.get("Identity/Classes") || !this.get("Identity/Classes/_string")) return undefined; - return this._template.Identity.Classes._string.split(/\s+/); + return this.get("Identity/Classes/_string").split(/\s+/); }, requiredTech: function() { - if (!this._template.Identity || !this._template.Identity.RequiredTechnology) - return undefined; - return this._template.Identity.RequiredTechnology; + return this.get("Identity/RequiredTechnology"); }, - + available: function(gameState) { - if (!this._template.Identity || !this._template.Identity.RequiredTechnology) - return true; - return gameState.isResearched(this._template.Identity.RequiredTechnology); + return gameState.isResearched(this.get("Identity/RequiredTechnology")); }, - + // specifically phase: function() { - if (!this._template.Identity || !this._template.Identity.RequiredTechnology) + if (!this.get("Identity/RequiredTechnology")) return 0; - if (this.template.Identity.RequiredTechnology == "phase_village") + if (this.get("Identity/RequiredTechnology") == "phase_village") return 1; - if (this.template.Identity.RequiredTechnology == "phase_town") + if (this.get("Identity/RequiredTechnology") == "phase_town") return 2; - if (this.template.Identity.RequiredTechnology == "phase_city") + if (this.get("Identity/RequiredTechnology") == "phase_city") return 3; return 0; }, hasClass: function(name) { var classes = this.classes(); return (classes && classes.indexOf(name) != -1); }, hasClasses: function(array) { var classes = this.classes(); if (!classes) return false; for (var i in array) if (classes.indexOf(array[i]) === -1) return false; return true; }, civ: function() { - if (!this._template.Identity) - return undefined; - return this._template.Identity.Civ; + return this.get("Identity/Civ"); }, cost: function() { - if (!this._template.Cost) + if (!this.get("Cost")) return undefined; var ret = {}; - for (var type in this._template.Cost.Resources) - ret[type] = GetTechModifiedProperty(this._techModifications, this._template, "Cost/Resources/"+type, +this._template.Cost.Resources[type]); + for (var type in this.get("Cost/Resources")) + ret[type] = +this.get("Cost/Resources/" + type); return ret; }, costSum: function() { - if (!this._template.Cost) + if (!this.get("Cost")) return undefined; var ret = 0; - for (var type in this._template.Cost.Resources) - ret += +this._template.Cost.Resources[type]; + 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._template.Obstruction) + if (!this.get("Obstruction")) return undefined; - if (this._template.Obstruction.Static) + if (this.get("Obstruction/Static")) { - var w = +this._template.Obstruction.Static["@width"]; - var h = +this._template.Obstruction.Static["@depth"]; + var w = +this.get("Obstruction/Static/@width"); + var h = +this.get("Obstruction/Static/@depth"); return Math.sqrt(w*w + h*h) / 2; } - if (this._template.Obstruction.Unit) - return +this._template.Obstruction.Unit["@radius"]; + 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._template.Footprint) + if (!this.get("Footprint")) return undefined; - if (this._template.Footprint.Square) + if (this.get("Footprint/Square")) { - var w = +this._template.Footprint.Square["@width"]; - var h = +this._template.Footprint.Square["@depth"]; + var w = +this.get("Footprint/Square/@width"); + var h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } - if (this._template.Footprint.Circle) - return +this._template.Footprint.Circle["@radius"]; + if (this.get("Footprint/Circle")) + return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, maxHitpoints: function() { - if (this._template.Health !== undefined) - return GetTechModifiedProperty(this._techModifications, this._template, "Health/Max",+this._template.Health.Max); + if (this.get("Health") !== undefined) + return +this.get("Health/Max"); return 0; }, + isHealable: function() { - if (this._template.Health !== undefined) - return this._template.Health.Unhealable !== "true"; + if (this.get("Health") !== undefined) + return this.get("Health/Unhealable") !== "true"; return false; }, + isRepairable: function() { - if (this._template.Health !== undefined) - return this._template.Health.Repairable === "true"; + if (this.get("Health") !== undefined) + return this.get("Health/Repairable") === "true"; return false; }, getPopulationBonus: function() { - if (!this._template.Cost || !this._template.Cost.PopulationBonus) - return undefined; - return this._template.Cost.PopulationBonus; + return this.get("Cost/PopulationBonus"); }, armourStrengths: function() { - if (!this._template.Armour) + if (!this.get("Armour")) return undefined; return { - hack: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Hack", +this._template.Armour.Hack), - pierce: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Pierce", +this._template.Armour.Pierce), - crush: GetTechModifiedProperty(this._techModifications, this._template, "Armour/Crush", +this._template.Armour.Crush) + hack: +this.get("Armour/Hack"), + pierce: +this.get("Armour/Pierce"), + crush: +this.get("Armour/Crush") }; }, attackTypes: function() { - if (!this._template.Attack) + if (!this.get("Attack")) return undefined; var ret = []; - for (var type in this._template.Attack) + for (var type in this.get("Attack")) ret.push(type); return ret; }, attackRange: function(type) { - if (!this._template.Attack || !this._template.Attack[type]) + if (!this.get("Attack/" + type +"")) return undefined; return { - max: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MaxRange", +this._template.Attack[type].MaxRange), - min: GetTechModifiedProperty(this._techModifications, this._template, "Attack/MinRange", +(this._template.Attack[type].MinRange || 0)) + max: +this.get("Attack/" + type +"/MaxRange"), + min: +(this.get("Attack/" + type +"/MinRange") || 0) }; }, attackStrengths: function(type) { - if (!this._template.Attack || !this._template.Attack[type]) + if (!this.get("Attack/" + type +"")) return undefined; return { - hack: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Hack", +(this._template.Attack[type].Hack || 0)), - pierce: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Pierce", +(this._template.Attack[type].Pierce || 0)), - crush: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/Crush", +(this._template.Attack[type].Crush || 0)) + hack: +(this.get("Attack/" + type + "/Hack") || 0), + pierce: +(this.get("Attack/" + type + "/Pierce") || 0), + crush: +(this.get("Attack/" + type + "/Crush") || 0) }; }, attackTimes: function(type) { - if (!this._template.Attack || !this._template.Attack[type]) + if (!this.get("Attack/" + type +"")) return undefined; return { - prepare: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/PrepareTime", +(this._template.Attack[type].PrepareTime || 0)), - repeat: GetTechModifiedProperty(this._techModifications, this._template, "Attack/"+type+"/RepeatTime", +(this._template.Attack[type].RepeatTime || 1000)) + 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 ]. + // Return type is [ [-neededClasses- , multiplier], … ]. getCounteredClasses: function() { - if (!this._template.Attack) + if (!this.get("Attack")) return undefined; var Classes = []; - for (var i in this._template.Attack) { - if (!this._template.Attack[i].Bonuses) + for (var i in this.get("Attack")) { + if (!this.get("Attack/" + i + "/Bonuses")) continue; - for (var o in this._template.Attack[i].Bonuses) - if (this._template.Attack[i].Bonuses[o].Classes) - Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]); + 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._template.Attack) + if (!this.get("Attack")) return false; var mcounter = []; - for (var i in this._template.Attack) { - if (!this._template.Attack[i].Bonuses) + for (var i in this.get("Attack")) { + if (!this.get("Attack/" + i + "/Bonuses")) continue; - for (var o in this._template.Attack[i].Bonuses) - if (this._template.Attack[i].Bonuses[o].Classes) - mcounter.concat(this._template.Attack[i].Bonuses[o].Classes.split(" ")); + 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._template.Attack || !this._template.Attack[type]) + if (!this.get("Attack/" + type +"")) return undefined; - if (this._template.Attack[type].Bonuses) - for (var o in this._template.Attack[type].Bonuses) { - if (!this._template.Attack[type].Bonuses[o].Classes) + 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._template.Attack[type].Bonuses[o].Classes.split(" "); + var total = this.get("Attack/" + type + "/Bonuses/" + o + "/Classes").split(" "); for (var j in total) if (total[j] === againstClass) - return this._template.Attack[type].Bonuses[o].Multiplier; + 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._template.Attack) + if (!this.get("Attack")) return false; - for (var i in this._template.Attack) { - if (!this._template.Attack[i].RestrictedClasses || !this._template.Attack[i].RestrictedClasses._string) + for (var i in this.get("Attack")) { + if (!this.get("Attack/" + i + "/RestrictedClasses") || !this.get("Attack/" + i + "/RestrictedClasses/_string")) continue; - var cannotAttack = this._template.Attack[i].RestrictedClasses._string.split(" "); + var cannotAttack = this.get("Attack/" + i + "/RestrictedClasses/_string").split(" "); if (cannotAttack.indexOf(saidClass) !== -1) return false; } return true; }, buildableEntities: function() { - if (!this._template.Builder) - return undefined; - if (!this._template.Builder.Entities._string) + if (!this.get("Builder/Entities/_string")) return []; var civ = this.civ(); - var templates = this._template.Builder.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); + var templates = this.get("Builder/Entities/_string").replace(/\{civ\}/g, civ).split(/\s+/); return templates; // TODO: map to Entity? }, trainableEntities: function() { - if (!this._template.ProductionQueue || !this._template.ProductionQueue.Entities || !this._template.ProductionQueue.Entities._string) + if (!this.get("ProductionQueue/Entities/_string")) return undefined; var civ = this.civ(); - var templates = this._template.ProductionQueue.Entities._string.replace(/\{civ\}/g, civ).split(/\s+/); + var templates = this.get("ProductionQueue/Entities/_string").replace(/\{civ\}/g, civ).split(/\s+/); return templates; }, researchableTechs: function() { - if (!this._template.ProductionQueue || !this._template.ProductionQueue.Technologies || !this._template.ProductionQueue.Technologies._string) + if (!this.get("ProductionQueue/Technologies/_string")) return undefined; - var templates = this._template.ProductionQueue.Technologies._string.split(/\s+/); + var templates = this.get("ProductionQueue/Technologies/_string").split(/\s+/); return templates; }, resourceSupplyType: function() { - if (!this._template.ResourceSupply) + if (!this.get("ResourceSupply")) return undefined; - var [type, subtype] = this._template.ResourceSupply.Type.split('.'); + 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._template.ResourceSupply) + if (!this.get("ResourceSupply")) return undefined; - var [type, subtype] = this._template.ResourceSupply.Type.split('.'); + var [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, resourceSupplyMax: function() { - if (!this._template.ResourceSupply) + if (!this.get("ResourceSupply")) return undefined; - return +this._template.ResourceSupply.Amount; + return +this.get("ResourceSupply/Amount"); }, maxGatherers: function() { - if (this._template.ResourceSupply !== undefined) - return +this._template.ResourceSupply.MaxGatherers; + if (this.get("ResourceSupply") !== undefined) + return +this.get("ResourceSupply/MaxGatherers"); return 0; }, resourceGatherRates: function() { - if (!this._template.ResourceGatherer) + if (!this.get("ResourceGatherer")) return undefined; var ret = {}; - var baseSpeed = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/BaseSpeed", +this._template.ResourceGatherer.BaseSpeed); - for (var r in this._template.ResourceGatherer.Rates) - ret[r] = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/Rates/"+r, +this._template.ResourceGatherer.Rates[r]) * baseSpeed; + 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._template.ResourceDropsite) + if (!this.get("ResourceDropsite")) return undefined; - return this._template.ResourceDropsite.Types.split(/\s+/); + return this.get("ResourceDropsite/Types").split(/\s+/); }, garrisonableClasses: function() { - if (!this._template.GarrisonHolder || !this._template.GarrisonHolder.List._string) + if (!this.get("GarrisonHolder") || !this.get("GarrisonHolder/List/_string")) return undefined; - return this._template.GarrisonHolder.List._string.split(/\s+/); + return this.get("GarrisonHolder/List/_string").split(/\s+/); }, garrisonMax: function() { - if (!this._template.GarrisonHolder) + if (!this.get("GarrisonHolder")) return undefined; - return this._template.GarrisonHolder.Max; + return this.get("GarrisonHolder/Max"); }, /** * Returns whether this is an animal that is too difficult to hunt. * (Any non domestic currently.) */ isUnhuntable: function() { - if (!this._template.UnitAI || !this._template.UnitAI.NaturalBehaviour) + if (!this.get("UnitAI") || !this.get("UnitAI/NaturalBehaviour")) return false; // only attack domestic animals since they won't flee nor retaliate. - return this._template.UnitAI.NaturalBehaviour !== "domestic"; + return this.get("UnitAI/NaturalBehaviour") !== "domestic"; }, - + walkSpeed: function() { - if (!this._template.UnitMotion || !this._template.UnitMotion.WalkSpeed) + if (!this.get("UnitMotion") || !this.get("UnitMotion/WalkSpeed")) return undefined; - return this._template.UnitMotion.WalkSpeed; + return this.get("UnitMotion/WalkSpeed"); }, buildCategory: function() { - if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Category) + if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Category")) return undefined; - return this._template.BuildRestrictions.Category; + return this.get("BuildRestrictions/Category"); }, buildTime: function() { - if (!this._template.Cost || !this._template.Cost.BuildTime) + if (!this.get("Cost") || !this.get("Cost/BuildTime")) return undefined; - return this._template.Cost.BuildTime; + return this.get("Cost/BuildTime"); }, buildDistance: function() { - if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Distance) + if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Distance")) return undefined; - return this._template.BuildRestrictions.Distance; + return this.get("BuildRestrictions/Distance"); }, buildPlacementType: function() { - if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.PlacementType) + if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/PlacementType")) return undefined; - return this._template.BuildRestrictions.PlacementType; + return this.get("BuildRestrictions/PlacementType"); }, buildTerritories: function() { - if (!this._template.BuildRestrictions || !this._template.BuildRestrictions.Territory) + if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; - return this._template.BuildRestrictions.Territory.split(/\s+/); + return this.get("BuildRestrictions/Territory").split(/\s+/); }, hasBuildTerritory: function(territory) { var territories = this.buildTerritories(); return (territories && territories.indexOf(territory) != -1); }, hasTerritoryInfluence: function() { - return (this._template.TerritoryInfluence !== undefined); + return (this.get("TerritoryInfluence") !== undefined); }, territoryInfluenceRadius: function() { - if (this._template.TerritoryInfluence !== undefined) - return (this._template.TerritoryInfluence.Radius); + if (this.get("TerritoryInfluence") !== undefined) + return (this.get("TerritoryInfluence/Radius")); else return -1; }, territoryInfluenceWeight: function() { - if (this._template.TerritoryInfluence !== undefined) - return (this._template.TerritoryInfluence.Weight); + if (this.get("TerritoryInfluence") !== undefined) + return (this.get("TerritoryInfluence/Weight")); else return -1; }, visionRange: function() { - if (!this._template.Vision) - return undefined; - return this._template.Vision.Range; + 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.EntityTemplate, + _super: m.Template, _init: function(sharedAI, entity) { - this._super.call(this, sharedAI.GetTemplate(entity.template), sharedAI._techModifications[entity.owner]); + this._super.call(this, sharedAI.GetTemplate(entity.template)); - this._ai = sharedAI; this._templateName = entity.template; this._entity = entity; + this._auraTemplateModif = {}; // 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] = {}; + 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 is not shared with any other AI scripts.) + * (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; }, unitAIOrderData: function() { return this._entity.unitAIOrderData; }, - hitpoints: function() { if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return undefined; }, + + hitpoints: function() {if (this._entity.hitpoints !== undefined) return this._entity.hitpoints; return 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(); }, /** * 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; // TODO: compute total time for units in training queue return queue.length; }, foundationProgress: function() { if (this._entity.foundationProgress == undefined) return undefined; return this._entity.foundationProgress; }, owner: function() { return this._entity.owner; }, + isOwn: function(player) { if (typeof(this._entity.owner) === "undefined") return false; return this._entity.owner === player; }, + isFriendly: function(player) { return this.isOwn(player); // TODO: diplomacy }, + isEnemy: function(player) { return !this.isOwn(player); // TODO: diplomacy }, resourceSupplyAmount: function() { if(this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, - + resourceSupplyGatherers: function(player) { if (this._entity.resourceSupplyGatherers !== undefined) - return this._entity.resourceSupplyGatherers[player-1]; + return this._entity.resourceSupplyGatherers[player]; return []; }, - + isFull: function(player) { if (this._entity.resourceSupplyGatherers !== undefined) - return (this.maxGatherers() === this._entity.resourceSupplyGatherers[player-1].length); + return (this.maxGatherers() === this._entity.resourceSupplyGatherers[player].length); 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._template.ResourceGatherer) + if (!this.get("ResourceGatherer")); return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] === "GATHER" || this.unitAIState().split(".")[1] === "RETURNRESOURCE")) { var ress = undefined; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] === "GATHER" && this.unitAIOrderData()[0]["target"] !== undefined) ress = this._ai._entities[this.unitAIOrderData()[0]["target"]]; else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1]["target"] !== undefined) ress = this._ai._entities[this.unitAIOrderData()[1]["target"]]; if (ress == undefined) return undefined; var type = ress.resourceSupplyType(); var tstring = type.generic + "." + type.specific; - + if (type.generic == "treasure") return 1000; - var speed = GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/BaseSpeed", +this._template.ResourceGatherer.BaseSpeed); - speed *= GetTechModifiedProperty(this._techModifications, this._template, "ResourceGatherer/Rates/"+tstring, +this._template.ResourceGatherer.Rates[tstring]); - + var speed = +this.get("ResourceGatherer/BaseSpeed"); + speed *= +this.get("ResourceGatherer/Rates/" +tstring); + if (speed) return speed; return 0; } return undefined; }, garrisoned: function() { return new m.EntityCollection(this._ai, this._entity.garrisoned); }, canGarrisonInside: function() { return this._entity.garrisoned.length < this.garrisonMax(); }, // TODO: visibility move: function(x, z, queued) { queued = queued || false; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, attackMove: function(x, z, queued) { queued = queued || false; Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground setStance: function(stance,queued){ Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, // TODO: replace this with the proper "STOP" command stopMoving: function() { if (this.position() !== undefined) Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false}); }, unload: function(id) { - if (!this._template.GarrisonHolder) + 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._template.GarrisonHolder) + if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload-all-own", "garrisonHolders": [this.id()]}); return this; }, garrison: function(target) { Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false}); return this; }, attack: function(unitId) { Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "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) { queued = queued || false; Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, repair: function(target, queued) { queued = queued || false; Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": false, "queued": queued}); return this; }, returnResources: function(target, queued) { queued = 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; }, train: function(type, count, metadata) { var trainable = this.trainableEntities(); 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 }); 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 i in queue) { if (queue[i].progress < percentToStopAt) Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": queue[i].id }); } return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 14588) @@ -1,651 +1,651 @@ var API3 = function(m) { /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. this.cellSize = 4.0; // Size of each map tile this.buildingsBuilt = 0; this.turnCache = {}; }; m.GameState.prototype.init = function(SharedScript, state, player) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = this.sharedScript.playersData[this.player]; this.techModifications = SharedScript._techModifications[this.player]; this.barterPrices = SharedScript.barterPrices; }; m.GameState.prototype.update = function(SharedScript, state) { this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.EntCollec = SharedScript._entityCollections; this.timeElapsed = SharedScript.timeElapsed; this.templates = SharedScript._templates; this.techTemplates = SharedScript._techTemplates; this._entities = SharedScript._entities; this.entities = SharedScript.entities; this.playerData = SharedScript.playersData[this.player]; this.techModifications = SharedScript._techModifications[this.player]; this.barterPrices = SharedScript.barterPrices; this.buildingsBuilt = 0; this.turnCache = {}; }; m.GameState.prototype.updatingCollection = function(id, filter, collection, allowQuick){ // automatically add the player ID in front. id = this.player + "-" + id; if (!this.EntCollecNames[id]) { if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else { this.EntCollecNames[id] = this.entities.filter(filter); } if (allowQuick) this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); // warn ("New Collection named " +id); } return this.EntCollecNames[id]; }; m.GameState.prototype.destroyCollection = function(id){ // automatically add the player ID id = this.player + "-" + id; if (this.EntCollecNames[id] !== undefined){ this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]); delete this.EntCollecNames[id]; } }; m.GameState.prototype.getEC = function(id){ // automatically add the player ID id = this.player + "-" + id; if (this.EntCollecNames[id] !== undefined) return this.EntCollecNames[id]; return undefined; }; m.GameState.prototype.updatingGlobalCollection = function(id, filter, collection, allowQuick) { if (!this.EntCollecNames[id]){ if (collection !== undefined) this.EntCollecNames[id] = collection.filter(filter); else this.EntCollecNames[id] = this.entities.filter(filter); if (allowQuick) this.EntCollecNames[id].allowQuickIter(); this.EntCollecNames[id].registerUpdates(); //warn ("New Global Collection named " +id); } return this.EntCollecNames[id]; }; m.GameState.prototype.destroyGlobalCollection = function(id) { if (this.EntCollecNames[id] !== undefined){ this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames[id]); delete this.EntCollecNames[id]; } }; m.GameState.prototype.getGEC = function(id) { if (this.EntCollecNames[id] !== undefined) return this.EntCollecNames[id]; return undefined; }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.barterPrices; }; m.GameState.prototype.getTemplate = function(type) { if (this.techTemplates[type] !== undefined) return new m.Technology(this.techTemplates, type); if (!this.templates[type]) return null; - return new m.EntityTemplate(this.templates[type], this.techModifications); + return new m.Template(this.templates[type], this.techModifications); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.civ = function() { return this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { if (this.isResearched("phase_city")) return 3; if (this.isResearched("phase_town")) return 2; if (this.isResearched("phase_village")) return 1; return 0; }; m.GameState.prototype.townPhase = function() { if (this.playerData.civ == "athen") return "phase_town_athen"; return "phase_town_generic"; }; m.GameState.prototype.cityPhase = function() { return "phase_city_generic"; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs[template] !== undefined; }; // true if started or queued m.GameState.prototype.isResearching = function(template) { return (this.playerData.researchStarted[template] !== undefined || this.playerData.researchQueued[template] !== undefined); }; // this is an "in-absolute" check that doesn't check if we have a building to research from. m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { var template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued[techTemplateName] || this.playerData.researchStarted[techTemplateName] || this.playerData.researchedTechs[techTemplateName]) return false; if (noRequirementCheck === true) return true; // not already researched, check if we can. // basically a copy of the function in technologyManager since we can't use it. // Checks the requirements for a technology to see if it can be researched at the current time // The technology which this technology supersedes is required if (template.supersedes() && !this.playerData.researchedTechs[template.supersedes()]) return false; // if this is a pair, we must check that the paire tech is not being researched if (template.pair()) { var other = template.pairedWith(); if (this.playerData.researchQueued[other] || this.playerData.researchStarted[other] || this.playerData.researchedTechs[other]) return false; } return this.checkTechRequirements(template.requirements()); } // Private function for checking a set of requirements is met // basically copies TechnologyManager's m.GameState.prototype.checkTechRequirements = function (reqs) { // If there are no requirements then all requirements are met if (!reqs) return true; if (reqs.tech) { return (this.playerData.researchedTechs[reqs.tech] !== undefined && this.playerData.researchedTechs[reqs.tech]); } else if (reqs.all) { for (var i = 0; i < reqs.all.length; i++) { if (!this.checkTechRequirements(reqs.all[i])) return false; } return true; } else if (reqs.any) { for (var i = 0; i < reqs.any.length; i++) { if (this.checkTechRequirements(reqs.any[i])) return true; } return false; } else if (reqs.class) { if (reqs.numberOfTypes) { if (this.playerData.typeCountsByClass[reqs.class]) return (reqs.numberOfTypes <= Object.keys(this.playerData.typeCountsByClass[reqs.class]).length); else return false; } else if (reqs.number) { if (this.playerData.classCounts[reqs.class]) return (reqs.number <= this.playerData.classCounts[reqs.class]); else return false; } } else if (reqs.civ) { if (this.playerData.civ == reqs.civ) return true; else return false; } // The technologies requirements are not a recognised format error("Bad requirements " + uneval(reqs)); return false; }; m.GameState.prototype.getMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { if (!(name in this.sharedScript.passabilityClasses)){ error("Tried to use invalid passability class name '" + name + "'"); } return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; m.GameState.prototype.getEnemies = function(){ var ret = []; for (var i in this.playerData.isEnemy){ if (this.playerData.isEnemy[i]){ ret.push(i); } } return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return this.playerData.isAlly[ent.owner()]; } else if (ent && ent.owner){ return this.playerData.isAlly[ent.owner]; } return false; }; m.GameState.prototype.isEntityEnemy = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return this.playerData.isEnemy[ent.owner()]; } else if (ent && ent.owner){ return this.playerData.isEnemy[ent.owner]; } return false; }; m.GameState.prototype.isEntityOwn = function(ent) { if (ent && ent.owner && (typeof ent.owner) === "function"){ return ent.owner() == this.player; } else if (ent && ent.owner){ return ent.owner == this.player; } return false; }; m.GameState.prototype.getEntityById = function(id){ if (this.entities._entities[id]) return this.entities._entities[id]; return undefined; }; m.GameState.prototype.getEntities = function() { return this.entities; }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; // Try to use a parameter for those three, it'll be a lot faster. m.GameState.prototype.getEnemyEntities = function(enemyID) { if (enemyID === undefined) return this.entities.filter(m.Filters.byOwners(this.getEnemies())); return this.updatingGlobalCollection("" + enemyID + "-entities", m.Filters.byOwner(enemyID)); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Structure")); return this.updatingGlobalCollection("" + enemyID + "-structures", m.Filters.byClass("Structure"), this.getEnemyEntities(enemyID)); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEnemyEntities(enemyID)); }; // if maintain is true, this will be stored. Otherwise it's one-shot. m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain){ if (maintain === true) return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value),this.getOwnEntities()); return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain){ return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain){ var filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function(){ return this.updatingGlobalCollection("" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities(), true); }; m.GameState.prototype.getOwnResearchFacilities = function(){ return this.updatingGlobalCollection("" + this.player + "-research-facilities", m.Filters.byResearchAvailable(), this.getOwnEntities(), true); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { var count = this.countEntitiesByType(type, maintain); // Count building foundations if (this.getTemplate(type).hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (this.getTemplate(type).resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent){ ent.trainingQueue().forEach(function(item) { if (item.unitTemplate == type){ count += item.count; } }); }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { var foundationType = "foundation|" + type; if (maintain === true) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; var count = 0; this.getOwnStructures().forEach(function(ent) { var t = ent.templateName(); if (t == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role).length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { var count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata.role == role) count += item.count; }); }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues var count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { ent.trainingQueue().forEach(function(item) { if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource){ if (resource !== undefined) return this.updatingCollection("dropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities(), true); return this.updatingCollection("dropsite-all", m.Filters.isDropsite(), this.getOwnEntities(), true); }; m.GameState.prototype.getResourceSupplies = function(resource){ return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities(), true); }; // This returns only units from buildings. m.GameState.prototype.findTrainableUnits = function(classes){ var allTrainable = []; this.getOwnStructures().forEach(function(ent) { var trainable = ent.trainableEntities(); for (var i in trainable){ if (allTrainable.indexOf(trainable[i]) === -1){ allTrainable.push(trainable[i]); } } }); var ret = []; for (var i in allTrainable) { var template = this.getTemplate(allTrainable[i]); if (template.hasClass("Hero")) // disabling heroes for now continue; var okay = true; for (var o in classes) if (!template.hasClass(classes[o])) okay = false; if (okay) ret.push( [allTrainable[i], template] ); } return ret; }; // Return all techs which can currently be researched // Does not factor cost. // If there are pairs, both techs are returned. m.GameState.prototype.findAvailableTech = function() { var allResearchable = []; this.getOwnEntities().forEach(function(ent) { var searchable = ent.researchableTechs(); for (var i in searchable) { if (allResearchable.indexOf(searchable[i]) === -1) { allResearchable.push(searchable[i]); } } }); var ret = []; for (var i in allResearchable) { var template = this.getTemplate(allResearchable[i]); if (template.pairDef()) { var techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]] ); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else { if (this.canResearch(allResearchable[i]) && template._templateName != this.townPhase() && template._templateName != this.cityPhase()) ret.push( [allResearchable[i], template] ); } } return ret; }; /** * Find buildings that are capable of training said template. * Getting the best is up to the AI. */ m.GameState.prototype.findTrainers = function(template) { return this.getOwnTrainingFacilities().filter(function(ent) { var trainable = ent.trainableEntities(); if (!trainable || trainable.indexOf(template) == -1) return false; return true; }); }; /** * Find units that are capable of constructing the given building type. */ m.GameState.prototype.findBuilders = function(template) { return this.getOwnUnits().filter(function(ent) { var buildable = ent.buildableEntities(); if (!buildable || buildable.indexOf(template) == -1) return false; return true; }); }; // Find buildings that are capable of researching the given tech m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return []; var template = this.getTemplate(templateName); var self = this; return this.getOwnResearchFacilities().filter(function(ent) { var techs = ent.researchableTechs(); for (var i in techs) { var thisTemp = self.getTemplate(techs[i]); if (thisTemp.pairDef()) { var pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else { if (techs[i] == templateName) return true; } } return false; }); }; m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; // Checks whether the maximum number of buildings have been cnstructed for a certain catergory m.GameState.prototype.isEntityLimitReached = function(category) { if(this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return (this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]); }; // defcon utilities m.GameState.prototype.timeSinceDefconChange = function() { return this.getTimeElapsed()-this.ai.defconChangeTime; }; m.GameState.prototype.setDefcon = function(level,force) { if (this.ai.defcon >= level || force) { this.ai.defcon = level; this.ai.defconChangeTime = this.getTimeElapsed(); } }; m.GameState.prototype.defcon = function() { return this.ai.defcon; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 14588) @@ -1,407 +1,432 @@ var API3 = function(m) { // Shared script handling templates and basic terrain analysis m.SharedScript = function(settings) { if (!settings) return; this._players = settings.players; this._templates = settings.templates; this._derivedTemplates = {}; this._techTemplates = settings.techTemplates; this._entityMetadata = {}; for (var i in this._players) this._entityMetadata[this._players[i]] = {}; // always create for 8 + gaia players, since _players isn't aware of the human. this._techModifications = { 0 : {}, 1 : {}, 2 : {}, 3 : {}, 4 : {}, 5 : {}, 6 : {}, 7 : {}, 8 : {} }; // array of entity collections this._entityCollections = []; // each name is a reference to the actual one. this._entityCollectionsName = {}; this._entityCollectionsByDynProp = {}; this._entityCollectionsUID = 0; // A few notes about these maps. They're updated by checking for "create" and "destroy" events for all resources // TODO: change the map when the resource amounts change for at least stone and metal mines. this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement. // Resource maps data. // By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4"). this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0}; this.turn = 0; } //Return a simple object (using no classes etc) that will be serialized //into saved games //TODO: that m.SharedScript.prototype.Serialize = function() { return { "players" : this._players, "templates" : this._templates, "techTp" : this._techTemplates }; }; // Called after the constructor when loading a saved game, with 'data' being // whatever Serialize() returned m.SharedScript.prototype.Deserialize = function(data) { this._players = data.players; this._templates = data.templates; this._techTemplates = data.techTp; this.isDeserialized = true; }; // Components that will be disabled in foundation entity templates. // (This is a bit yucky and fragile since it's the inverse of // CCmpTemplateManager::CopyFoundationSubset and only includes components -// that our EntityTemplate class currently uses.) +// that our Template class currently uses.) m.g_FoundationForbiddenComponents = { "ProductionQueue": 1, "ResourceSupply": 1, "ResourceDropsite": 1, "GarrisonHolder": 1, }; // Components that will be disabled in resource entity templates. // Roughly the inverse of CCmpTemplateManager::CopyResourceSubset. m.g_ResourceForbiddenComponents = { "Cost": 1, "Decay": 1, "Health": 1, "UnitAI": 1, "UnitMotion": 1, "Vision": 1 }; m.SharedScript.prototype.GetTemplate = function(name) { if (this._templates[name]) return this._templates[name]; if (this._derivedTemplates[name]) return this._derivedTemplates[name]; // If this is a foundation template, construct it automatically if (name.indexOf("foundation|") !== -1) { var base = this.GetTemplate(name.substr(11)); var foundation = {}; for (var key in base) if (!m.g_FoundationForbiddenComponents[key]) foundation[key] = base[key]; this._derivedTemplates[name] = foundation; return foundation; } else if (name.indexOf("resource|") !== -1) { var base = this.GetTemplate(name.substr(9)); var resource = {}; for (var key in base) if (!m.g_ResourceForbiddenComponents[key]) resource[key] = base[key]; this._derivedTemplates[name] = resource; return resource; } error("Tried to retrieve invalid template '"+name+"'"); return null; }; // Initialize the shared component. // We need to now the initial state of the game for this, as we will use it. // This is called right at the end of the map generation. m.SharedScript.prototype.init = function(state) { + this.ApplyTemplatesDelta(state); + this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; this.players = this._players; this.playersData = state.players; this.territoryMap = state.territoryMap; this.timeElapsed = state.timeElapsed; this.barterPrices = state.barterPrices; - for (var o in state.players) - this._techModifications[o] = state.players[o].techModifications; - this._entities = {}; for (var id in state.entities) this._entities[id] = new m.Entity(this, state.entities[id]); // entity collection updated on create/destroy event. this.entities = new m.EntityCollection(this, this._entities); // create the terrain analyzer this.terrainAnalyzer = new m.TerrainAnalysis(); this.terrainAnalyzer.init(this, state); this.accessibility = new m.Accessibility(); this.accessibility.init(state, this.terrainAnalyzer); // defined in TerrainAnalysis.js this.createResourceMaps(this); this.gameState = {}; for (var i in this._players) { this.gameState[this._players[i]] = new m.GameState(); this.gameState[this._players[i]].init(this,state,this._players[i]); } }; // General update of the shared script, before each AI's update // applies entity deltas, and each gamestate. m.SharedScript.prototype.onUpdate = function(state) { if (this.isDeserialized && this.turn !== 0) { this.isDeserialized = false; this.init(state); } else if (this.isDeserialized) return; // deals with updating based on create and destroy messages. this.ApplyEntitiesDelta(state); + this.ApplyTemplatesDelta(state); Engine.ProfileStart("onUpdate"); // those are dynamic and need to be reset as the "state" object moves in memory. this.events = state.events; this.passabilityClasses = state.passabilityClasses; this.passabilityMap = state.passabilityMap; this.playersData = state.players; this.territoryMap = state.territoryMap; this.timeElapsed = state.timeElapsed; this.barterPrices = state.barterPrices; - for (var o in state.players) - this._techModifications[o] = state.players[o].techModifications; - for (var i in this.gameState) this.gameState[i].update(this,state); // TODO: merge those two with "ApplyEntitiesDelta" since after all they do the same. this.updateResourceMaps(this, this.events); this.terrainAnalyzer.updateMapWithEvents(this); this.turn++; Engine.ProfileStop(); }; m.SharedScript.prototype.ApplyEntitiesDelta = function(state) { Engine.ProfileStart("Shared ApplyEntitiesDelta"); var foundationFinished = {}; // by order of updating: // we "Destroy" last because we want to be able to switch Metadata first. var CreateEvents = state.events["Create"]; var DestroyEvents = state.events["Destroy"]; var RenamingEvents = state.events["EntityRenamed"]; var TrainingEvents = state.events["TrainingFinished"]; var ConstructionEvents = state.events["ConstructionFinished"]; var MetadataEvents = state.events["AIMetadata"]; var ownershipChangeEvents = state.events["OwnershipChanged"]; for (var i = 0; i < CreateEvents.length; ++i) { var evt = CreateEvents[i]; if (!state.entities[evt.entity]) continue; // Sometimes there are things like foundations which get destroyed too fast this._entities[evt.entity] = new m.Entity(this, state.entities[evt.entity]); this.entities.addEnt(this._entities[evt.entity]); // Update all the entity collections since the create operation affects static properties as well as dynamic for (var entCollection in this._entityCollections) this._entityCollections[entCollection].updateEnt(this._entities[evt.entity]); } for (var i in RenamingEvents) { var evt = RenamingEvents[i]; // Switch the metadata for (var p in this._players) { this._entityMetadata[this._players[p]][evt.newentity] = this._entityMetadata[this._players[p]][evt.entity]; this._entityMetadata[this._players[p]][evt.entity] = {}; } } for (var i in TrainingEvents) { var evt = TrainingEvents[i]; // Apply metadata stored in training queues for each (var ent in evt.entities) { for (var key in evt.metadata) { this.setMetadata(evt.owner, this._entities[ent], key, evt.metadata[key]) } } } for (var i in ConstructionEvents) { var evt = ConstructionEvents[i]; // we'll move metadata. if (!this._entities[evt.entity]) continue; var ent = this._entities[evt.entity]; var newEnt = this._entities[evt.newentity]; if (this._entityMetadata[ent.owner()] && this._entityMetadata[ent.owner()][evt.entity] !== undefined) for (var key in this._entityMetadata[ent.owner()][evt.entity]) { this.setMetadata(ent.owner(), newEnt, key, this._entityMetadata[ent.owner()][evt.entity][key]) } foundationFinished[evt.entity] = true; } for (var i in MetadataEvents) { var evt = MetadataEvents[i]; if (!this._entities[evt.id]) continue; // might happen in some rare cases of foundations getting destroyed, perhaps. // Apply metadata (here for buildings for example) for (var key in evt.metadata) { this.setMetadata(evt.owner, this._entities[evt.id], key, evt.metadata[key]) } } for (var i = 0; i < DestroyEvents.length; ++i) { var evt = DestroyEvents[i]; // A small warning: javascript "delete" does not actually delete, it only removes the reference in this object. // the "deleted" object remains in memory, and any older reference to it will still reference it as if it were not "deleted". // Worse, they might prevent it from being garbage collected, thus making it stay alive and consuming ram needlessly. // So take care, and if you encounter a weird bug with deletion not appearing to work correctly, this is probably why. if (!this._entities[evt.entity]) continue; if (foundationFinished[evt.entity]) evt["SuccessfulFoundation"] = true; // The entity was destroyed but its data may still be useful, so // remember the entity and this AI's metadata concerning it evt.metadata = {}; evt.entityObj = this._entities[evt.entity]; for (var j in this._players) evt.metadata[this._players[j]] = this._entityMetadata[this._players[j]][evt.entity]; for each (var entCol in this._entityCollections) { entCol.removeEnt(this._entities[evt.entity]); } this.entities.removeEnt(this._entities[evt.entity]); delete this._entities[evt.entity]; for (var j in this._players) delete this._entityMetadata[this._players[j]][evt.entity]; } for (var id in state.entities) { var changes = state.entities[id]; for (var prop in changes) { this._entities[id]._entity[prop] = changes[prop]; this.updateEntityCollections(prop, this._entities[id]); } } + + // apply per-entity aura-related changes. + // this supersedes tech-related changes. + for (var id in state.changedEntityTemplateInfo) + { + var changes = state.changedEntityTemplateInfo[id]; + for each (var change in changes) + this._entities[id]._auraTemplateModif[change.variable] = change.value; + } + Engine.ProfileStop(); +}; + +m.SharedScript.prototype.ApplyTemplatesDelta = function(state) +{ + Engine.ProfileStart("Shared ApplyTemplatesDelta"); + + for (var player in state.changedTemplateInfo) + { + var playerDiff = state.changedTemplateInfo[player]; + for (var template in playerDiff) + { + var changes = playerDiff[template]; + if (!this._techModifications[player][template]) + this._techModifications[player][template] = {}; + for each (var change in changes) + this._techModifications[player][template][change.variable] = change.value; + } + } Engine.ProfileStop(); }; m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection, noPush) { if (!noPush) { this._entityCollections.push(entCollection); } entCollection.setUID(this._entityCollectionsUID); for each (var prop in entCollection.dynamicProperties()) { this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || []; this._entityCollectionsByDynProp[prop].push(entCollection); } this._entityCollectionsUID++; }; m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection) { for (var i in this._entityCollections) { if (this._entityCollections[i].getUID() === entCollection.getUID()) { this._entityCollections.splice(i, 1); } } for each (var prop in entCollection.dynamicProperties()) { for (var i in this._entityCollectionsByDynProp[prop]) { if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID()) { this._entityCollectionsByDynProp[prop].splice(i, 1); } } } }; m.SharedScript.prototype.updateEntityCollections = function(property, ent) { if (this._entityCollectionsByDynProp[property] !== undefined) { for (var entCollectionid in this._entityCollectionsByDynProp[property]) { this._entityCollectionsByDynProp[property][entCollectionid].updateEnt(ent); } } } m.SharedScript.prototype.setMetadata = function(player, ent, key, value) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata) metadata = this._entityMetadata[player][ent.id()] = {}; metadata[key] = value; this.updateEntityCollections('metadata', ent); this.updateEntityCollections('metadata.' + key, ent); }; m.SharedScript.prototype.getMetadata = function(player, ent, key) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return undefined; return metadata[key]; }; m.SharedScript.prototype.deleteMetadata = function(player, ent, key) { var metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return true; metadata[key] = undefined; delete metadata[key]; return true; }; m.copyPrototype = function(descendant, parent) { var sConstructor = parent.toString(); var aMatch = sConstructor.match( /\s*function (.*)\(/ ); if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; } for (var m in parent.prototype) { descendant.prototype[m] = parent.prototype[m]; } }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/components/AIInterface.js (revision 14588) @@ -1,159 +1,245 @@ function AIInterface() {} AIInterface.prototype.Schema = ""; AIInterface.prototype.Init = function() { this.events = {}; this.events["Create"] = []; this.events["Destroy"] = []; this.events["Attacked"] = []; this.events["RangeUpdate"] = []; this.events["ConstructionFinished"] = []; this.events["TrainingFinished"] = []; this.events["AIMetadata"] = []; this.events["PlayerDefeated"] = []; this.events["EntityRenamed"] = []; this.events["OwnershipChanged"] = []; this.changedEntities = {}; + + // cache for technology changes; + // this one is PlayerID->TemplateName->{StringForTheValue, ActualValue} + this.changedTemplateInfo = {}; + // this is for auras and is EntityID->{StringForTheValue, ActualValue} + this.changedEntityTemplateInfo = {}; }; -AIInterface.prototype.GetRepresentation = function() +AIInterface.prototype.GetNonEntityRepresentation = function() { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - + // Return the same game state as the GUI uses var state = cmpGuiInterface.GetExtendedSimulationState(-1); - + // Add some extra AI-specific data state.events = {}; state.events["Create"] = this.events["Create"]; state.events["Destroy"] = this.events["Destroy"]; state.events["Attacked"] = this.events["Attacked"]; state.events["RangeUpdate"] = this.events["RangeUpdate"]; state.events["ConstructionFinished"] = this.events["ConstructionFinished"]; state.events["TrainingFinished"] = this.events["TrainingFinished"]; state.events["AIMetadata"] = this.events["AIMetadata"]; state.events["PlayerDefeated"] = this.events["PlayerDefeated"]; state.events["EntityRenamed"] = this.events["EntityRenamed"]; state.events["OwnershipChanged"] = this.events["OwnershipChanged"]; - // Reset the event list for the next turn this.events["Create"] = []; this.events["Destroy"] = []; this.events["Attacked"] = []; this.events["RangeUpdate"] = []; this.events["ConstructionFinished"] = []; this.events["TrainingFinished"] = []; this.events["AIMetadata"] = []; this.events["PlayerDefeated"] = []; this.events["EntityRenamed"] = []; this.events["OwnershipChanged"] = []; - + + return state; +}; + +AIInterface.prototype.GetRepresentation = function() +{ + var state = this.GetNonEntityRepresentation(); + // Add entity representations Engine.ProfileStart("proxy representations"); state.entities = {}; for (var id in this.changedEntities) { var aiProxy = Engine.QueryInterface(+id, IID_AIProxy); if (aiProxy) state.entities[id] = aiProxy.GetRepresentation(); } this.changedEntities = {}; Engine.ProfileStop(); + state.changedTemplateInfo = this.changedTemplateInfo; + this.changedTemplateInfo = {}; + state.changedEntityTemplateInfo = this.changedEntityTemplateInfo; + this.changedEntityTemplateInfo = {}; + return state; }; + // Intended to be called first, during the map initialization: no caching AIInterface.prototype.GetFullRepresentation = function(flushEvents) -{ - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - - // Return the same game state as the GUI uses - var state = cmpGuiInterface.GetExtendedSimulationState(-1); - - // Add some extra AI-specific data - state.events = {}; - state.events["Create"] = this.events["Create"]; - state.events["Destroy"] = this.events["Destroy"]; - state.events["Attacked"] = this.events["Attacked"]; - state.events["RangeUpdate"] = this.events["RangeUpdate"]; - state.events["ConstructionFinished"] = this.events["ConstructionFinished"]; - state.events["TrainingFinished"] = this.events["TrainingFinished"]; - state.events["AIMetadata"] = this.events["AIMetadata"]; - state.events["PlayerDefeated"] = this.events["PlayerDefeated"]; - state.events["EntityRenamed"] = this.events["EntityRenamed"]; - state.events["OwnershipChanged"] = this.events["OwnershipChanged"]; - +{ + var state = this.GetNonEntityRepresentation(); if (flushEvents) { state.events["Create"] = []; state.events["Destroy"] = []; state.events["Attacked"] = []; state.events["RangeUpdate"] = []; state.events["ConstructionFinished"] = []; state.events["TrainingFinished"] = []; state.events["AIMetadata"] = []; state.events["PlayerDefeated"] = []; state.events["EntityRenamed"] = []; state.events["OwnershipChanged"] = []; } - // Reset the event list for the next turn - this.events["Create"] = []; - this.events["Destroy"] = []; - this.events["Attacked"] = []; - this.events["RangeUpdate"] = []; - this.events["ConstructionFinished"] = []; - this.events["TrainingFinished"] = []; - this.events["AIMetadata"] = []; - this.events["PlayerDefeated"] = []; - this.events["EntityRenamed"] = []; - this.events["OwnershipChanged"] = []; - - // Add entity representations Engine.ProfileStart("proxy representations"); state.entities = {}; // all entities are changed in the initial state. for (var id in this.changedEntities) { var aiProxy = Engine.QueryInterface(+id, IID_AIProxy); if (aiProxy) state.entities[id] = aiProxy.GetFullRepresentation(); } Engine.ProfileStop(); + state.changedTemplateInfo = this.changedTemplateInfo; + this.changedTemplateInfo = {}; + state.changedEntityTemplateInfo = this.changedEntityTemplateInfo; + this.changedEntityTemplateInfo = {}; + return state; }; AIInterface.prototype.ChangedEntity = function(ent) { this.changedEntities[ent] = 1; }; // AIProxy sets up a load of event handlers to capture interesting things going on // in the world, which we will report to AI. Handle those, and add a few more handlers // for events that AIProxy won't capture. AIInterface.prototype.PushEvent = function(type, msg) { if (this.events[type] === undefined) warn("Tried to push unknown event type " + type +", please add it to AIInterface.js"); this.events[type].push(msg); }; AIInterface.prototype.OnGlobalPlayerDefeated = function(msg) { this.events["PlayerDefeated"].push(msg); }; AIInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.events["EntityRenamed"].push(msg); }; +// When a new technology is researched, check which templates it affects, +// and send the updated values to the AI. +// this relies on the fact that any "value" in a technology can only ever change +// one template value, and that the naming is the same (with / in place of .) +// it's not incredibly fast but it's not incredibly slow. +AIInterface.prototype.OnTemplateModification = function(msg) +{ + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + if (!this.templates) + this.templates = cmpTemplateManager.FindAllTemplates(false); + + for each (var value in msg.valueNames) + { + for (var o = 0; o < this.templates.length; ++o) + { + var tmp = this.templates[o]; + var template = cmpTemplateManager.GetTemplateWithoutValidation(this.templates[o]); + // remove templates that we obviously don't care about. + if (!template || !template.Identity || ! template.Identity.Civ) + { + this.templates.splice(o--,1); + continue; + } + + // let's get the base template value. + var strings = value.split("/"); + var item = template; + var ended = true; + for (var i = 0; i < strings.length; ++i) + { + if (item !== undefined && item[strings[i]] !== undefined) + item = item[strings[i]]; + else + ended = false; + } + if (!ended) + continue; + // item now contains the template value for this. + + // check for numerals, they need to be handled properly + item = !isNaN(+item) ? +item : item; + var newValue = ApplyValueModificationsToTemplate(value, item, msg.player, template); + // round the value to 5th decimal or so. + newValue = !isNaN(+newValue) ? (Math.abs((+newValue) - Math.round(+newValue)) < 0.0001 ? Math.round(+newValue) : +newValue) : newValue; + + if(item != newValue) + { + if (!this.changedTemplateInfo[msg.player]) + this.changedTemplateInfo[msg.player] = {}; + if (!this.changedTemplateInfo[msg.player][this.templates[o]]) + this.changedTemplateInfo[msg.player][this.templates[o]] = [ { "variable" : value, "value" : newValue} ]; + else + this.changedTemplateInfo[msg.player][this.templates[o]].push({ "variable" : value, "value" : newValue }); + } + } + } +}; + +AIInterface.prototype.OnGlobalValueModification = function(msg) +{ + var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + for each (var ent in msg.entities) + { + var template = cmpTemplateManager.GetTemplateWithoutValidation(cmpTemplateManager.GetCurrentTemplateName(ent)); + for each (var value in msg.valueNames) + { + // let's get the base template value. + var strings = value.split("/"); + var item = template; + var ended = true; + for (var i = 0; i < strings.length; ++i) + { + if (item !== undefined && item[strings[i]] !== undefined) + item = item[strings[i]]; + else + ended = false; + } + if (!ended) + continue; + // "item" now contains the unmodified template value for this. + var newValue = ApplyValueModificationsToEntity(value, +item, ent); + newValue = typeof(newValue) === "Number" ? Math.round(newValue) : newValue; + if(item != newValue) + { + if (!this.changedEntityTemplateInfo[ent]) + this.changedEntityTemplateInfo[ent] = [{ "variable" : value, "value" : newValue }]; + else + this.changedEntityTemplateInfo[ent].push({ "variable" : value, "value" : newValue }); + } + } + } +}; + Engine.RegisterComponentType(IID_AIInterface, "AIInterface", AIInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 14588) @@ -1,315 +1,314 @@ function AIProxy() {} AIProxy.prototype.Schema = ""; /** * AIProxy passes its entity's state data to AI scripts. * * Efficiency is critical: there can be many thousands of entities, * and the data returned by this component is serialized and copied to * the AI thread every turn, so it can be quite expensive. * * We omit all data that can be derived statically from the template XML * files - the AI scripts can parse the templates themselves. * This violates the component interface abstraction and is potentially * fragile if the template formats change (since both the component code * and the AI will have to be updated in sync), but it's not *that* bad * really and it helps performance significantly. * * We also add an optimisation to avoid copying non-changing values. * The first call to GetRepresentation calls GetFullRepresentation, * which constructs the complete entity state representation. * After that, we simply listen to events from the rest of the gameplay code, * and store the changed data in this.changes. * Properties in this.changes will override those previously returned * from GetRepresentation; if a property isn't overridden then the AI scripts * will keep its old value. * * The event handlers should set this.changes.whatever to exactly the * same as GetFullRepresentation would set. */ AIProxy.prototype.Init = function() { this.changes = null; this.needsFullGet = true; this.owner = -1; // for convenience now and then. + this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); + // Let the AIInterface know that we exist and that it should query us this.NotifyChange(); }; +AIProxy.prototype.Serialize = null; // we have no dynamic state to save + +AIProxy.prototype.Deserialize = function () +{ + this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); +}; + AIProxy.prototype.GetRepresentation = function() { // Return the full representation the first time we're called var ret; if (this.needsFullGet) { ret = this.GetFullRepresentation(); this.needsFullGet = false; } else { ret = this.changes; } // Initialise changes to null instead of {}, to avoid memory allocations in the // common case where there will be no changes; event handlers should each reset // it to {} if needed this.changes = null; return ret; }; AIProxy.prototype.NotifyChange = function() { if (!this.changes) { this.changes = {}; - - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.ChangedEntity(this.entity); + this.cmpAIInterface.ChangedEntity(this.entity); } }; // AI representation-updating event handlers: AIProxy.prototype.OnPositionChanged = function(msg) { this.NotifyChange(); if (msg.inWorld) this.changes.position = [msg.x, msg.z]; else this.changes.position = undefined; }; AIProxy.prototype.OnHealthChanged = function(msg) { this.NotifyChange(); this.changes.hitpoints = msg.to; }; AIProxy.prototype.OnUnitIdleChanged = function(msg) { this.NotifyChange(); this.changes.idle = msg.idle; }; AIProxy.prototype.OnUnitAIStateChanged = function(msg) { this.NotifyChange(); this.changes.unitAIState = msg.to; }; AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg) { this.NotifyChange(); this.changes.unitAIOrderData = msg.to; }; AIProxy.prototype.OnProductionQueueChanged = function(msg) { this.NotifyChange(); var cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); this.changes.trainingQueue = cmpProductionQueue.GetQueue(); }; AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg) { this.NotifyChange(); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); this.changes.garrisoned = cmpGarrisonHolder.GetEntities(); }; AIProxy.prototype.OnResourceSupplyChanged = function(msg) { this.NotifyChange(); this.changes.resourceSupplyAmount = msg.to; }; AIProxy.prototype.OnResourceSupplyGatherersChanged = function(msg) { this.NotifyChange(); this.changes.resourceSupplyGatherers = msg.to; }; AIProxy.prototype.OnResourceCarryingChanged = function(msg) { this.NotifyChange(); this.changes.resourceCarrying = msg.to; }; AIProxy.prototype.OnFoundationProgressChanged = function(msg) { this.NotifyChange(); this.changes.foundationProgress = msg.to; }; // TODO: event handlers for all the other things AIProxy.prototype.GetFullRepresentation = function() { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var ret = { // These properties are constant and won't need to be updated "id": this.entity, "template": cmpTemplateManager.GetCurrentTemplateName(this.entity) } var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) { // Updated by OnPositionChanged if (cmpPosition.IsInWorld()) { var pos = cmpPosition.GetPosition2D(); ret.position = [pos.x, pos.y]; } else { ret.position = undefined; } } var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (cmpHealth) { // Updated by OnHealthChanged ret.hitpoints = cmpHealth.GetHitpoints(); } var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) { // Updated by OnOwnershipChanged ret.owner = cmpOwnership.GetOwner(); if (!this.owner) this.owner = ret.owner; } var cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) { // Updated by OnUnitIdleChanged ret.idle = cmpUnitAI.IsIdle(); // Updated by OnUnitAIStateChanged ret.unitAIState = cmpUnitAI.GetCurrentState(); // Updated by OnUnitAIOrderDataChanged ret.unitAIOrderData = cmpUnitAI.GetOrderData(); } var cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue) { // Updated by OnProductionQueueChanged ret.trainingQueue = cmpProductionQueue.GetQueue(); } var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) { // Updated by OnFoundationProgressChanged ret.foundationProgress = cmpFoundation.GetBuildPercentage(); } var cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); if (cmpResourceSupply) { // Updated by OnResourceSupplyChanged ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); ret.resourceSupplyGatherers = cmpResourceSupply.GetGatherers(); } var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); if (cmpResourceGatherer) { // Updated by OnResourceCarryingChanged ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Updated by OnGarrisonedUnitsChanged ret.garrisoned = cmpGarrisonHolder.GetEntities(); } return ret; }; // AI event handlers: // (These are passed directly as events to the AI scripts, rather than updating // our proxy representation.) // (This shouldn't include extremely high-frequency events, like PositionChanged, // because that would be very expensive and AI will rarely care about all those // events.) // special case: this changes the state and sends an event. AIProxy.prototype.OnOwnershipChanged = function(msg) { this.NotifyChange(); if (msg.from === -1) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("Create", {"entity" : msg.entity}); + this.cmpAIInterface.PushEvent("Create", {"entity" : msg.entity}); return; } else if (msg.to === -1) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("Destroy", {"entity" : msg.entity}); + this.cmpAIInterface.PushEvent("Destroy", {"entity" : msg.entity}); return; } this.owner = msg.to; this.changes.owner = msg.to; - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("OwnershipChanged", msg); + this.cmpAIInterface.PushEvent("OwnershipChanged", msg); }; AIProxy.prototype.OnAttacked = function(msg) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("Attacked", msg); + this.cmpAIInterface.PushEvent("Attacked", msg); }; /* Deactivated for actually not really being practical for most uses. AIProxy.prototype.OnRangeUpdate = function(msg) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); msg.owner = this.owner; - cmpAIInterface.PushEvent("RangeUpdate", msg); + this.cmpAIInterface.PushEvent("RangeUpdate", msg); warn(uneval(msg)); };*/ AIProxy.prototype.OnConstructionFinished = function(msg) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("ConstructionFinished", msg); + this.cmpAIInterface.PushEvent("ConstructionFinished", msg); }; AIProxy.prototype.OnTrainingFinished = function(msg) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("TrainingFinished", msg); + this.cmpAIInterface.PushEvent("TrainingFinished", msg); }; AIProxy.prototype.OnAIMetadata = function(msg) { - var cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); - cmpAIInterface.PushEvent("AIMetadata", msg); + this.cmpAIInterface.PushEvent("AIMetadata", msg); }; Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy); Index: ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/components/AuraManager.js (revision 14588) @@ -1,171 +1,175 @@ function AuraManager() {} AuraManager.prototype.Schema = ""; AuraManager.prototype.Init = function() { this.modificationsCache = {}; this.modifications = {}; this.templateModificationsCache = {}; this.templateModifications = {}; }; AuraManager.prototype.ensureExists = function(name, value, id, key, defaultData) { if (!this[name][value]) { this[name][value] = {}; this[name+'Cache'][value] = {}; } if (!this[name][value][id]) { this[name][value][id] = {}; this[name+'Cache'][value][id] = defaultData; } if (!this[name][value][id][key]) this[name][value][id][key] = []; } AuraManager.prototype.ApplyBonus = function(value, ent, data, key) { this.ensureExists("modifications", value, ent, key, {"add":0, "multiply":1}); this.modifications[value][ent][key].push(data); if (this.modifications[value][ent][key].length > 1) return; // first time added this aura if (data.multiply) this.modificationsCache[value][ent].multiply *= data.multiply; if (data.add) this.modificationsCache[value][ent].add += data.add; // post message to the entity to notify it about the change - Engine.PostMessage(ent, MT_ValueModification, { "component": value.split("/")[0] }); + Engine.PostMessage(ent, MT_ValueModification, { "entities": [ent], "component": value.split("/")[0], "valueNames": [value] }); }; AuraManager.prototype.ApplyTemplateBonus = function(value, player, classes, data, key) { this.ensureExists("templateModifications", value, player, key, {}); this.templateModifications[value][player][key].push(data); if (this.templateModifications[value][player][key].length > 1) return; // first time added this aura for each (var c in classes) { if (!this.templateModificationsCache[value][player][c]) this.templateModificationsCache[value][player][c] = []; if (!this.templateModificationsCache[value][player][c][key]) this.templateModificationsCache[value][player][c][key] = { "add": 0, "multiply": 1}; if (data.multiply) this.templateModificationsCache[value][player][c][key].multiply *= data.multiply; if (data.add) this.templateModificationsCache[value][player][c][key].add += data.add; + Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": player, "component": value.split("/")[0], "valueNames": [value] }); } }; AuraManager.prototype.RemoveBonus = function(value, ent, key) { if (!this.modifications[value] || !this.modifications[value][ent] || !this.modifications[value][ent][key] || !this.modifications[value][ent][key].length) return; // get the applied data to remove again var data = this.modifications[value][ent][key].pop(); if (this.modifications[value][ent][key].length > 0) return; // out of last aura of this kind, remove modifications if (data.add) this.modificationsCache[value][ent].add -= data.add; if (data.multiply) this.modificationsCache[value][ent].multiply /= data.multiply; // post message to the entity to notify it about the change - Engine.PostMessage(ent, MT_ValueModification, { "component": value.split("/")[0] }); + var effects = {}; + effects[value] = this.modificationsCache[value][ent]; + Engine.PostMessage(ent, MT_ValueModification, { "entities": [ent], "component": value.split("/")[0], "valueNames": [value] }); }; AuraManager.prototype.RemoveTemplateBonus = function(value, player, classes, key) { if (!this.templateModifications[value] || !this.templateModifications[value][player] || !this.templateModifications[value][player][key] || !this.templateModifications[value][player][key].length) return; this.templateModifications[value][player][key].pop(); if (this.templateModifications[value][player][key].length > 0) return; for each (var c in classes) { this.templateModificationsCache[value][player][c][key].multiply = 1; this.templateModificationsCache[value][player][c][key].add = 0; } + Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": player, "component": value.split("/")[0], "valueNames": [value] }); }; AuraManager.prototype.ApplyModifications = function(valueName, value, ent) { if (!this.modificationsCache[valueName] || !this.modificationsCache[valueName][ent]) return value; value *= this.modificationsCache[valueName][ent].multiply; value += this.modificationsCache[valueName][ent].add; return value; }; AuraManager.prototype.ApplyTemplateModifications = function(valueName, value, player, template) { if (!this.templateModificationsCache[valueName] || !this.templateModificationsCache[valueName][player]) return value; var rawClasses; if (template && template.Identity) { rawClasses = template.Identity.Classes; rawClasses = "_string" in rawClasses ? rawClasses._string : ""; if (template.Identity.Rank) rawClasses += " " + template.Identity.Rank; } var classes = rawClasses && rawClasses.length ? rawClasses.split(/\s+/) : []; var keyList = []; for (var c in this.templateModificationsCache[valueName][player]) { if (classes.indexOf(c) == -1) continue; for (var key in this.templateModificationsCache[valueName][player][c]) { // don't add an aura with the same key twice if (keyList.indexOf(key) != -1) continue; value *= this.templateModificationsCache[valueName][player][c][key].multiply; value += this.templateModificationsCache[valueName][player][c][key].add; keyList.push(key); } } return value; }; Engine.RegisterComponentType(IID_AuraManager, "AuraManager", AuraManager); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 14588) @@ -1,2004 +1,2003 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised so we mustn't serialise // its non-deterministic data. Instead just return an empty object. return {}; }; GuiInterface.prototype.Deserialize = function(obj) { this.Init(); }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function(player) { var ret = { "players": [] }; var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); // Work out what phase we are in var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); var phase = ""; if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; // store player ally/neutral/enemy data as arrays var allies = []; var mutualAllies = []; var neutrals = []; var enemies = []; for (var j = 0; j < n; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } var playerData = { "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "colour": cmpPlayer.GetColour(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "heroes": cmpPlayer.GetHeroes(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits.GetLimits(), "entityCounts": cmpPlayerEntityLimits.GetCounts(), "entityLimitChangers": cmpPlayerEntityLimits.GetLimitChangers(), - "techModifications": cmpTechnologyManager.GetTechModifications(), "researchQueued": cmpTechnologyManager.GetQueuedResearch(), "researchStarted": cmpTechnologyManager.GetStartedResearch(), "researchedTechs": cmpTechnologyManager.GetResearchedTechs(), "classCounts": cmpTechnologyManager.GetClassCounts(), "typeCountsByClass": cmpTechnologyManager.GetTypeCountsByClass() }; ret.players.push(playerData); } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) { ret.circularMap = cmpRangeManager.GetLosCircular(); } // Add timeElapsed var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); return ret; }; GuiInterface.prototype.GetExtendedSimulationState = function(player) { // Get basic simulation info var ret = this.GetSimulationState(); // Add statistics to each player var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var n = cmpPlayerMan.GetNumPlayers(); for (var i = 0; i < n; ++i) { var playerEnt = cmpPlayerMan.GetPlayerByID(i); var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); } // Add bartering prices var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterPrices = cmpBarter.GetPrices(); return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function(player) { this.renamedEntities = []; }; /** * Get common entity info, often used in the gui */ GuiInterface.prototype.GetEntityState = function(player, ent) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id var template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; var ret = { "id": ent, "template": template }; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) { ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName() }; } var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); ret.rotation = cmpPosition.GetRotation(); } var cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = Math.ceil(cmpHealth.GetHitpoints()); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); ret.needsHeal = !cmpHealth.IsUnhealable(); } var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) { ret.buildEntities = cmpBuilder.GetEntitiesList(); } var cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) { ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; } var cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) { ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "queue": cmpProductionQueue.GetQueue(), }; } var cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) { ret.trader = { "goods": cmpTrader.GetGoods(), "requiredGoods": cmpTrader.GetRequiredGoods() }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) { ret.player = cmpOwnership.GetOwner(); } var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { ret.rallyPoint = {'position': cmpRallyPoint.GetPositions()[0]}; // undefined or {x,z} object } var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) { ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; } var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), }; // Add some information needed for ungarrisoning if (cmpUnitAI.isGarrisoned && ret.player !== undefined) ret.template = "p" + ret.player + "&" + ret.template; } var cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) { ret.guard = { "entities": cmpGuard.GetEntities(), }; } var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) { ret.gate = { "locked": cmpGate.IsLocked(), }; } var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) { ret.alertRaiser = { "level": cmpAlertRaiser.GetLevel(), "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(), "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(), }; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); return ret; }; /** * Get additionnal entity info, rarely used in the gui */ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) { var ret = {}; var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? show all? ret.attack = cmpAttack.GetAttackStrengths(type); var range = cmpAttack.GetRange(type); ret.attack.type = type; ret.attack.minRange = range.min; ret.attack.maxRange = range.max; var timers = cmpAttack.GetTimers(type); ret.attack.prepareTime = timers.prepare; ret.attack.repeatTime = timers.repeat; if (type == "Ranged") { ret.attack.elevationBonus = range.elevationBonus; var cmpPosition = Engine.QueryInterface(ent, IID_Position); var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the rage in front of it, no spread. So angle = 0 ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack.elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack.elevationAdaptedRange = ret.attack.maxRange; } } else { // not a ranged attack, set some defaults ret.attack.elevationBonus = 0; ret.attack.elevationAdaptedRange = ret.attack.maxRange; } } var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) { ret.armour = cmpArmour.GetArmourStrengths(); } var cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) { ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) { ret.obstruction = { "controlGroup": cmpObstruction.GetControlGroup(), "controlGroup2": cmpObstruction.GetControlGroup2(), }; } var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) { ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "gatherers": cmpResourceSupply.GetGatherers(player) }; } var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) { ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); } var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) { ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes() }; } var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) { ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; } var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); ret.barterMarket = { "prices": cmpBarter.GetPrices() }; } var cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) { ret.healer = { "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; } return ret; }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var rot = {x:0, y:0, z:0}; var pos = {x:cmd.x,z:cmd.z}; pos.y = cmpTerrain.GetGroundLevel(cmd.x, cmd.z); var elevationBonus = cmd.elevationBonus || 0; var range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, extendedName) { var name = extendedName; // Special case for garrisoned units which have a extended template if (extendedName.indexOf("&") != -1) name = extendedName.slice(extendedName.indexOf("&")+1); var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(name); if (!template) return null; var ret = {}; if (template.Armour) { ret.armour = { "hack": ApplyValueModificationsToTemplate("Armour/Hack", +template.Armour.Hack, player, template), - "pierce": ApplyValueModificationsToTemplate("Armour/Pierce", +template.Armour.Hack, player, template), - "crush": ApplyValueModificationsToTemplate("Armour/Crush", +template.Armour.Hack, player, template), + "pierce": ApplyValueModificationsToTemplate("Armour/Pierce", +template.Armour.Pierce, player, template), + "crush": ApplyValueModificationsToTemplate("Armour/Crush", +template.Armour.Crush, player, template), }; } if (template.Attack) { ret.attack = {}; for (var type in template.Attack) { ret.attack[type] = { "hack": ApplyValueModificationsToTemplate("Attack/"+type+"/Hack", +(template.Attack[type].Hack || 0), player, template), "pierce": ApplyValueModificationsToTemplate("Attack/"+type+"/Pierce", +(template.Attack[type].Pierce || 0), player, template), "crush": ApplyValueModificationsToTemplate("Attack/"+type+"/Crush", +(template.Attack[type].Crush || 0), player, template), "minRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MinRange", +(template.Attack[type].MinRange || 0), player, template), "maxRange": ApplyValueModificationsToTemplate("Attack/"+type+"/MaxRange", +template.Attack[type].MaxRange, player, template), "elevationBonus": ApplyValueModificationsToTemplate("Attack/"+type+"/ElevationBonus", +(template.Attack[type].ElevationBonus || 0), player, template), }; } } if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromCategory": template.BuildRestrictions.Distance.FromCategory, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance; if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance; } } if (template.TrainingRestrictions) { ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; } if (template.Cost) { ret.cost = {}; if (template.Cost.Resources.food) ret.cost.food = ApplyValueModificationsToTemplate("Cost/Resources/food", +template.Cost.Resources.food, player, template); if (template.Cost.Resources.wood) ret.cost.wood = ApplyValueModificationsToTemplate("Cost/Resources/wood", +template.Cost.Resources.wood, player, template); if (template.Cost.Resources.stone) ret.cost.stone = ApplyValueModificationsToTemplate("Cost/Resources/stone", +template.Cost.Resources.stone, player, template); if (template.Cost.Resources.metal) ret.cost.metal = ApplyValueModificationsToTemplate("Cost/Resources/metal", +template.Cost.Resources.metal, player, template); if (template.Cost.Population) ret.cost.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, player, template); if (template.Cost.PopulationBonus) ret.cost.populationBonus = ApplyValueModificationsToTemplate("Cost/PopulationBonus", +template.Cost.PopulationBonus, player, template); if (template.Cost.BuildTime) ret.cost.time = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, player, template); } if (template.Footprint) { ret.footprint = {"height": template.Footprint.Height}; if (template.Footprint.Square) ret.footprint.square = {"width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"]}; else if (template.Footprint.Circle) ret.footprint.circle = {"radius": +template.Footprint.Circle["@radius"]}; else warn("[GetTemplateData] Unrecognized Footprint type"); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else { ret.obstruction.shape.type = "cluster"; } } if (template.Pack) { ret.pack = { "state": template.Pack.State, "time": ApplyValueModificationsToTemplate("Pack/Time", +template.Pack.Time, player, template), }; } if (template.Health) { ret.health = Math.round(ApplyValueModificationsToTemplate("Health/Max", +template.Health.Max, player, template)); } if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.identityClassesString = GetTemplateIdentityClassesString(template); } if (template.UnitMotion) { ret.speed = { "walk": ApplyValueModificationsToTemplate("UnitMotion/WalkSpeed", +template.UnitMotion.WalkSpeed, player, template), }; if (template.UnitMotion.Run) ret.speed.run = ApplyValueModificationsToTemplate("UnitMotion/Run/Speed", +template.UnitMotion.Run.Speed, player, template); } if (template.Trader) ret.trader = template.Trader; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort, }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap, }; } if (template.WallPiece) { ret.wallPiece = {"length": +template.WallPiece.Length}; } return ret; }; GuiInterface.prototype.GetTechnologyData = function(player, name) { var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); var template = cmpTechTempMan.GetTemplate(name); if (!template) { warn("Tried to get data for invalid technology: " + name); return null; } var ret = {}; // Get specific name for this civ or else the generic specific name var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); var specific = undefined; if (template.specificName) { if (template.specificName[cmpPlayer.GetCiv()]) specific = template.specificName[cmpPlayer.GetCiv()]; else specific = template.specificName['generic']; } ret.name = { "specific": specific, "generic": template.genericName, }; ret.icon = "technologies/" + template.icon; ret.cost = { "food": template.cost ? (+template.cost.food) : 0, "wood": template.cost ? (+template.cost.wood) : 0, "metal": template.cost ? (+template.cost.metal) : 0, "stone": template.cost ? (+template.cost.stone) : 0, "time": template.researchTime ? (+template.researchTime) : 0, } ret.tooltip = template.tooltip; if (template.requirementsTooltip) ret.requirementsTooltip = template.requirementsTooltip; else ret.requirementsTooltip = ""; ret.description = template.description; return ret; }; GuiInterface.prototype.IsTechnologyResearched = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(tech); }; // Returns technologies that are being actively researched, along with // which entity is researching them and how far along the research is. GuiInterface.prototype.GetStartedResearch = function(player) { var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; var ret = {}; for (var tech in cmpTechnologyManager.GetTechsStarted()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; var cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; } // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { var cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); return cmpBattleDetection.GetState(); }; // Returns a list of ongoing attacks against the player. GuiInterface.prototype.GetIncomingAttacks = function(player) { var cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection); return cmpAttackDetection.GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, amounts) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return cmpPlayer.GetNeededResources(amounts); }; GuiInterface.prototype.AddTimeNotification = function(notification) { var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); notification.endTime = notification.time + time; notification.id = ++this.timeNotificationID; this.timeNotifications.push(notification); this.timeNotifications.sort(function (n1, n2){return n2.endTime - n1.endTime}); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { for (var i in this.timeNotifications) { if (this.timeNotifications[i].id == notificationID) { this.timeNotifications.splice(i); return; } } }; GuiInterface.prototype.GetTimeNotificationText = function(playerID) { var formatTime = function(time) { // add 1000 ms to get ceiled instead of floored millisecons // displaying 00:00 for a full second isn't nice time += 1000; var hours = Math.floor(time / 1000 / 60 / 60); var minutes = Math.floor(time / 1000 / 60) % 60; var seconds = Math.floor(time / 1000) % 60; return (hours ? hours + ':' : "") + (minutes < 10 ? '0' + minutes : minutes) + ':' + (seconds < 10 ? '0' + seconds : seconds); }; var time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); var text = ""; for each (var n in this.timeNotifications) { if (time >= n.endTime) { // delete the notification and start over this.DeleteTimeNotification(n.id); return this.GetTimeNotificationText(playerID); } if (n.players.indexOf(playerID) >= 0) text += n.message.replace("%T",formatTime(n.endTime - time))+"\n"; } return text; }; GuiInterface.prototype.PushNotification = function(notification) { this.notifications.push(notification); }; GuiInterface.prototype.GetNextNotification = function() { if (this.notifications.length) return this.notifications.pop(); else return ""; }; GuiInterface.prototype.GetAvailableFormations = function(player, data) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); return cmpPlayer.GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { var r = {}; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return r; r.name = template.Formation.FormationName; r.tooltip = template.Formation.DisabledTooltip; return r; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) return true; } } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for each (var ent in data.ents) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) { if (cmpUnitAI.GetStanceName() == data.stance) return true; } } return false; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var playerColours = {}; // cache of owner -> colour map for each (var ent in cmd.entities) { var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's colour: var owner = -1; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); var colour = playerColours[owner]; if (!colour) { colour = {"r":1, "g":1, "b":1}; var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); if (cmpPlayer) colour = cmpPlayer.GetColour(); playerColours[owner] = colour; } cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}, cmd.selected); } }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { for each (var ent in cmd.entities) { var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.SetEnabled(cmd.enabled); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); // If there are some rally points already displayed, first hide them for each (var ent in this.entsRallyPointsDisplayed) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for each (var ent in cmd.entities) { var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location) var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position var pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({'x': pos.x, 'y': pos.z}); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else if (cmd.queued == false) cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for each (var posi in cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({'x': posi.x, 'y': posi.z}); cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else empty string * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { var result = { "success": false, "message": "", } // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") { this.placementEntity = undefined; } else { this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } } if (this.placementEntity) { var ent = this.placementEntity[1]; // Move the preview into the right location var pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); // Set it to a red shade if this is an invalid location var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColour(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * 'populationBonus': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { var wallSet = cmd.wallSet; var start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; var end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // do some entity cache management and check for snapping if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // we're clearing the preview, clear the entity cache and bail var numCleared = 0; for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } else { // Move all existing cached entities outside of the world and reset their use count for (var tpl in this.placementWallEntities) { for each (var ent in this.placementWallEntities[tpl].entities) { var pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before for each (var tpl in wallSet.templates) { if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } } // prevent division by zero errors further on if the start and end positions are the same if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { var snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error var startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { var endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // clear the single-building preview entity (we'll be rolling our own) this.SetBuildingPlacementPreview(player, {"template": ""}); // -------------------------------------------------------------------------------- // calculate wall placement and position preview entities var result = { "pieces": [], "cost": {"food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0}, }; var previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { var startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group var startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { var cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true, // preview only, must not appear in the result }); } } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle) }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { var endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []); previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup()); } // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group var endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { var cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true }); } } } else { previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle) }); } } var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. var allPiecesValid = true; var numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (var i = 0; i < previewEntities.length; ++i) { var entInfo = previewEntities[i]; var ent = null; var tpl = entInfo.template; var tplData = this.placementWallEntities[tpl].templateData; var entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else { // reuse an existing one ent = entPool.entities[entPool.numUsed]; } if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // move piece to right location // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces if (tpl === wallSet.templates.tower) { var terrainGroundPrev = null; var terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { var targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. var primaryControlGroup = ent; var secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); // check whether this wall piece can be validly positioned here var validPlacement = false; var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region // tell GetLosVisibility to force RetainInFog because preview entities set this to false, // which would show them as hidden instead of fogged // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta var visible = (cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden"); if (visible) { var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success); // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) numRequiredPieces++; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. result.cost.food += tplData.cost.food; result.cost.wood += tplData.cost.wood; result.cost.stone += tplData.cost.stone; result.cost.metal += tplData.cost.metal; result.cost.population += tplData.cost.population; result.cost.populationBonus += tplData.cost.populationBonus; result.cost.time += tplData.cost.time; } var canAfford = true; var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) var canAfford = false; var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColour(1, 1, 1, 1); } entPool.numUsed++; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateMgr.GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest // (TODO: break unlikely ties by choosing the lowest entity ID) var minDist2 = -1; var minDistEntitySnapData = null; var radius2 = data.snapRadius * data.snapRadius; for each (var ent in data.snapEntities) { var cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; var pos = cmpPosition.GetPosition(); var dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = {"x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent}; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (template.BuildRestrictions.Category == "Dock") { // warning: copied almost identically in helpers/command.js , "GetDockAngle". var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) { return false; } // Get footprint size var halfSize = 0; if (template.Footprint.Square) { halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; } else if (template.Footprint.Circle) { halfSize = template.Footprint.Circle["@radius"]; } /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = data.x - d*Math.sin(angle); var nz = data.z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) { waterPoints.push(i); } } var consec = []; var length = waterPoints.length; 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 {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)}; } } } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { // Ignore if no entity was passed if (!data.entity) return; PlaySound(data.name, data.entity); }; function isIdleUnit(ent, idleClass) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: Do something with garrisoned idle units return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass)); } GuiInterface.prototype.FindIdleUnits = function(player, data) { var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerEntities = rangeMan.GetEntitiesByPlayer(player).filter( function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); if (cmpUnitAI) return true; return false; }); var idleUnits = []; var noFilter = (data.prevUnit == undefined && data.excludeUnits == undefined); for (var j = 0; j < playerEntities.length; ++j) { var ent = playerEntities[j]; if (!isIdleUnit(ent, data.idleClass)) continue; if (noFilter || ((data.prevUnit == undefined || ent > data.prevUnit) && (data.excludeUnits == undefined || data.excludeUnits.indexOf(ent) == -1))) { idleUnits.push(ent); playerEntities.splice(j--, 1); } if (data.limit && idleUnits.length >= data.limit) break; } return idleUnits; } GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); } GuiInterface.prototype.GetTradingDetails = function(player, data) { var cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; var firstMarket = cmpEntityTrader.GetFirstMarket(); var secondMarket = cmpEntityTrader.GetSecondMarket(); var result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGain(); } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGain(), }; } else if (!firstMarket) { result = {"type": "set first"}; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; } else { // Else both markets are not null and target is different from them result = {"type": "set first"}; } return result; }; GuiInterface.prototype.CanAttack = function(player, data) { var cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); if (!cmpAttack) return false; return cmpAttack.CanAttack(data.target); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { var cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetLosRevealAll(-1); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); cmpPathfinder.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); cmpObstructionManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for each (var ent in data.entities) { var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var traders = cmpRangeManager.GetEntitiesByPlayer(player).filter( function(e) { return Engine.QueryInterface(e, IID_Trader); }); var landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; var shipTrader = { "total": 0, "trading": 0 }; for each (var ent in traders) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { var holder = cmpUnitAI.order.data.target; var cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player, tradingGoods) { var cmpPlayer = QueryPlayerIDInterface(player, IID_Player); return cmpPlayer.GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); } // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) var exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetExtendedEntityState": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "GetTechnologyData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNextNotification": 1, "GetTimeNotificationText": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "SetSelectionHighlight": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "DisplayRallyPoint": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 14587) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 14588) @@ -1,444 +1,460 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Serialize = function() { // The modifications cache will be affected by property reads from the GUI and other places so we shouldn't // serialize it. var ret = {}; for (var i in this) { if (this.hasOwnProperty(i)) ret[i] = this[i]; } ret.modificationCache = {}; return ret; }; TechnologyManager.prototype.Init = function () { var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager); this.allTechs = cmpTechTempMan.GetAllTechs(); this.researchedTechs = {}; // technologies which have been researched this.researchQueued = {}; // technologies which are queued for research this.researchStarted = {}; // technologies which are being researched currently (non-queued) // This stores the modifications to unit stats from researched technologies // Example data: {"ResourceGatherer/Rates/food.grain": [ // {"multiply": 1.15, "affects": ["Female", "Infantry Swordsman"]}, // {"add": 2} // ]} this.modifications = {}; this.modificationCache = {}; // Caches the values after technologies have been applied // e.g. { "Attack/Melee/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...} // where 5 and 7 are entity id's this.typeCounts = {}; // stores the number of entities of each type this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.autoResearchTech = {}; for (var key in this.allTechs) { if (this.allTechs[key].autoResearch || this.allTechs[key].top) this.autoResearchTech[key] = this.allTechs[key]; } this.UpdateAutoResearch(); }; TechnologyManager.prototype.OnUpdate = function () { this.UpdateAutoResearch(); } // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function () { for (var key in this.autoResearchTech) { if ((this.allTechs[key].autoResearch && this.CanResearch(key)) || (this.allTechs[key].top && (this.IsTechnologyResearched(this.allTechs[key].top) || this.IsTechnologyResearched(this.allTechs[key].bottom)))) { delete this.autoResearchTech[key]; this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } } TechnologyManager.prototype.GetTechnologyTemplate = function (tech) { return this.allTechs[tech]; }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function (templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); else return true; // If there is no required technology then this entity can be produced }; TechnologyManager.prototype.IsTechnologyResearched = function (tech) { return (this.researchedTechs[tech] !== undefined); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function (tech) { var template = this.GetTechnologyTemplate(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } // The technology which this technology supersedes is required if (template.supersedes && !this.IsTechnologyResearched(template.supersedes)) return false; if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; return this.CheckTechnologyRequirements(template.requirements); }; // Private function for checking a set of requirements is met TechnologyManager.prototype.CheckTechnologyRequirements = function (reqs) { // If there are no requirements then all requirements are met if (!reqs) return true; if (reqs.tech) { return this.IsTechnologyResearched(reqs.tech); } else if (reqs.all) { for (var i = 0; i < reqs.all.length; i++) { if (!this.CheckTechnologyRequirements(reqs.all[i])) return false; } return true; } else if (reqs.any) { for (var i = 0; i < reqs.any.length; i++) { if (this.CheckTechnologyRequirements(reqs.any[i])) return true; } return false; } else if (reqs.class) { if (reqs.numberOfTypes) { if (this.typeCountsByClass[reqs.class]) return (reqs.numberOfTypes <= Object.keys(this.typeCountsByClass[reqs.class]).length); else return false; } else if (reqs.number) { if (this.classCounts[reqs.class]) return (reqs.number <= this.classCounts[reqs.class]); else return false; } } else if (reqs.civ) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetCiv() == reqs.civ) return true; else return false; } // The technologies requirements are not a recognised format error("Bad requirements " + uneval(reqs)); return false; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function (msg) { // This automatically updates typeCounts, classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); this.typeCounts[template] = this.typeCounts[template] || 0; this.typeCounts[template] += 1; var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (var i in classes) { this.classCounts[classes[i]] = this.classCounts[classes[i]] || 0; this.classCounts[classes[i]] += 1; this.typeCountsByClass[classes[i]] = this.typeCountsByClass[classes[i]] || {}; this.typeCountsByClass[classes[i]][template] = this.typeCountsByClass[classes[i]][template] || 0; this.typeCountsByClass[classes[i]][template] += 1; } } // Newly created entity, check if any researched techs might apply // (only do this for new entities because even if an entity is converted or captured, // we want it to maintain whatever technologies previously applied) if (msg.from == -1) { var modifiedComponents = {}; for (var name in this.modifications) { // We only need to find one one tech per component for a match var modifications = this.modifications[name]; var component = name.split("/")[0]; for (var i in modifications) - if (!modifiedComponents[component] && DoesModificationApply(modifications[i], classes)) - modifiedComponents[component] = true; + if (DoesModificationApply(modifications[i], classes)) + { + if (!modifiedComponents[component]) + modifiedComponents[component] = []; + modifiedComponents[component].push(name); + } } // Send mesage(s) to the entity so it knows about researched techs for (var component in modifiedComponents) - Engine.PostMessage(msg.entity, MT_ValueModification, { "component": component }); + Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); this.typeCounts[template] -= 1; if (this.typeCounts[template] <= 0) delete this.typeCounts[template]; // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (var i in classes) { this.classCounts[classes[i]] -= 1; if (this.classCounts[classes[i]] <= 0) delete this.classCounts[classes[i]]; this.typeCountsByClass[classes[i]][template] -= 1; if (this.typeCountsByClass[classes[i]][template] <= 0) delete this.typeCountsByClass[classes[i]][template]; } } } this.clearModificationCache(msg.entity); } }; // Marks a technology as researched. Note that this does not verify that the requirements are met. TechnologyManager.prototype.ResearchTechnology = function (tech) { this.StoppedResearch(tech); // The tech is no longer being currently researched var template = this.GetTechnologyTemplate(tech); if (!template) { error("Tried to research invalid techonology: " + uneval(tech)); return; } var modifiedComponents = {}; this.researchedTechs[tech] = template; // store the modifications in an easy to access structure if (template.modifications) { var affects = []; if (template.affects && template.affects.length > 0) { for (var i in template.affects) { // Put the list of classes into an array for convenient access affects.push(template.affects[i].split(/\s+/)); } } else { affects.push([]); } // We add an item to this.modifications for every modification in the template.modifications array for (var i in template.modifications) { var modification = template.modifications[i]; if (!this.modifications[modification.value]) this.modifications[modification.value] = []; var modAffects = []; if (modification.affects) { for (var j in modification.affects) modAffects.push(modification.affects[j].split(/\s+/)); } var mod = {"affects": affects.concat(modAffects)}; // copy the modification data into our new data structure for (var j in modification) if (j !== "value" && j !== "affects") mod[j] = modification[j]; this.modifications[modification.value].push(mod); - modifiedComponents[modification.value.split("/")[0]] = true; + var component = modification.value.split("/")[0]; + if (!modifiedComponents[component]) + modifiedComponents[component] = []; + modifiedComponents[component].push(modification.value); this.modificationCache[modification.value] = {}; } } this.UpdateAutoResearch(); + var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); + if (!cmpPlayer || ! cmpPlayer.GetPlayerID()) + return; + var playerID = cmpPlayer.GetPlayerID(); + var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + var ents = cmpRangeManager.GetEntitiesByPlayer(playerID); // TODO: Handle technology broadcasting for autoresearch properly (some components might not be initialized currently) for (var component in modifiedComponents) - Engine.BroadcastMessage(MT_ValueModification, { "component": component }); + { + Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]}); + Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]}); + } }; // Clears the cached data for an entity from the modifications cache TechnologyManager.prototype.clearModificationCache = function(ent) { for (var valueName in this.modificationCache) delete this.modificationCache[valueName][ent]; }; // Caching layer in front of ApplyModificationsWorker // Note: be careful with the type of curValue, if it should be a numerical // value and is derived from template data, you must convert the string // from the template to a number using the + operator, before calling // this function! TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent) { if (!this.modificationCache[valueName]) this.modificationCache[valueName] = {}; if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue) { this.modificationCache[valueName][ent] = {"origValue": curValue}; var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var templateName = cmpTemplateManager.GetCurrentTemplateName(ent); // Ensure that preview entites have the same properties as the final building if (templateName.indexOf("preview|") != -1) templateName = templateName.slice(8); this.modificationCache[valueName][ent].newValue = GetTechModifiedProperty(this.modifications, cmpTemplateManager.GetTemplate(templateName), valueName, curValue); } return this.modificationCache[valueName][ent].newValue; }; // Alternative version of ApplyModifications, applies to templates instead of entities TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template) { return GetTechModifiedProperty(this.modifications, template, valueName, curValue); }; // Marks a technology as being queued for research TechnologyManager.prototype.QueuedResearch = function (tech, researcher) { this.researchQueued[tech] = researcher; }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function (tech) { this.researchStarted[tech] = true; }; // Marks a technology as not being currently researched TechnologyManager.prototype.StoppedResearch = function (tech) { delete this.researchQueued[tech]; delete this.researchStarted[tech]; }; // Checks whether a technology is set to be researched TechnologyManager.prototype.IsInProgress = function(tech) { if (this.researchQueued[tech]) return true; else return false; }; // Get all techs that are currently being researched TechnologyManager.prototype.GetTechsStarted = function() { return this.researchStarted; }; // Gets the entity currently researching a technology TechnologyManager.prototype.GetResearcher = function(tech) { if (this.researchQueued[tech]) return this.researchQueued[tech]; return undefined; }; // Get helper data for tech modifications TechnologyManager.prototype.GetTechModifications = function() { return this.modifications; }; // called by GUIInterface for PlayerData. AI use. TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; TechnologyManager.prototype.GetStartedResearch = function() { return this.researchStarted; }; TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager); Index: ps/trunk/source/simulation2/MessageTypes.h =================================================================== --- ps/trunk/source/simulation2/MessageTypes.h (revision 14587) +++ ps/trunk/source/simulation2/MessageTypes.h (revision 14588) @@ -1,415 +1,439 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MESSAGETYPES #define INCLUDED_MESSAGETYPES #include "simulation2/system/Components.h" #include "simulation2/system/Entity.h" #include "simulation2/system/Message.h" #include "simulation2/helpers/Player.h" #include "simulation2/helpers/Position.h" #include "simulation2/components/ICmpPathfinder.h" #define DEFAULT_MESSAGE_IMPL(name) \ virtual int GetType() const { return MT_##name; } \ virtual const char* GetScriptHandlerName() const { return "On" #name; } \ virtual const char* GetScriptGlobalHandlerName() const { return "OnGlobal" #name; } \ virtual jsval ToJSVal(ScriptInterface& scriptInterface) const; \ static CMessage* FromJSVal(ScriptInterface&, jsval val); class SceneCollector; class CFrustum; class CMessageTurnStart : public CMessage { public: DEFAULT_MESSAGE_IMPL(TurnStart) CMessageTurnStart() { } }; // The update process is split into a number of phases, in an attempt // to cope with dependencies between components. Each phase is implemented // as a separate message. Simulation2.cpp sends them in sequence. /** * Generic per-turn update message, for things that don't care much about ordering. */ class CMessageUpdate : public CMessage { public: DEFAULT_MESSAGE_IMPL(Update) CMessageUpdate(fixed turnLength) : turnLength(turnLength) { } fixed turnLength; }; /** * Update phase for formation controller movement (must happen before individual * units move to follow their formation). */ class CMessageUpdate_MotionFormation : public CMessage { public: DEFAULT_MESSAGE_IMPL(Update_MotionFormation) CMessageUpdate_MotionFormation(fixed turnLength) : turnLength(turnLength) { } fixed turnLength; }; /** * Update phase for non-formation-controller unit movement. */ class CMessageUpdate_MotionUnit : public CMessage { public: DEFAULT_MESSAGE_IMPL(Update_MotionUnit) CMessageUpdate_MotionUnit(fixed turnLength) : turnLength(turnLength) { } fixed turnLength; }; /** * Final update phase, after all other updates. */ class CMessageUpdate_Final : public CMessage { public: DEFAULT_MESSAGE_IMPL(Update_Final) CMessageUpdate_Final(fixed turnLength) : turnLength(turnLength) { } fixed turnLength; }; /** * Prepare for rendering a new frame (set up model positions etc). */ class CMessageInterpolate : public CMessage { public: DEFAULT_MESSAGE_IMPL(Interpolate) CMessageInterpolate(float deltaSimTime, float offset, float deltaRealTime) : deltaSimTime(deltaSimTime), offset(offset), deltaRealTime(deltaRealTime) { } /// Elapsed simulation time since previous interpolate, in seconds. This is similar to the elapsed real time, except /// it is scaled by the current simulation rate (and might indeed be zero). float deltaSimTime; /// Range [0, 1] (inclusive); fractional time of current frame between previous/next simulation turns. float offset; /// Elapsed real time since previous interpolate, in seconds. float deltaRealTime; }; /** * Add renderable objects to the scene collector. * Called after CMessageInterpolate. */ class CMessageRenderSubmit : public CMessage { public: DEFAULT_MESSAGE_IMPL(RenderSubmit) CMessageRenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) : collector(collector), frustum(frustum), culling(culling) { } SceneCollector& collector; const CFrustum& frustum; bool culling; }; /** * Handle progressive loading of resources. * A component that listens to this message must do the following: * - Increase *msg.total by the non-zero number of loading tasks this component can perform. * - If *msg.progressed == true, return and do nothing. * - If you've loaded everything, increase *msg.progress by the value you added to .total * - Otherwise do some loading, set *msg.progressed = true, and increase *msg.progress by a * value indicating how much progress you've made in total (0 <= p <= what you added to .total) * In some situations these messages will never be sent - components must ensure they * load all their data themselves before using it in that case. */ class CMessageProgressiveLoad : public CMessage { public: DEFAULT_MESSAGE_IMPL(ProgressiveLoad) CMessageProgressiveLoad(bool* progressed, int* total, int* progress) : progressed(progressed), total(total), progress(progress) { } bool* progressed; int* total; int* progress; }; /** * This is sent immediately after a new entity's components have all been created * and initialised. */ class CMessageCreate : public CMessage { public: DEFAULT_MESSAGE_IMPL(Create) CMessageCreate(entity_id_t entity) : entity(entity) { } entity_id_t entity; }; /** * This is sent immediately before a destroyed entity is flushed and really destroyed. * (That is, after CComponentManager::DestroyComponentsSoon and inside FlushDestroyedComponents). * The entity will still exist at the time this message is sent. * It's possible for this message to be sent multiple times for one entity, but all its components * will have been deleted after the first time. */ class CMessageDestroy : public CMessage { public: DEFAULT_MESSAGE_IMPL(Destroy) CMessageDestroy(entity_id_t entity) : entity(entity) { } entity_id_t entity; }; class CMessageOwnershipChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(OwnershipChanged) CMessageOwnershipChanged(entity_id_t entity, player_id_t from, player_id_t to) : entity(entity), from(from), to(to) { } entity_id_t entity; player_id_t from; player_id_t to; }; /** * Sent during TurnStart. * * If @c inWorld is false, then the other fields are invalid and meaningless. * Otherwise they represent the current position. */ class CMessagePositionChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(PositionChanged) CMessagePositionChanged(entity_id_t entity, bool inWorld, entity_pos_t x, entity_pos_t z, entity_angle_t a) : entity(entity), inWorld(inWorld), x(x), z(z), a(a) { } entity_id_t entity; bool inWorld; entity_pos_t x, z; entity_angle_t a; }; /** * Sent by CCmpUnitMotion during Update, whenever the motion status has changed * since the previous update. */ class CMessageMotionChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(MotionChanged) CMessageMotionChanged(bool starting, bool error) : starting(starting), error(error) { } bool starting; // whether this is a start or end of movement bool error; // whether we failed to start moving (couldn't find any path) }; /** * Sent when terrain (texture or elevation) has been changed. */ class CMessageTerrainChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(TerrainChanged) CMessageTerrainChanged(int32_t i0, int32_t j0, int32_t i1, int32_t j1) : i0(i0), j0(j0), i1(i1), j1(j1) { } int32_t i0, j0, i1, j1; // inclusive lower bound, exclusive upper bound, in tiles }; /** * Sent when territory assignments have changed. */ class CMessageTerritoriesChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(TerritoriesChanged) CMessageTerritoriesChanged() { } }; /** * Sent by CCmpRangeManager at most once per turn, when an active range query * has had matching units enter/leave the range since the last RangeUpdate. */ class CMessageRangeUpdate : public CMessage { public: DEFAULT_MESSAGE_IMPL(RangeUpdate) u32 tag; std::vector added; std::vector removed; // CCmpRangeManager wants to store a vector of messages and wants to // swap vectors instead of copying (to save on memory allocations), // so add some constructors for it: // don't init tag in empty ctor CMessageRangeUpdate() { } CMessageRangeUpdate(u32 tag) : tag(tag) { } CMessageRangeUpdate(u32 tag, const std::vector& added, const std::vector& removed) : tag(tag), added(added), removed(removed) { } CMessageRangeUpdate(const CMessageRangeUpdate& other) : CMessage(), tag(other.tag), added(other.added), removed(other.removed) { } CMessageRangeUpdate& operator=(const CMessageRangeUpdate& other) { tag = other.tag; added = other.added; removed = other.removed; return *this; } }; /** * Sent by CCmpPathfinder after async path requests. */ class CMessagePathResult : public CMessage { public: DEFAULT_MESSAGE_IMPL(PathResult) CMessagePathResult(u32 ticket, const ICmpPathfinder::Path& path) : ticket(ticket), path(path) { } u32 ticket; ICmpPathfinder::Path path; }; /** - * Sent by value modification manager when a value of a certain component is changed + * Sent by aura manager when a value of a certain entity's component is changed */ class CMessageValueModification : public CMessage { public: DEFAULT_MESSAGE_IMPL(ValueModification) - CMessageValueModification(std::wstring component) : - component(component) + CMessageValueModification(const std::vector& entities, std::wstring component, const std::vector& valueNames) : + entities(entities), + component(component), + valueNames(valueNames) { } + + std::vector entities; + std::wstring component; + std::vector valueNames; +}; +/** + * Sent by aura and tech managers when a value of a certain template's component is changed + */ +class CMessageTemplateModification : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(TemplateModification) + + CMessageTemplateModification(player_id_t player, std::wstring component, const std::vector& valueNames) : + player(player), + component(component), + valueNames(valueNames) + { + } + + player_id_t player; std::wstring component; + std::vector valueNames; }; /** * Sent by CCmpVision when an entity's vision range changes. */ class CMessageVisionRangeChanged : public CMessage { public: DEFAULT_MESSAGE_IMPL(VisionRangeChanged) CMessageVisionRangeChanged(entity_id_t entity, entity_pos_t oldRange, entity_pos_t newRange) : entity(entity), oldRange(oldRange), newRange(newRange) { } entity_id_t entity; entity_pos_t oldRange; entity_pos_t newRange; }; /** * Sent when an entity pings the minimap */ class CMessageMinimapPing : public CMessage { public: DEFAULT_MESSAGE_IMPL(MinimapPing) CMessageMinimapPing() { } }; #endif // INCLUDED_MESSAGETYPES Index: ps/trunk/source/simulation2/TypeList.h =================================================================== --- ps/trunk/source/simulation2/TypeList.h (revision 14587) +++ ps/trunk/source/simulation2/TypeList.h (revision 14588) @@ -1,170 +1,171 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ // MESSAGE: message types // INTERFACE: component interface types // COMPONENT: component types // Components intended only for use in test cases: // (The tests rely on the enum IDs, so don't change the order of these) INTERFACE(Test1) COMPONENT(Test1A) COMPONENT(Test1B) COMPONENT(Test1Scripted) INTERFACE(Test2) COMPONENT(Test2A) COMPONENT(Test2Scripted) // Message types: MESSAGE(TurnStart) MESSAGE(Update) MESSAGE(Update_MotionFormation) MESSAGE(Update_MotionUnit) MESSAGE(Update_Final) MESSAGE(Interpolate) // non-deterministic (use with caution) MESSAGE(RenderSubmit) // non-deterministic (use with caution) MESSAGE(ProgressiveLoad) // non-deterministic (use with caution) MESSAGE(Create) MESSAGE(Destroy) MESSAGE(OwnershipChanged) MESSAGE(PositionChanged) MESSAGE(MotionChanged) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(TerritoriesChanged) MESSAGE(PathResult) MESSAGE(ValueModification) +MESSAGE(TemplateModification) MESSAGE(VisionRangeChanged) MESSAGE(MinimapPing) // TemplateManager must come before all other (non-test) components, // so that it is the first to be (de)serialized INTERFACE(TemplateManager) COMPONENT(TemplateManager) // Special component for script component types with no native interface INTERFACE(UnknownScript) COMPONENT(UnknownScript) // In alphabetical order: INTERFACE(AIInterface) COMPONENT(AIInterfaceScripted) INTERFACE(AIManager) COMPONENT(AIManager) INTERFACE(CommandQueue) COMPONENT(CommandQueue) INTERFACE(Decay) COMPONENT(Decay) // Note: The VisualActor component relies on this component being initialized before itself, in order to support using // an entity's footprint shape for the selection boxes. This dependency is not strictly necessary, but it does avoid // some extra plumbing code to set up on-demand initialization. If you find yourself forced to break this dependency, // see VisualActor's Init method for a description of how you can avoid it. INTERFACE(Footprint) COMPONENT(Footprint) INTERFACE(GuiInterface) COMPONENT(GuiInterfaceScripted) INTERFACE(Identity) COMPONENT(IdentityScripted) INTERFACE(Minimap) COMPONENT(Minimap) INTERFACE(Motion) COMPONENT(MotionBall) COMPONENT(MotionScripted) INTERFACE(Obstruction) COMPONENT(Obstruction) INTERFACE(ObstructionManager) COMPONENT(ObstructionManager) INTERFACE(OverlayRenderer) COMPONENT(OverlayRenderer) INTERFACE(Ownership) COMPONENT(Ownership) INTERFACE(ParticleManager) COMPONENT(ParticleManager) INTERFACE(Pathfinder) COMPONENT(Pathfinder) INTERFACE(Player) COMPONENT(PlayerScripted) INTERFACE(PlayerManager) COMPONENT(PlayerManagerScripted) INTERFACE(Position) COMPONENT(Position) // must be before VisualActor INTERFACE(ProjectileManager) COMPONENT(ProjectileManager) INTERFACE(RallyPointRenderer) COMPONENT(RallyPointRenderer) INTERFACE(RangeManager) COMPONENT(RangeManager) INTERFACE(Selectable) COMPONENT(Selectable) INTERFACE(Settlement) COMPONENT(SettlementScripted) INTERFACE(SoundManager) COMPONENT(SoundManager) INTERFACE(ValueModificationManager) COMPONENT(ValueModificationManagerScripted) INTERFACE(TechnologyTemplateManager) COMPONENT(TechnologyTemplateManagerScripted) INTERFACE(Terrain) COMPONENT(Terrain) INTERFACE(TerritoryInfluence) COMPONENT(TerritoryInfluence) INTERFACE(TerritoryManager) COMPONENT(TerritoryManager) INTERFACE(UnitMotion) COMPONENT(UnitMotion) // must be after Obstruction COMPONENT(UnitMotionScripted) INTERFACE(Vision) COMPONENT(Vision) // Note: this component relies on the Footprint component being initialized before itself. See the comments above for // the Footprint component to find out why. INTERFACE(Visual) COMPONENT(VisualActor) // must be after Ownership (dependency in Deserialize) and Vision (dependency in Init) INTERFACE(WaterManager) COMPONENT(WaterManager) Index: ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp (revision 14587) +++ ps/trunk/source/simulation2/components/ICmpTemplateManager.cpp (revision 14588) @@ -1,29 +1,30 @@ /* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpTemplateManager.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(TemplateManager) DEFINE_INTERFACE_METHOD_1("GetTemplate", const CParamNode*, ICmpTemplateManager, GetTemplate, std::string) +DEFINE_INTERFACE_METHOD_1("GetTemplateWithoutValidation", const CParamNode*, ICmpTemplateManager, GetTemplateWithoutValidation, std::string) DEFINE_INTERFACE_METHOD_1("GetCurrentTemplateName", std::string, ICmpTemplateManager, GetCurrentTemplateName, entity_id_t) DEFINE_INTERFACE_METHOD_1("FindAllTemplates", std::vector, ICmpTemplateManager, FindAllTemplates, bool) DEFINE_INTERFACE_METHOD_1("GetEntitiesUsingTemplate", std::vector, ICmpTemplateManager, GetEntitiesUsingTemplate, std::string) END_INTERFACE_WRAPPER(TemplateManager) Index: ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp (revision 14587) +++ ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp (revision 14588) @@ -1,366 +1,390 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/MessageTypes.h" #include "js/jsapi.h" #define TOJSVAL_SETUP() \ JSObject* obj = JS_NewObject(scriptInterface.GetContext(), NULL, NULL, NULL); \ if (! obj) \ return JSVAL_VOID; #define SET_MSG_PROPERTY(name) \ do { \ jsval prop = ScriptInterface::ToJSVal(scriptInterface.GetContext(), this->name); \ if (! JS_SetProperty(scriptInterface.GetContext(), obj, #name, &prop)) \ return JSVAL_VOID; \ } while (0); #define FROMJSVAL_SETUP() \ if (! JSVAL_IS_OBJECT(val)) \ return NULL; \ JSObject* obj = JSVAL_TO_OBJECT(val); \ jsval prop; #define GET_MSG_PROPERTY(type, name) \ if (! JS_GetProperty(scriptInterface.GetContext(), obj, #name, &prop)) \ return NULL; \ type name; \ if (! ScriptInterface::FromJSVal(scriptInterface.GetContext(), prop, name)) \ return NULL; jsval CMessage::ToJSValCached(ScriptInterface& scriptInterface) const { if (m_Cached.uninitialised()) m_Cached = CScriptValRooted(scriptInterface.GetContext(), ToJSVal(scriptInterface)); return m_Cached.get(); } //////////////////////////////// jsval CMessageTurnStart::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageTurnStart::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { return new CMessageTurnStart(); } //////////////////////////////// #define MESSAGE_1(name, t0, a0) \ jsval CMessage##name::ToJSVal(ScriptInterface& scriptInterface) const \ { \ TOJSVAL_SETUP(); \ SET_MSG_PROPERTY(a0); \ return OBJECT_TO_JSVAL(obj); \ } \ CMessage* CMessage##name::FromJSVal(ScriptInterface& scriptInterface, jsval val) \ { \ FROMJSVAL_SETUP(); \ GET_MSG_PROPERTY(t0, a0); \ return new CMessage##name(a0); \ } MESSAGE_1(Update, fixed, turnLength) MESSAGE_1(Update_MotionFormation, fixed, turnLength) MESSAGE_1(Update_MotionUnit, fixed, turnLength) MESSAGE_1(Update_Final, fixed, turnLength) //////////////////////////////// jsval CMessageInterpolate::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(deltaSimTime); SET_MSG_PROPERTY(offset); SET_MSG_PROPERTY(deltaRealTime); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageInterpolate::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(float, deltaSimTime); GET_MSG_PROPERTY(float, offset); GET_MSG_PROPERTY(float, deltaRealTime); return new CMessageInterpolate(deltaSimTime, offset, deltaRealTime); } //////////////////////////////// jsval CMessageRenderSubmit::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const { LOGWARNING(L"CMessageRenderSubmit::ToJSVal not implemented"); return JSVAL_VOID; } CMessage* CMessageRenderSubmit::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { LOGWARNING(L"CMessageRenderSubmit::FromJSVal not implemented"); return NULL; } //////////////////////////////// jsval CMessageProgressiveLoad::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const { LOGWARNING(L"CMessageProgressiveLoad::ToJSVal not implemented"); return JSVAL_VOID; } CMessage* CMessageProgressiveLoad::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { LOGWARNING(L"CMessageProgressiveLoad::FromJSVal not implemented"); return NULL; } //////////////////////////////// jsval CMessageCreate::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(entity); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageCreate::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(entity_id_t, entity); return new CMessageCreate(entity); } //////////////////////////////// jsval CMessageDestroy::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(entity); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageDestroy::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(entity_id_t, entity); return new CMessageDestroy(entity); } //////////////////////////////// jsval CMessageOwnershipChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(entity); SET_MSG_PROPERTY(from); SET_MSG_PROPERTY(to); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageOwnershipChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(entity_id_t, entity); GET_MSG_PROPERTY(player_id_t, from); GET_MSG_PROPERTY(player_id_t, to); return new CMessageOwnershipChanged(entity, from, to); } //////////////////////////////// jsval CMessagePositionChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(entity); SET_MSG_PROPERTY(inWorld); SET_MSG_PROPERTY(x); SET_MSG_PROPERTY(z); SET_MSG_PROPERTY(a); return OBJECT_TO_JSVAL(obj); } CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(entity_id_t, entity); GET_MSG_PROPERTY(bool, inWorld); GET_MSG_PROPERTY(entity_pos_t, x); GET_MSG_PROPERTY(entity_pos_t, z); GET_MSG_PROPERTY(entity_angle_t, a); return new CMessagePositionChanged(entity, inWorld, x, z, a); } //////////////////////////////// jsval CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(starting); SET_MSG_PROPERTY(error); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(bool, starting); GET_MSG_PROPERTY(bool, error); return new CMessageMotionChanged(starting, error); } //////////////////////////////// jsval CMessageTerrainChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(i0); SET_MSG_PROPERTY(j0); SET_MSG_PROPERTY(i1); SET_MSG_PROPERTY(j1); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageTerrainChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(int32_t, i0); GET_MSG_PROPERTY(int32_t, j0); GET_MSG_PROPERTY(int32_t, i1); GET_MSG_PROPERTY(int32_t, j1); return new CMessageTerrainChanged(i0, i1, j0, j1); } //////////////////////////////// jsval CMessageTerritoriesChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageTerritoriesChanged::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { return new CMessageTerritoriesChanged(); } //////////////////////////////// jsval CMessageRangeUpdate::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(tag); SET_MSG_PROPERTY(added); SET_MSG_PROPERTY(removed); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageRangeUpdate::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { LOGWARNING(L"CMessageRangeUpdate::FromJSVal not implemented"); return NULL; } //////////////////////////////// jsval CMessagePathResult::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const { LOGWARNING(L"CMessagePathResult::ToJSVal not implemented"); return JSVAL_VOID; } CMessage* CMessagePathResult::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { LOGWARNING(L"CMessagePathResult::FromJSVal not implemented"); return NULL; } //////////////////////////////// jsval CMessageValueModification::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); + SET_MSG_PROPERTY(entities); SET_MSG_PROPERTY(component); + SET_MSG_PROPERTY(valueNames); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageValueModification::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(std::vector, entities); GET_MSG_PROPERTY(std::wstring, component); - return new CMessageValueModification(component); + GET_MSG_PROPERTY(std::vector, valueNames); + return new CMessageValueModification(entities, component, valueNames); +} + +//////////////////////////////// + +jsval CMessageTemplateModification::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(player); + SET_MSG_PROPERTY(component); + SET_MSG_PROPERTY(valueNames); + return OBJECT_TO_JSVAL(obj); +} + +CMessage* CMessageTemplateModification::FromJSVal(ScriptInterface& scriptInterface, jsval val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(player_id_t, player); + GET_MSG_PROPERTY(std::wstring, component); + GET_MSG_PROPERTY(std::vector, valueNames); + return new CMessageTemplateModification(player, component, valueNames); } //////////////////////////////// jsval CMessageVisionRangeChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); SET_MSG_PROPERTY(entity); SET_MSG_PROPERTY(oldRange); SET_MSG_PROPERTY(newRange); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageVisionRangeChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val) { FROMJSVAL_SETUP(); GET_MSG_PROPERTY(entity_id_t, entity); GET_MSG_PROPERTY(entity_pos_t, oldRange); GET_MSG_PROPERTY(entity_pos_t, newRange); return new CMessageVisionRangeChanged(entity, oldRange, newRange); } //////////////////////////////// jsval CMessageMinimapPing::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP(); return OBJECT_TO_JSVAL(obj); } CMessage* CMessageMinimapPing::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val)) { return new CMessageMinimapPing(); } //////////////////////////////////////////////////////////////// CMessage* CMessageFromJSVal(int mtid, ScriptInterface& scriptingInterface, jsval val) { switch (mtid) { #define MESSAGE(name) case MT_##name: return CMessage##name::FromJSVal(scriptingInterface, val); #define INTERFACE(name) #define COMPONENT(name) #include "simulation2/TypeList.h" #undef COMPONENT #undef INTERFACE #undef MESSAGE } return NULL; }