Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 13822) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 13823) @@ -1,332 +1,339 @@ //Number of rounds of firing per 2 seconds const roundCount = 10; const attackType = "Ranged"; function BuildingAI() {} BuildingAI.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Initialize BuildingAI Component */ BuildingAI.prototype.Init = function() { if (this.GetDefaultArrowCount() > 0 || this.GetGarrisonArrowMultiplier() > 0) { this.currentRound = 0; //Arrows left to fire this.arrowsLeft = 0; this.targetUnits = []; } }; BuildingAI.prototype.OnOwnershipChanged = function(msg) { // Remove current targets, to prevent them from being added twice this.targetUnits = []; if (msg.to != -1) this.SetupRangeQuery(msg.to); // Non-Gaia buildings should attack certain Gaia units. if (msg.to != 0 || this.gaiaUnitsQuery) this.SetupGaiaRangeQuery(msg.to); }; BuildingAI.prototype.OnDiplomacyChanged = function(msg) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && cmpOwnership.GetOwner() == msg.player) { // Remove maybe now allied/neutral units this.targetUnits = []; this.SetupRangeQuery(msg.player); } }; /** * Cleanup on destroy */ BuildingAI.prototype.OnDestroy = function() { if (this.timer) { var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } // Clean up range queries var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (this.enemyUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); if (this.gaiaUnitsQuery) cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); }; /** * Setup the Range Query to detect units coming in & out of range */ BuildingAI.prototype.SetupRangeQuery = function(owner) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; if (this.enemyUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery); this.enemyUnitsQuery = undefined; } var players = []; var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player); var numPlayers = cmpPlayerManager.GetNumPlayers(); for (var i = 1; i < numPlayers; ++i) { // Exclude gaia, allies, and self // TODO: How to handle neutral players - Special query to attack military only? if (cmpPlayer.IsEnemy(i)) players.push(i); } var range = cmpAttack.GetRange(attackType); this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery); }; // Set up a range query for Gaia units within LOS range which can be attacked. // This should be called whenever our ownership changes. BuildingAI.prototype.SetupGaiaRangeQuery = function() { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); var owner = cmpOwnership.GetOwner(); var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; if (this.gaiaUnitsQuery) { cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery); this.gaiaUnitsQuery = undefined; } if (owner == -1) return; var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player); if (!cmpPlayer.IsEnemy(0)) return; var range = cmpAttack.GetRange(attackType); // This query is only interested in Gaia entities that can attack. this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(this.entity, range.min, range.max, range.elevationBonus, [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal")); cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery); }; /** * Called when units enter or leave range */ BuildingAI.prototype.OnRangeUpdate = function(msg) { var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; if (msg.tag == this.gaiaUnitsQuery) { const filter = function(e) { var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal())); }; if (msg.added.length) msg.added = msg.added.filter(filter); // Removed entities may not have cmpUnitAI. for (var i = 0; i < msg.removed.length; ++i) if (this.targetUnits.indexOf(msg.removed[i]) == -1) msg.removed.splice(i--, 1); } else if (msg.tag != this.enemyUnitsQuery) return; - const restrictedClasses = cmpAttack.GetRestrictedClasses(attackType); - if (msg.added.length > 0) { for each (var entity in msg.added) { - var cmpIdentity = Engine.QueryInterface(entity, IID_Identity); - var targetClasses = cmpIdentity.GetClassesList(); - - if (!targetClasses.some(function(c){return restrictedClasses.indexOf(c) > -1;})) + if (cmpAttack.CanAttack(entity)) this.targetUnits.push(entity); } } if (msg.removed.length > 0) { for each (var entity in msg.removed) { var index = this.targetUnits.indexOf(entity); if (index > -1) this.targetUnits.splice(index, 1); } } if (!this.targetUnits.length || this.timer) return; // units entered the range, prepare to shoot var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); var attackTimers = cmpAttack.GetTimers(attackType); this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows", attackTimers.prepare, attackTimers.repeat / roundCount, null); }; BuildingAI.prototype.GetDefaultArrowCount = function() { var arrowCount = +this.template.DefaultArrowCount; return ApplyTechModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity); }; BuildingAI.prototype.GetGarrisonArrowMultiplier = function() { var arrowMult = +this.template.GarrisonArrowMultiplier; return ApplyTechModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity); }; BuildingAI.prototype.GetGarrisonArrowClasses = function() { var string = this.template.GarrisonArrowClasses; if (string) return string.split(/\s+/); return []; }; /** * Returns the number of arrows which needs to be fired. * DefaultArrowCount + Garrisoned Archers(ie., any unit capable * of shooting arrows from inside buildings) */ BuildingAI.prototype.GetArrowCount = function() { var count = this.GetDefaultArrowCount(); var cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { count += Math.round(cmpGarrisonHolder.GetGarrisonedArcherCount(this.GetGarrisonArrowClasses()) * this.GetGarrisonArrowMultiplier()); } return count; }; /** * Fires arrows. Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range */ BuildingAI.prototype.FireArrows = function() { if (!this.targetUnits.length) { if (this.timer) { // stop the timer var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); this.timer = undefined; } return; } var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return; var arrowsToFire = 0; if (this.currentRound > (roundCount - 1)) { //Reached end of rounds. Reset count this.currentRound = 0; } if (this.currentRound == 0) { //First round. Calculate arrows to fire this.arrowsLeft = this.GetArrowCount(); } if (this.currentRound == (roundCount - 1)) { //Last round. Need to fire all left-over arrows arrowsToFire = this.arrowsLeft; } else { //Fire N arrows, 0 <= N <= Number of arrows left arrowsToFire = Math.min( Math.round(2*Math.random() * this.GetArrowCount()/roundCount), this.arrowsLeft ); } - var clonedTargets = this.targetUnits.slice(); + if (arrowsToFire <= 0) + { + this.currentRound++; + return; + } + var targets = new WeightedList(); + for (var i = 0; i < this.targetUnits.length; i++) + { + var target = this.targetUnits[i], + weight = (cmpAttack.GetPreference(target) || 0) + 1 + targets.add(target, weight); + } for (var i = 0;i < arrowsToFire;i++) { - var target = clonedTargets[Math.floor(Math.random() * this.targetUnits.length)]; - if (target && this.CheckTargetVisible(target)) + var selectedIndex = targets.randomIndex(), + selectedTarget = targets.itemAt(selectedIndex); + if (selectedTarget && this.CheckTargetVisible(selectedTarget)) { - cmpAttack.PerformAttack(attackType, target); + cmpAttack.PerformAttack(attackType, selectedTarget); PlaySound("attack", this.entity); } else { - clonedTargets.splice(clonedTargets.indexOf(target),1); + targets.removeAt(selectedIndex); i--; // one extra arrow left to fire - if(clonedTargets.length < 1) + if(targets.length() < 1) { this.arrowsLeft += arrowsToFire; // no targets found in this round, save arrows and go to next round break; } } } this.arrowsLeft -= arrowsToFire; this.currentRound++; }; /** * Returns true if the target entity is visible through the FoW/SoD. */ BuildingAI.prototype.CheckTargetVisible = function(target) { var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) return false; var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden") return false; // Either visible directly, or visible in fog return true; }; Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/WeightedList.js (revision 13823) @@ -0,0 +1,47 @@ +var WeightedList = function() +{ + this.elements = [ ]; + this.totalWeight = 0; +}; + +WeightedList.prototype.length = function() +{ + return this.elements.length; +}; + +WeightedList.prototype.push = function(item, weight) +{ + if (weight === undefined) + weight = 1; + this.totalWeight += weight; + this.elements.push({ "item": item, "weight": weight }); +}; + +WeightedList.prototype.removeAt = function(index) +{ + var element = this.elements.splice(index, 1)[0]; + if (element) + this.totalWeight -= element.weight; +}; + +WeightedList.prototype.itemAt = function(index) +{ + var element = this.elements[index]; + return element ? element.item : null; +}; + +WeightedList.prototype.randomIndex = function() { + var element, + targetWeight = Math.random() * this.totalWeight, + cumulativeWeight = 0; + for (var index = 0; index < this.elements.length; index++) + { + element = this.elements[index]; + cumulativeWeight += element.weight; + if (cumulativeWeight >= targetWeight) + return index; + } + return -1; +}; + +Engine.RegisterGlobal("WeightedList", WeightedList);