Index: ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js +++ ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js @@ -7,9 +7,12 @@ AlertRaiser.prototype.Init = function() { this.level = 0; - this.garrisonedUnits = []; - this.prodBuildings = []; - this.walkingUnits = []; + + // Range at which units will search for garrison holders + this.searchRange = 100; + + // Extra allowance for units to garrison in buildings outside the alert range + this.bufferRange = 50; }; AlertRaiser.prototype.GetLevel = function() @@ -32,23 +35,6 @@ PlaySound("alert" + this.level, this.entity); }; -/** - * Used when units are spawned and need to follow alert orders. - * @param {number[]} units - Entity IDs of spawned units. - */ -AlertRaiser.prototype.UpdateUnits = function(units) -{ - for (let unit of units) - { - let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); - if (!cmpUnitAI || !cmpUnitAI.ReactsToAlert(this.level)) - continue; - - cmpUnitAI.ReplaceOrder("Alert", { "raiser": this.entity, "force": true }); - this.walkingUnits.push(unit); - } -}; - AlertRaiser.prototype.IncreaseAlertLevel = function() { if (!this.CanIncreaseLevel()) @@ -58,97 +44,106 @@ this.SoundAlert(); // Find buildings/units owned by this unit's player - let players = []; let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (cmpOwnership) - players = [cmpOwnership.GetOwner()]; + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) + return false; - // Select production buildings to put "under alert", including the raiser itself if possible + let players = [cmpOwnership.GetOwner()]; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let buildings = cmpRangeManager.ExecuteQuery(this.entity, 0, this.template.Range, players, IID_ProductionQueue); - if (Engine.QueryInterface(this.entity, IID_ProductionQueue)) - buildings.push(this.entity); - - for (let building of buildings) - { - let cmpProductionQueue = Engine.QueryInterface(building, IID_ProductionQueue); - cmpProductionQueue.PutUnderAlert(this.entity); - this.prodBuildings.push(building); - } // Select units to put under alert, according to their reaction to this level - let units = cmpRangeManager.ExecuteQuery(this.entity, 0, this.template.Range, players, IID_UnitAI).filter(ent => { - let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); - return !cmpUnitAI.IsUnderAlert() && cmpUnitAI.ReactsToAlert(this.level); - }); + let units = cmpRangeManager.ExecuteQuery(this.entity, 0, this.template.Range, players, IID_UnitAI).filter(ent => + Engine.QueryInterface(ent, IID_UnitAI).ReactsToAlert(this.level) + ); + // Store the number of available garrison spots, so that units don't try to garrison in buildings that will be full + let reserved = new Map(); for (let unit of units) { + let holder = cmpRangeManager.ExecuteQuery(unit, 0, this.searchRange, players, IID_GarrisonHolder).find(ent => { + // Ignore moving garrison holders + if (Engine.QueryInterface(ent, IID_UnitAI)) + return false; + + // Ensure that the garrison holder is within range of the alert raiser + if (DistanceBetweenEntities(this.entity, ent) > +this.template.Range + this.bufferRange) + return false; + + let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); + if (!reserved.has(ent)) + reserved.set(ent, cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.GetGarrisonedEntitiesCount()); + + return cmpGarrisonHolder.IsAllowedToGarrison(unit) && reserved.get(ent); + }); + let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); - cmpUnitAI.ReplaceOrder("Alert", { "raiser": this.entity, "force": true }); - this.walkingUnits.push(unit); + if (holder) + { + reserved.set(holder, reserved.get(holder) - 1); + cmpUnitAI.ReplaceOrder("Garrison", { "target": holder, "force": true }); + } + else + // If no available spots, move to the alert raiser + cmpUnitAI.ReplaceOrder("WalkToTarget", { "target": this.entity, "force": true }); } - return true; }; -AlertRaiser.prototype.OnUnitGarrisonedAfterAlert = function(msg) -{ - this.garrisonedUnits.push({ "holder": msg.holder, "unit": msg.unit }); - - let index = this.walkingUnits.indexOf(msg.unit); - if (index != -1) - this.walkingUnits.splice(index, 1); -}; - AlertRaiser.prototype.EndOfAlert = function() { - this.level = 0; this.SoundAlert(); - // First, handle units not yet garrisoned - for (let unit of this.walkingUnits) - { - let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); - if (!cmpUnitAI) - continue; + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) + return false; - cmpUnitAI.ResetAlert(); + let players = [cmpOwnership.GetOwner()]; + + // Units that are not garrisoned should stop and go back to work + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let units = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.Range + this.bufferRange, players, IID_UnitAI).filter(ent => + Engine.QueryInterface(ent, IID_UnitAI).ReactsToAlert(this.level) + ); - if (cmpUnitAI.HasWorkOrders()) + for (let unit of units) + { + let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); + if (cmpUnitAI.HasWorkOrders() && (cmpUnitAI.HasGarrisonOrder() || cmpUnitAI.IsIdle())) cmpUnitAI.BackToWork(); - else + else if (cmpUnitAI.HasGarrisonOrder()) + // Stop rather than continue to try to garrison cmpUnitAI.ReplaceOrder("Stop", undefined); } - this.walkingUnits = []; - // Then, eject garrisoned units - for (let slot of this.garrisonedUnits) + // Units that are garrisoned should ungarrison and go back to work + let holders = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.Range + this.bufferRange, players, IID_GarrisonHolder); + if (Engine.QueryInterface(this.entity, IID_GarrisonHolder)) + holders.push(this.entity); + + for (let holder of holders) { - let cmpGarrisonHolder = Engine.QueryInterface(slot.holder, IID_GarrisonHolder); - let cmpUnitAI = Engine.QueryInterface(slot.unit, IID_UnitAI); - if (!cmpUnitAI) + if (Engine.QueryInterface(holder, IID_UnitAI)) continue; - // If the garrison building was destroyed, the unit is already ejected - if (!cmpGarrisonHolder || cmpGarrisonHolder.PerformEject([slot.unit], true)) - { - cmpUnitAI.ResetAlert(); - if (cmpUnitAI.HasWorkOrders()) - cmpUnitAI.BackToWork(); - } + let cmpGarrisonHolder = Engine.QueryInterface(holder, IID_GarrisonHolder); + let units = cmpGarrisonHolder.GetEntities().filter(ent => + Engine.QueryInterface(ent, IID_UnitAI).ReactsToAlert(this.level) && + Engine.QueryInterface(ent, IID_Ownership).GetOwner() == players[0] + ); + + for (let unit of units) + if (cmpGarrisonHolder.PerformEject([unit], false)) + { + let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); + if (cmpUnitAI.HasWorkOrders()) + cmpUnitAI.BackToWork(); + else + // Stop rather than walk to the rally point + cmpUnitAI.ReplaceOrder("Stop", undefined); + } } - this.garrisonedUnits = []; - - // Finally, reset production buildings state - for (let building of this.prodBuildings) - { - let cmpProductionQueue = Engine.QueryInterface(building, IID_ProductionQueue); - if (cmpProductionQueue) - cmpProductionQueue.ResetAlert(); - } - this.prodBuildings = []; + this.level = 0; return true; }; Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -249,14 +249,6 @@ cmpAura.ApplyGarrisonBonus(this.entity); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); - - let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); - if (cmpUnitAI && cmpUnitAI.IsUnderAlert()) - Engine.PostMessage(cmpUnitAI.GetAlertRaiser(), MT_UnitGarrisonedAfterAlert, { - "holder": this.entity, - "unit": entity - }); - return true; }; Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -68,18 +68,6 @@ this.entityCache = []; this.spawnNotified = false; - - this.alertRaiser = undefined; -}; - -ProductionQueue.prototype.PutUnderAlert = function(raiser) -{ - this.alertRaiser = raiser; -}; - -ProductionQueue.prototype.ResetAlert = function() -{ - this.alertRaiser = undefined; }; /* @@ -550,8 +538,6 @@ // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); - - this.ResetAlert(); }; ProductionQueue.prototype.OnCivChanged = function() @@ -681,21 +667,12 @@ } if (createdEnts.length > 0) - { Engine.PostMessage(this.entity, MT_TrainingFinished, { "entities": createdEnts, "owner": cmpOwnership.GetOwner(), "metadata": metadata, }); - if (this.alertRaiser && spawnedEnts.length > 0) - { - var cmpAlertRaiser = Engine.QueryInterface(this.alertRaiser, IID_AlertRaiser); - if (cmpAlertRaiser) - cmpAlertRaiser.UpdateUnits(spawnedEnts); - } - } - return createdEnts.length; }; Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js +++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js @@ -746,22 +746,6 @@ this.isGarrisoned = false; }, - "Order.Alert": function(msg) { - this.alertRaiser = this.order.data.raiser; - - // Find a target to garrison into, if we don't already have one - if (!this.alertGarrisoningTarget) - this.alertGarrisoningTarget = this.FindNearbyGarrisonHolder(); - - if (this.alertGarrisoningTarget) - this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); - else - { - this.StopMoving(); - this.FinishOrder(); - } - }, - "Order.Cheering": function(msg) { this.SetNextState("INDIVIDUAL.CHEERING"); }, @@ -2898,44 +2882,18 @@ }, "MoveCompleted": function() { - if (this.IsUnderAlert() && this.alertGarrisoningTarget) - { - // check that we can garrison in the building we're supposed to garrison in - var cmpGarrisonHolder = Engine.QueryInterface(this.alertGarrisoningTarget, IID_GarrisonHolder); - if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) - { - // Try to find another nearby building - var nearby = this.FindNearbyGarrisonHolder(); - if (nearby) - { - this.alertGarrisoningTarget = nearby; - this.ReplaceOrder("Garrison", {"target": this.alertGarrisoningTarget}); - } - else - this.FinishOrder(); - } - else - this.SetNextState("GARRISONED"); - } - else - this.SetNextState("GARRISONED"); + this.SetNextState("GARRISONED"); }, }, "GARRISONED": { "enter": function() { - // Target is not handled the same way with Alert and direct garrisoning if (this.order.data.target) var target = this.order.data.target; else { - if (!this.alertGarrisoningTarget) - { - // We've been unable to find a target nearby, so give up - this.FinishOrder(); - return true; - } - var target = this.alertGarrisoningTarget; + this.FinishOrder(); + return true; } // Check that we can garrison here @@ -3281,10 +3239,6 @@ this.isGuardOf = undefined; - // "Town Bell" behaviour - this.alertRaiser = undefined; - this.alertGarrisoningTarget = undefined; - // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; this.lastHealed = undefined; @@ -3305,22 +3259,6 @@ return this.template.AlertReactiveLevel <= level; }; -UnitAI.prototype.IsUnderAlert = function() -{ - return this.alertRaiser != undefined; -}; - -UnitAI.prototype.ResetAlert = function() -{ - this.alertGarrisoningTarget = undefined; - this.alertRaiser = undefined; -}; - -UnitAI.prototype.GetAlertRaiser = function() -{ - return this.alertRaiser; -}; - UnitAI.prototype.IsFormationController = function() { return (this.template.FormationController == "true"); @@ -3389,6 +3327,11 @@ return INVALID_ENTITY; }; +UnitAI.prototype.HasGarrisonOrder = function() +{ + return this.orderQueue.length && this.orderQueue[0].type == "Garrison"; +}; + UnitAI.prototype.IsFleeing = function() { var state = this.GetCurrentState().split(".").pop(); @@ -3843,10 +3786,6 @@ UnitAI.prototype.UpdateWorkOrders = function(type) { - // Under alert, remembered work orders won't be forgotten - if (this.IsUnderAlert()) - return; - var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"; // If we are being re-affected to a work order, forget the previous ones @@ -4222,34 +4161,6 @@ }; /** - * Returns the entity ID of the nearest building in which the unit can garrison, - * or undefined if none can be found close enough. - */ -UnitAI.prototype.FindNearbyGarrisonHolder = function() -{ - var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER) - return undefined; - - // Find buildings owned by this unit's player - var players = [cmpOwnership.GetOwner()]; - - var range = 128; // TODO: what's a sensible number? - - var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_GarrisonHolder); - - return nearby.find(ent => { - // We only want to garrison in buildings, not in moving units like ships,... - if (Engine.QueryInterface(ent, IID_UnitAI)) - return false; - - var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); - return cmpGarrisonHolder.IsAllowedToGarrison(this.entity) && !cmpGarrisonHolder.IsFull(); - }); -}; - -/** * Play a sound appropriate to the current entity. */ UnitAI.prototype.PlaySound = function(name) Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/GarrisonHolder.js @@ -5,9 +5,3 @@ * sent from the GarrisonHolder component to the current entity whenever the garrisoned units change. */ Engine.RegisterMessageType("GarrisonedUnitsChanged"); - -/** - * Message of the form { "holder": number, "unit" : number } - * sent to the AlertRaiser which ordered the specified unit to garrison. - */ -Engine.RegisterMessageType("UnitGarrisonedAfterAlert"); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AlertRaiser.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AlertRaiser.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_AlertRaiser.js @@ -1,68 +1,73 @@ +Engine.LoadHelperScript("Entity.js"); Engine.LoadComponentScript("interfaces/AlertRaiser.js") -Engine.LoadComponentScript("interfaces/ProductionQueue.js"); -Engine.LoadComponentScript("interfaces/Sound.js"); +Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("AlertRaiser.js"); -const alertRaiserId = 5; -const unitsIds = [10, 11, 12]; -const buildingsIds = [13, 14, 15]; +const alertRaiserID = 5; +const unitIDs = [10, 11, 12]; +const buildingIDs = [13, 14, 15]; -let cmpAlertRaiser = ConstructComponent(alertRaiserId, "AlertRaiser", { +let cmpAlertRaiser = ConstructComponent(alertRaiserID, "AlertRaiser", { "MaximumLevel": 0, "Range": 50 }); Engine.RegisterGlobal("PlaySound", (name, source) => { TS_ASSERT_EQUALS(name, "alert" + cmpAlertRaiser.GetLevel()); - TS_ASSERT_EQUALS(source, alertRaiserId); + TS_ASSERT_EQUALS(source, alertRaiserID); +}); + +AddMock(alertRaiserID, IID_Ownership, { + "GetOwner": () => 1 }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { - "ExecuteQuery": (ent, value, range, players, iid) => iid === IID_UnitAI ? unitsIds : buildingsIds + "ExecuteQuery": (ent, value, range, players, iid) => iid === IID_UnitAI ? unitIDs : buildingIDs }); -unitsIds.forEach((unitId) => { - AddMock(unitId, IID_UnitAI, { +for (let unitID of unitIDs) + AddMock(unitID, IID_UnitAI, { "ReactsToAlert": (alertLevel) => alertLevel >= 2, "ReplaceOrder": () => {}, - "IsUnderAlert": () => {}, "HasWorkOrders": () => true, - "BackToWork": () => {}, - "ResetAlert": () => {} + "HasGarrisonOrder": () => true, + "BackToWork": () => {} }); -}); -buildingsIds.forEach((buildingId) => { - AddMock(buildingId, IID_ProductionQueue, { - "PutUnderAlert": (alertRaiserId) => {}, - "ResetAlert": () => {} +for (let buildingID of buildingIDs) + AddMock(buildingID, IID_GarrisonHolder, { + "GetCapacity": () => 10, + "GetGarrisonedEntitiesCount": () => 0, + "IsAllowedToGarrison": () => true, + "GetEntities": () => [] }); -}); TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 0); TS_ASSERT_EQUALS(cmpAlertRaiser.HasRaisedAlert(), false); TS_ASSERT_EQUALS(cmpAlertRaiser.CanIncreaseLevel(), false); TS_ASSERT_EQUALS(cmpAlertRaiser.IncreaseAlertLevel(), false); +TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 0); -cmpAlertRaiser = ConstructComponent(alertRaiserId, "AlertRaiser", { +cmpAlertRaiser = ConstructComponent(alertRaiserID, "AlertRaiser", { "MaximumLevel": 2, "Range": 50 }); +TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 0); +TS_ASSERT_EQUALS(cmpAlertRaiser.HasRaisedAlert(), false); TS_ASSERT_EQUALS(cmpAlertRaiser.CanIncreaseLevel(), true); - -cmpAlertRaiser.UpdateUnits([]); -cmpAlertRaiser.UpdateUnits(unitsIds); - -TS_ASSERT_UNEVAL_EQUALS(cmpAlertRaiser.walkingUnits, []); TS_ASSERT_EQUALS(cmpAlertRaiser.IncreaseAlertLevel(), true); + TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 1); -TS_ASSERT_UNEVAL_EQUALS(cmpAlertRaiser.prodBuildings, buildingsIds); +TS_ASSERT_EQUALS(cmpAlertRaiser.HasRaisedAlert(), true); +TS_ASSERT_EQUALS(cmpAlertRaiser.CanIncreaseLevel(), true); TS_ASSERT_EQUALS(cmpAlertRaiser.IncreaseAlertLevel(), true); + TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 2); -TS_ASSERT_UNEVAL_EQUALS(cmpAlertRaiser.walkingUnits, unitsIds); -TS_ASSERT_EQUALS(cmpAlertRaiser.IncreaseAlertLevel(), false); TS_ASSERT_EQUALS(cmpAlertRaiser.HasRaisedAlert(), true); +TS_ASSERT_EQUALS(cmpAlertRaiser.CanIncreaseLevel(), false); +TS_ASSERT_EQUALS(cmpAlertRaiser.IncreaseAlertLevel(), false); + TS_ASSERT_EQUALS(cmpAlertRaiser.EndOfAlert(), true); TS_ASSERT_EQUALS(cmpAlertRaiser.GetLevel(), 0);