Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 25188) +++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 25189) @@ -1,538 +1,547 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** + * Time between heals. + */ +GarrisonHolder.prototype.HEAL_TIMEOUT = 1000; + +/** * Initialize GarrisonHolder Component * Garrisoning when loading a map is set in the script of the map, by setting initGarrison * which should contain the array of garrisoned entities. */ GarrisonHolder.prototype.Init = function() { this.entities = []; this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity); }; /** * @param {number} entity - The entity to verify. * @return {boolean} - Whether the given entity is garrisoned in this GarrisonHolder. */ GarrisonHolder.prototype.IsGarrisoned = function(entity) { return this.entities.indexOf(entity) != -1; }; /** * @return {Object} max and min range at which entities can garrison the holder. */ GarrisonHolder.prototype.LoadingRange = function() { return { "max": +this.template.LoadingRange, "min": 0 }; }; GarrisonHolder.prototype.CanPickup = function(ent) { if (!this.template.Pickup || this.IsFull()) return false; let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership); return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent); }; GarrisonHolder.prototype.GetEntities = function() { return this.entities; }; /** * @return {Array} unit classes which can be garrisoned inside this * particular entity. Obtained from the entity's template. */ GarrisonHolder.prototype.GetAllowedClasses = function() { return this.allowedClasses; }; GarrisonHolder.prototype.GetCapacity = function() { return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity); }; GarrisonHolder.prototype.IsFull = function() { return this.OccupiedSlots() >= this.GetCapacity(); }; GarrisonHolder.prototype.GetHealRate = function() { return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity); }; /** * Set this entity to allow or disallow garrisoning in the entity. * Every component calling this function should do it with its own ID, and as long as one * component doesn't allow this entity to garrison, it can't be garrisoned * When this entity already contains garrisoned soldiers, * these will not be able to ungarrison until the flag is set to true again. * * This more useful for modern-day features. For example you can't garrison or ungarrison * a driving vehicle or plane. * @param {boolean} allow - Whether the entity should be garrisonable. */ GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID) { if (!this.allowGarrisoning) this.allowGarrisoning = new Map(); this.allowGarrisoning.set(callerID, allow); }; /** * @return {boolean} - Whether (un)garrisoning is allowed. */ GarrisonHolder.prototype.IsGarrisoningAllowed = function() { return !this.allowGarrisoning || Array.from(this.allowGarrisoning.values()).every(allow => allow); }; GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function() { let count = this.entities.length; for (let ent of this.entities) { let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) count += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); } return count; }; GarrisonHolder.prototype.OccupiedSlots = function() { let count = 0; for (let ent of this.entities) { let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) count += cmpGarrisonable.TotalSize(); } return count; }; GarrisonHolder.prototype.IsAllowedToGarrison = function(entity) { if (!this.IsGarrisoningAllowed()) return false; let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable); if (!cmpGarrisonable || this.OccupiedSlots() + cmpGarrisonable.TotalSize() > this.GetCapacity()) return false; return this.IsAllowedToBeGarrisoned(entity); }; GarrisonHolder.prototype.IsAllowedToBeGarrisoned = function(entity) { if (!IsOwnedByMutualAllyOfEntity(entity, this.entity)) return false; let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), this.allowedClasses); }; /** * @param {number} entity - The entityID to garrison. * @return {boolean} - Whether the entity was garrisoned. */ GarrisonHolder.prototype.Garrison = function(entity) { if (!this.IsAllowedToGarrison(entity)) return false; if (!this.HasEnoughHealth()) return false; - if (!this.timer && this.GetHealRate() > 0) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); - } + if (!this.timer && this.GetHealRate()) + this.StartTimer(); this.entities.push(entity); this.UpdateGarrisonFlag(); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); return true; }; /** * @param {number} entity - The entity ID of the entity to eject. * @param {boolean} forced - Whether eject is forced (e.g. if building is destroyed). * @return {boolean} Whether the entity was ejected. */ GarrisonHolder.prototype.Eject = function(entity, forced) { if (!this.IsGarrisoningAllowed() && !forced) return false; let entityIndex = this.entities.indexOf(entity); // Error: invalid entity ID, usually it's already been ejected, assume success. if (entityIndex == -1) return true; this.entities.splice(entityIndex, 1); this.UpdateGarrisonFlag(); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] }); return true; }; /** * Tell unit to unload from this entity. * @param {number} entity - The entity to unload. * @return {boolean} Whether the command was successful. */ GarrisonHolder.prototype.Unload = function(entity) { let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable); return cmpGarrisonable && cmpGarrisonable.UnGarrison(); }; /** * Tell units to unload from this entity. * @param {number[]} entities - The entities to unload. * @return {boolean} - Whether all unloads were successful. */ GarrisonHolder.prototype.UnloadEntities = function(entities) { let success = true; for (let entity of entities) if (!this.Unload(entity)) success = false; return success; }; /** * Unload one or all units that match a template and owner from us. * @param {string} template - Type of units that should be ejected. * @param {number} owner - Id of the player whose units should be ejected. * @param {boolean} all - Whether all units should be ejected. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all) { let entities = []; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); for (let entity of this.entities) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); // Units with multiple ranks are grouped together. let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity); if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner()) continue; entities.push(entity); // If 'all' is false, only ungarrison the first matched unit. if (!all) break; } return this.UnloadEntities(entities); }; /** * Unload all units, that belong to certain player * and order all own units to move to the rally point. * @param {number} owner - Id of the player whose units should be ejected. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAllByOwner = function(owner) { let entities = this.entities.filter(ent => { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); return cmpOwnership && cmpOwnership.GetOwner() == owner; }); return this.UnloadEntities(entities); }; /** * Unload all units from the entity and order them to move to the rally point. * @return {boolean} Whether the unloading was successful. */ GarrisonHolder.prototype.UnloadAll = function() { return this.UnloadEntities(this.entities.slice()); }; /** * Used to check if the garrisoning entity's health has fallen below * a certain limit after which all garrisoned units are unloaded. */ GarrisonHolder.prototype.OnHealthChanged = function(msg) { if (!this.HasEnoughHealth() && this.entities.length) this.EjectOrKill(this.entities.slice()); }; GarrisonHolder.prototype.HasEnoughHealth = function() { // 0 is a valid value so explicitly check for undefined. if (this.template.EjectHealth === undefined) return true; let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); return !cmpHealth || cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints()); }; +GarrisonHolder.prototype.StartTimer = function() +{ + if (this.timer) + return; + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + this.timer = cmpTimer.SetInterval(this.entity, IID_GarrisonHolder, "HealTimeout", this.HEAL_TIMEOUT, this.HEAL_TIMEOUT, null); +}; + +GarrisonHolder.prototype.StopTimer = function() +{ + if (!this.timer) + return; + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.timer); + delete this.timer; +}; + /** - * Called every second. Heals garrisoned units. + * @params data and lateness are unused. */ -GarrisonHolder.prototype.HealTimeout = function(data) +GarrisonHolder.prototype.HealTimeout = function(data, lateness) { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - if (!this.entities.length) + let healRate = this.GetHealRate(); + if (!this.entities.length || !healRate) { - cmpTimer.CancelTimer(this.timer); - delete this.timer; + this.StopTimer(); return; } for (let entity of this.entities) { let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth && !cmpHealth.IsUnhealable()) - cmpHealth.Increase(this.GetHealRate()); + cmpHealth.Increase(healRate); } - - this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); }; /** * Updates the garrison flag depending whether something is garrisoned in the entity. */ GarrisonHolder.prototype.UpdateGarrisonFlag = function() { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned"); }; /** * Cancel timer when destroyed. */ GarrisonHolder.prototype.OnDestroy = function() { if (this.timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); } }; /** * If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'), * remove it from the building so we only ever contain valid entities. */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { // The ownership change may be on the garrisonholder if (this.entity == msg.entity) { let entities = this.entities.filter(ent => msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, ent)); if (entities.length) this.EjectOrKill(entities); return; } // or on some of its garrisoned units let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1 && (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))) this.EjectOrKill([msg.entity]); }; /** * Update list of garrisoned entities when a game inits. */ GarrisonHolder.prototype.OnGlobalSkirmishReplacerReplaced = function(msg) { if (!this.initGarrison) return; if (msg.entity == this.entity) { let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder); if (cmpGarrisonHolder) cmpGarrisonHolder.initGarrison = this.initGarrison; } else { let entityIndex = this.initGarrison.indexOf(msg.entity); if (entityIndex != -1) this.initGarrison[entityIndex] = msg.newentity; } }; /** * Eject all foreign garrisoned entities which are no more allied. */ GarrisonHolder.prototype.OnDiplomacyChanged = function() { this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent))); }; /** * Eject or kill a garrisoned unit which can no more be garrisoned * (garrisonholder's health too small or ownership changed). */ GarrisonHolder.prototype.EjectOrKill = function(entities) { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); // Eject the units which can be ejected (if not in world, it generally means this holder // is inside a holder which kills its entities, so do not eject) if (cmpPosition && cmpPosition.IsInWorld()) { let ejectables = entities.filter(ent => this.IsEjectable(ent)); if (ejectables.length) this.UnloadEntities(ejectables); } // And destroy all remaining entities let killedEntities = []; for (let entity of entities) { let entityIndex = this.entities.indexOf(entity); if (entityIndex == -1) continue; let cmpHealth = Engine.QueryInterface(entity, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(entity); this.entities.splice(entityIndex, 1); killedEntities.push(entity); } if (killedEntities.length) { Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities }); this.UpdateGarrisonFlag(); } }; /** * Whether an entity is ejectable. * @param {number} entity - The entity-ID to be tested. * @return {boolean} - Whether the entity is ejectable. */ GarrisonHolder.prototype.IsEjectable = function(entity) { if (!this.entities.find(ent => ent == entity)) return false; let ejectableClasses = this.template.EjectClassesOnDestroy._string; let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList(); return MatchesClassList(entityClasses, ejectableClasses); }; /** * Sets the intitGarrison to the specified entities. Used by the mapreader. * * @param {number[]} entities - The entity IDs to garrison on init. */ GarrisonHolder.prototype.SetInitGarrison = function(entities) { this.initGarrison = clone(entities); }; /** * Initialise the garrisoned units. */ GarrisonHolder.prototype.OnGlobalInitGame = function(msg) { if (!this.initGarrison) return; for (let ent of this.initGarrison) { let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable) cmpGarrisonable.Garrison(this.entity); } - this.initGarrison = undefined; + delete this.initGarrison; }; GarrisonHolder.prototype.OnValueModification = function(msg) { if (msg.component != "GarrisonHolder") return; if (msg.valueNames.indexOf("GarrisonHolder/List/_string") !== -1) { this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity); this.EjectOrKill(this.entities.filter(entity => !this.IsAllowedToBeGarrisoned(entity))); } if (msg.valueNames.indexOf("GarrisonHolder/BuffHeal") === -1) return; - if (this.timer && this.GetHealRate() == 0) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - delete this.timer; - } - else if (!this.timer && this.GetHealRate() > 0) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {}); - } + if (this.timer && !this.GetHealRate()) + this.StopTimer(); + else if (!this.timer && this.GetHealRate()) + this.StartTimer(); }; Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 25188) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 25189) @@ -1,291 +1,291 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; const unitToGarrisonId = 24; const enemyUnitId = 34; const largeUnitId = 35; const player = 1; const friendlyPlayer = 2; const enemyPlayer = 3; let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": "10", "List": { "_string": "Infantry+Cavalry" }, "EjectHealth": "0.1", "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": "1", "LoadingRange": "2.1", "Pickup": false }); AddMock(garrisonHolderId, IID_Ownership, { "GetOwner": () => player }); AddMock(player, IID_Player, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, "GetPlayerID": () => player }); AddMock(friendlyPlayer, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetTimeout": (ent, iid, funcname, time, data) => 1 + "SetInterval": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); for (let i = 24; i <= 35; ++i) { AddMock(i, IID_Identity, { "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); if (i < 28) AddMock(i, IID_Ownership, { "GetOwner": () => player }); else if (i == 34) AddMock(i, IID_Ownership, { "GetOwner": () => enemyPlayer }); else AddMock(i, IID_Ownership, { "GetOwner": () => friendlyPlayer }); if (i == largeUnitId) AddMock(i, IID_Garrisonable, { "UnitSize": () => 9, "TotalSize": () => 9, "Garrison": (entity) => cmpGarrisonHolder.Garrison(i), "UnGarrison": () => cmpGarrisonHolder.Eject(i) }); else AddMock(i, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": entity => cmpGarrisonHolder.Garrison(i), "UnGarrison": () => cmpGarrisonHolder.Eject(i) }); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); } AddMock(33, IID_Identity, { "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "spart_infantry_archer_a" }); let testGarrisonAllowed = function() { TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); for (let entity of garrisonedEntitiesList) TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(entity), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27, 28, 29, 30, 31, 32]); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 4); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(null, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 3); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(largeUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); }; // No health component yet.Pick testGarrisonAllowed(); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 50, "GetMaxHitpoints": () => 600 }); cmpGarrisonHolder.AllowGarrisoning(true, "callerID1"); cmpGarrisonHolder.AllowGarrisoning(false, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), false); cmpGarrisonHolder.AllowGarrisoning(true, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.LoadingRange(), { "max": 2.1, "min": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetHealRate(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses(), "Infantry+Cavalry"); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetCapacity(), 10); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 600, "GetMaxHitpoints": () => 600 }); // No eject health. cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": "Infantry+Cavalry" }, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false }); testGarrisonAllowed(); // Test entity renaming. let siegeEngineId = 44; AddMock(siegeEngineId, IID_Identity, { "GetClassesList": () => ["Siege"] }); let archerId = 45; AddMock(archerId, IID_Identity, { "GetClassesList": () => ["Infantry", "Ranged"] }); let originalClassList = "Infantry+Ranged Siege Cavalry"; cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": originalClassList }, "EjectHealth": 0.1, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false }); let traderId = 32; AddMock(traderId, IID_Identity, { "GetClassesList": () => ["Trader"] }); AddMock(siegeEngineId, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); let currentSiegePlayer = player; AddMock(siegeEngineId, IID_Ownership, { "GetOwner": () => currentSiegePlayer }); AddMock(siegeEngineId, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(siegeEngineId, renamed), "UnGarrison": () => true }); let cavalryId = 46; AddMock(cavalryId, IID_Identity, { "GetClassesList": () => ["Infantry", "Ranged"] }); AddMock(cavalryId, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {} }); let currentCavalryPlayer = player; AddMock(cavalryId, IID_Ownership, { "GetOwner": () => currentCavalryPlayer }); AddMock(cavalryId, IID_Garrisonable, { "UnitSize": () => 1, "TotalSize": () => 1, "Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(cavalryId, renamed), "UnGarrison": () => true }); TS_ASSERT(cmpGarrisonHolder.Garrison(cavalryId)); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1); // Eject enemy units. currentCavalryPlayer = enemyPlayer; cmpGarrisonHolder.OnGlobalOwnershipChanged({ "entity": cavalryId, "to": enemyPlayer }); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); let oldApplyValueModificationsToEntity = ApplyValueModificationsToEntity; TS_ASSERT(cmpGarrisonHolder.Garrison(siegeEngineId)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [siegeEngineId]); Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => { if (valueName !== "GarrisonHolder/List/_string") return valueName; return HandleTokens(currentValue, "-Siege Trader"); }); cmpGarrisonHolder.OnValueModification({ "component": "GarrisonHolder", "valueNames": ["GarrisonHolder/List/_string"], "entities": [garrisonHolderId] }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses().split(/\s+/), ["Infantry+Ranged", "Cavalry", "Trader"]); // The new classes are now cached so we can restore the behavior. Engine.RegisterGlobal("ApplyValueModificationsToEntity", oldApplyValueModificationsToEntity); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT(!cmpGarrisonHolder.Garrison(siegeEngineId)); TS_ASSERT(cmpGarrisonHolder.Garrison(traderId)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 25188) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 25189) @@ -1,142 +1,142 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Position.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); const player = 1; const enemyPlayer = 2; const friendlyPlayer = 3; const garrison = 10; const holder = 11; let createGarrisonCmp = entity => { AddMock(entity, IID_Identity, { "GetClassesList": () => ["Ranged"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); AddMock(entity, IID_Ownership, { "GetOwner": () => player }); AddMock(entity, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {}, "SetYRotation": angle => {} }); return ConstructComponent(entity, "Garrisonable", { "Size": "1" }); }; AddMock(holder, IID_Footprint, { "PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30), "PickSpawnPoint": entity => new Vector3D(4, 3, 30) }); AddMock(holder, IID_Ownership, { "GetOwner": () => player }); AddMock(player, IID_Player, { "IsAlly": id => id != enemyPlayer, "IsMutualAlly": id => id != enemyPlayer, "GetPlayerID": () => player }); AddMock(friendlyPlayer, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetTimeout": (ent, iid, funcname, time, data) => 1 + "SetInterval": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); AddMock(garrison, IID_Identity, { "GetClassesList": () => ["Ranged"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); AddMock(garrison, IID_Ownership, { "GetOwner": () => player }); AddMock(garrison, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetHeightOffset": height => {}, "SetYRotation": angle => {} }); let cmpGarrisonable = ConstructComponent(garrison, "Garrisonable", { "Size": "1" }); let cmpGarrisonHolder = ConstructComponent(holder, "GarrisonHolder", { "Max": "10", "List": { "_string": "Ranged" }, "EjectHealth": "0.1", "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": "1", "LoadingRange": "2.1", "Pickup": "false" }); TS_ASSERT(cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); cmpGarrisonable.OnEntityRenamed({ "entity": garrison, "newentity": -1 }); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT(cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); // Can't garrison twice. TS_ASSERT(!cmpGarrisonable.Garrison(holder)); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]); TS_ASSERT(cmpGarrisonHolder.Unload(garrison)); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); // Test initGarrison. let entities = [21, 22, 23, 24]; for (let entity of entities) createGarrisonCmp(entity); cmpGarrisonHolder.SetInitGarrison(entities); cmpGarrisonHolder.OnGlobalInitGame(); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), entities); // They turned against us! AddMock(entities[0], IID_Ownership, { "GetOwner": () => enemyPlayer }); cmpGarrisonHolder.OnDiplomacyChanged(); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), entities.length - 1); TS_ASSERT(cmpGarrisonHolder.UnloadAll()); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);