Index: binaries/data/mods/public/simulation/components/BuildingAI.js =================================================================== --- binaries/data/mods/public/simulation/components/BuildingAI.js +++ binaries/data/mods/public/simulation/components/BuildingAI.js @@ -1,4 +1,4 @@ -//Number of rounds of firing per 2 seconds +// Number of rounds of firing per 2 seconds. const roundCount = 10; const attackType = "Ranged"; @@ -36,18 +36,26 @@ for (let ent of msg.added) { + if (msg.visible[ent]) + continue; + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; + if (MatchesClassList(cmpIdentity.GetClassesList(), classes)) ++this.archersGarrisoned; } for (let ent of msg.removed) { + if (msg.visible[ent]) + continue; + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; + if (MatchesClassList(cmpIdentity.GetClassesList(), classes)) --this.archersGarrisoned; } Index: binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -1,10 +1,10 @@ function GarrisonHolder() {} GarrisonHolder.prototype.Schema = - "" + + "" + "" + "" + - "" + + "" + "" + "tokens" + "" + @@ -46,6 +46,14 @@ "" + "" + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + ""+ + "" + "" + "" + "" + @@ -68,20 +76,21 @@ this.timer = undefined; this.allowGarrisoning = new Map(); this.visibleGarrisonPoints = []; - if (this.template.VisibleGarrisonPoints) - { - let points = this.template.VisibleGarrisonPoints; - for (let point in points) - this.visibleGarrisonPoints.push({ - "offset": { - "x": +points[point].X, - "y": +points[point].Y, - "z": +points[point].Z - }, - "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null, - "entity": null - }); - } + if (!this.template.VisibleGarrisonPoints) + return; + + let points = this.template.VisibleGarrisonPoints; + for (let point in points) + this.visibleGarrisonPoints.push({ + "offset": { + "x": +points[point].X, + "y": +points[point].Y, + "z": +points[point].Z + }, + "allowedClasses": points[point].AllowedClasses, + "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null, + "entity": null + }); }; /** @@ -179,10 +188,27 @@ }; /** + * @param {number} entity - The entity's id. + * @param {Object|undefined} visibleGarrisonPoint - The vgp object. + * @return {boolean} - Whether the unit is allowed be visible on that garrison point. + */ +GarrisonHolder.prototype.AllowedToVisibleGarrisoning = function(entity, visibleGarrisonPoint) +{ + if (!visibleGarrisonPoint) + return false; + + if (!visibleGarrisonPoint.allowedClasses) + return true; + + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), visibleGarrisonPoint.allowedClasses._string); +}; + +/** * Garrison a unit inside. The timer for AutoHeal is started here. * @param {number} vgpEntity - The visual garrison point that will be used. * If vgpEntity is given, this visualGarrisonPoint will be used for the entity. - * @return {boolean} Whether the entity was garrisonned. + * @return {boolean} - Whether the entity was garrisoned. */ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity) { @@ -193,16 +219,14 @@ if (!this.PerformGarrison(entity)) return false; - let visibleGarrisonPoint = vgpEntity; + let visibleGarrisonPoint; + if (vgpEntity && this.AllowedToVisibleGarrisoning(entity, vgpEntity)) + visibleGarrisonPoint = vgpEntity; + if (!visibleGarrisonPoint) - for (let vgp of this.visibleGarrisonPoints) - { - if (vgp.entity) - continue; - visibleGarrisonPoint = vgp; - break; - } + visibleGarrisonPoint = this.visibleGarrisonPoints.find(vgp => !vgp.entity && this.AllowedToVisibleGarrisoning(entity, vgp)); + let isVisiblyGarrisoned = false; if (visibleGarrisonPoint) { visibleGarrisonPoint.entity = entity; @@ -224,10 +248,21 @@ let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SetTurretStance(); + + isVisiblyGarrisoned = true; } else cmpPosition.MoveOutOfWorld(); + // Should only be called after the garrison has been performed else the visible Garrison Points are not updated yet. + Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { + "added": [entity], + "removed": [], + "visible": { + [entity]: isVisiblyGarrisoned, + } + }); + return true; }; @@ -268,7 +303,6 @@ if (cmpAura && cmpAura.HasGarrisonAura()) cmpAura.ApplyGarrisonAura(this.entity); - Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); return true; }; @@ -313,6 +347,11 @@ let cmpEntPosition = Engine.QueryInterface(entity, IID_Position); let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI); + // Needs to be set before the visible garrison points are cleared. + let visible = { + [entity]: this.IsVisiblyGarrisoned(entity) + }; + for (let vgp of this.visibleGarrisonPoints) { if (vgp.entity != entity) @@ -345,7 +384,11 @@ if (cmpPosition) cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); - Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [entity] }); + Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { + "added": [], + "removed": [entity], + "visible": visible + }); return true; }; @@ -581,7 +624,13 @@ if (cmpHealth && cmpHealth.GetHitpoints() == 0) { this.entities.splice(entityIndex, 1); - Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": [msg.entity] }); + Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { + "added": [], + "removed": [msg.entity], + "visible": { + [msg.entity]: this.IsVisiblyGarrisoned(msg.entity) + } + }); this.UpdateGarrisonFlag(); for (let point of this.visibleGarrisonPoints) @@ -670,11 +719,30 @@ } if (killedEntities.length) - Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [], "removed": killedEntities }); + { + let visibleEntitiesIds = {}; + for (let ent of killedEntities) + visibleEntitiesIds[ent] = this.IsVisiblyGarrisoned(ent); + Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { + "added": [], + "removed": killedEntities, + "visible": visibleEntitiesIds + }); + } this.UpdateGarrisonFlag(); }; /** + * Gives insight about the unit type of garrisoning. + * @param {number} entity - The entity's id. + * @return {boolean} - Whether the entity is visible on the garrison holder. + */ +GarrisonHolder.prototype.IsVisiblyGarrisoned = function(entity) +{ + return this.visibleGarrisonPoints.some(point => point.entity == entity); +}; + +/** * Whether an entity is ejectable. * @param {number} entity - The entity-ID to be tested. * @return {boolean} - Whether the entity is ejectable. Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js @@ -33,8 +33,8 @@ }); AddMock(player, IID_Player, { - "IsAlly": id => true, - "IsMutualAlly": id => true, + "IsAlly": id => id != enemyPlayer, + "IsMutualAlly": id => id != enemyPlayer, "GetPlayerID": () => player }); @@ -55,7 +55,7 @@ for (let i = 24; i <= 34; ++i) { AddMock(i, IID_Identity, { - "GetClassesList": () => "Infantry+Cavalry", + "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "mace_infantry_archer_a" }); @@ -88,7 +88,7 @@ } AddMock(33, IID_Identity, { - "GetClassesList": () => "Infantry+Cavalry", + "GetClassesList": () => ["Infantry", "Cavalry"], "GetSelectionGroupName": () => "spart_infantry_archer_a" }); @@ -168,3 +168,127 @@ TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); + +let siegeEngineId = 44; +AddMock(siegeEngineId, IID_Identity, { + "GetClassesList": () => ["Siege"] +}); +let archerId = 45; +AddMock(archerId, IID_Identity, { + "GetClassesList": () => ["Infantry", "Ranged"] +}); + +// Test visible garrisoning restrictions. +cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { + "Max": 10, + "List": { "_string": "Infantry+Ranged Siege Cavalry" }, + "EjectHealth": 0.1, + "EjectClassesOnDestroy": { "_string": "Infantry" }, + "BuffHeal": 1, + "LoadingRange": 2.1, + "Pickup": false, + "VisibleGarrisonPoints": { + "archer1": { + "X": 12, + "Y": 5, + "Z": 6 + }, + "archer2": { + "X": 15, + "Y": 5, + "Z": 6, + "AllowedClasses": { "_string": "Siege Trader" } + }, + "archer3": { + "X": 15, + "Y": 5, + "Z": 6, + "AllowedClasses": { "_string": "Siege Infantry+Ranged Infantry+Cavalry" } + } + } +}); + +AddMock(32, IID_Identity, { + "GetClassesList": () => ["Trader"] +}); + +TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(32), false); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(siegeEngineId, cmpGarrisonHolder.visibleGarrisonPoints[0]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(siegeEngineId, cmpGarrisonHolder.visibleGarrisonPoints[1]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(siegeEngineId, cmpGarrisonHolder.visibleGarrisonPoints[2]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(archerId, cmpGarrisonHolder.visibleGarrisonPoints[0]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(archerId, cmpGarrisonHolder.visibleGarrisonPoints[1]), false); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(archerId, cmpGarrisonHolder.visibleGarrisonPoints[2]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(33, cmpGarrisonHolder.visibleGarrisonPoints[0]), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(33, cmpGarrisonHolder.visibleGarrisonPoints[1]), false); +TS_ASSERT_EQUALS(cmpGarrisonHolder.AllowedToVisibleGarrisoning(33, cmpGarrisonHolder.visibleGarrisonPoints[2]), true); + +// If an entity gets renamed (e.g. promotion, upgrade) +// and is no longer able to be visibly garrisoned it +// should be garisoned instead or ejected. +AddMock(siegeEngineId, IID_Position, { + "GetHeightOffset": () => 0, + "GetPosition": () => new Vector3D(4, 3, 25), + "GetRotation": () => new Vector3D(4, 0, 6), + "GetTurretParent": () => INVALID_ENTITY, + "IsInWorld": () => true, + "JumpTo": (posX, posZ) => {}, + "MoveOutOfWorld": () => {}, + "SetTurretParent": (entity, offset) => {}, + "SetHeightOffset": height => {} +}); +let currentSiegePlayer = player; +AddMock(siegeEngineId, IID_Ownership, { + "GetOwner": () => currentSiegePlayer +}); +AddMock(siegeEngineId, IID_Garrisonable, {}); +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), + "GetTurretParent": () => INVALID_ENTITY, + "IsInWorld": () => true, + "JumpTo": (posX, posZ) => {}, + "MoveOutOfWorld": () => {}, + "SetTurretParent": (entity, offset) => {}, + "SetHeightOffset": height => {} +}); + +let currentCavalryPlayer = player; +AddMock(cavalryId, IID_Ownership, { + "GetOwner": () => currentCavalryPlayer +}); +AddMock(cavalryId, IID_Garrisonable, {}); +TS_ASSERT(cmpGarrisonHolder.Garrison(siegeEngineId)); +TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1); +TS_ASSERT(cmpGarrisonHolder.IsVisiblyGarrisoned(siegeEngineId)); +cmpGarrisonHolder.OnGlobalEntityRenamed({ + "entity": siegeEngineId, + "newentity": cavalryId +}); +TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1); +TS_ASSERT(!cmpGarrisonHolder.IsVisiblyGarrisoned(siegeEngineId)); +TS_ASSERT(!cmpGarrisonHolder.IsVisiblyGarrisoned(archerId)); + +// Eject enemy units. +currentCavalryPlayer = enemyPlayer; +cmpGarrisonHolder.OnGlobalOwnershipChanged({ + "entity": cavalryId, + "to": enemyPlayer +}); +TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); + +// Visibly garrisoned units should get ejected if they change players. +TS_ASSERT(cmpGarrisonHolder.Garrison(siegeEngineId)); +TS_ASSERT(cmpGarrisonHolder.IsVisiblyGarrisoned(siegeEngineId)); +TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1); +currentSiegePlayer = enemyPlayer; +cmpGarrisonHolder.OnGlobalOwnershipChanged({ + "entity": siegeEngineId, + "to": enemyPlayer +}); +TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0);