Index: binaries/data/mods/public/gui/common/tooltips.js =================================================================== --- binaries/data/mods/public/gui/common/tooltips.js +++ binaries/data/mods/public/gui/common/tooltips.js @@ -333,27 +333,44 @@ function getGarrisonTooltip(template) { - if (!template.garrisonHolder) - return ""; - - let tooltips = [ - sprintf(translate("%(label)s: %(garrisonLimit)s"), { - "label": headerFont(translate("Garrison Limit")), - "garrisonLimit": template.garrisonHolder.capacity - }) - ]; - - if (template.garrisonHolder.buffHeal) - tooltips.push( - sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), { - "healRateLabel": headerFont(translate("Heal:")), - "value": Math.round(template.garrisonHolder.buffHeal), - "health": unitFont(translate("Health")), - "second": unitFont(translate("second")), + let tooltips = []; + if (template.garrisonHolder) + { + tooltips.push ( + sprintf(translate("%(label)s: %(garrisonLimit)s"), { + "label": headerFont(translate("Garrison Limit")), + "garrisonLimit": template.garrisonHolder.capacity }) ); - return tooltips.join(commaFont(translate(", "))); + if (template.garrisonHolder.buffHeal) + tooltips.push( + sprintf(translate("%(healRateLabel)s %(value)s %(health)s / %(second)s"), { + "healRateLabel": headerFont(translate("Heal:")), + "value": Math.round(template.garrisonHolder.buffHeal), + "health": unitFont(translate("Health")), + "second": unitFont(translate("second")), + }) + ); + + tooltips.join(commaFont(translate(", "))); + } + if (template.canGarrison) + { + let extraSize; + if (template.garrisonHolder) + extraSize = template.garrisonHolder.garrisonedSlots; + if (template.canGarrison.size > 1 || extraSize) + tooltips.push ( + sprintf(translate("%(label)s: %(garrisonSize)s + %(extraSize)s"), { + "label": headerFont(translate("Garrison Size")), + "garrisonSize": template.canGarrison.size, + "extraSize": extraSize || "" + }) + ); + } + + return tooltips.join("\n"); } function getProjectilesTooltip(template) Index: binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- binaries/data/mods/public/gui/session/unit_actions.js +++ binaries/data/mods/public/gui/session/unit_actions.js @@ -635,15 +635,15 @@ return false; let tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { - "garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount, + "garrisoned": targetState.garrisonHolder.garrisonedSlots, "capacity": targetState.garrisonHolder.capacity }); - let extraCount = 0; + let extraCount = entState.canGarrison.size; if (entState.garrisonHolder) - extraCount += entState.garrisonHolder.garrisonedEntitiesCount; + extraCount += entState.garrisonHolder.garrisonedSlots; - if (targetState.garrisonHolder.garrisonedEntitiesCount + extraCount >= targetState.garrisonHolder.capacity) + if (targetState.garrisonHolder.garrisonedSlots + extraCount > targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); if (!MatchesClassList(entState.identity.classes, targetState.garrisonHolder.allowedClasses)) @@ -850,11 +850,11 @@ cursor = "action-garrison"; tooltip = sprintf(translate("Current garrison: %(garrisoned)s/%(capacity)s"), { - "garrisoned": targetState.garrisonHolder.garrisonedEntitiesCount, + "garrisoned": targetState.garrisonHolder.garrisonedSlots, "capacity": targetState.garrisonHolder.capacity }); - if (targetState.garrisonHolder.garrisonedEntitiesCount >= + if (targetState.garrisonHolder.garrisonedSlots >= targetState.garrisonHolder.capacity) tooltip = coloredText(tooltip, "orange"); } Index: binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- binaries/data/mods/public/simulation/ai/common-api/entity.js +++ binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -398,6 +398,8 @@ "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, + "garrisonSize": function() { return this.get("Garrisonable/Size"); }, + "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); }, "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); }, @@ -720,7 +722,18 @@ }, "garrisoned": function() { return this._entity.garrisoned; }, - "canGarrisonInside": function() { return this._entity.garrisoned.length < this.garrisonMax(); }, + + "garrisonedSlots": function() { + let count = 0; + + if (this._entity.garrisoned) + for (let ent of this._entity.garrisoned) + count += +this._ai._entities.get(ent).garrisonSize(); + + return count; + }, + + "canGarrisonInside": function() { return this.garrisonedSlots() < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. Index: binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -738,7 +738,7 @@ if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return false; let minGarrison = data.min || target.garrisonMax(); - if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) + if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) return false; if (data.attacker) { @@ -790,7 +790,7 @@ let ret = false; for (let ent of units.values()) { - if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) + if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) break; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { @@ -820,7 +820,7 @@ continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) + if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax()) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; @@ -856,7 +856,7 @@ continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; - if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && + if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) @@ -877,7 +877,7 @@ garrisonManager.garrison(gameState, unit, nearest, "protection"); return true; } - if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent + if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); Index: binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/garrisonManager.js +++ binaries/data/mods/public/simulation/ai/petra/garrisonManager.js @@ -182,7 +182,7 @@ } list.splice(j--, 1); } - if (this.numberOfGarrisonedUnits(holder) === 0) + if (this.numberOfGarrisonedSlots(holder) === 0) this.holders.delete(id); else holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); @@ -196,7 +196,7 @@ let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); - else if (this.numberOfGarrisonedUnits(ent) < gmin) + else if (this.numberOfGarrisonedSlots(ent) < gmin) gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": "decay" }); } }; @@ -210,6 +210,15 @@ return holder.garrisoned().length + this.holders.get(holder.id()).list.length; }; +/** TODO should add the units garrisoned inside garrisoned units */ +m.GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder) +{ + if (!this.holders.has(holder.id())) + return holder.garrisonedSlots(); + + return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length; +}; + m.GarrisonManager.prototype.allowMelee = function(holder) { if (!this.holders.has(holder.id())) @@ -221,7 +230,7 @@ /** This is just a pre-garrison state, while the entity walk to the garrison holder */ m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) { - if (this.numberOfGarrisonedUnits(holder) >= holder.garrisonMax() || !ent.canGarrison()) + if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison()) return; this.registerHolder(gameState, holder); Index: binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -2105,7 +2105,7 @@ // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units - let numGarrisoned = this.garrisonManager.numberOfGarrisonedUnits(nearestAnchor); + let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) Index: binaries/data/mods/public/simulation/components/AlertRaiser.js =================================================================== --- binaries/data/mods/public/simulation/components/AlertRaiser.js +++ binaries/data/mods/public/simulation/components/AlertRaiser.js @@ -63,7 +63,7 @@ let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (!reserved.has(ent)) - reserved.set(ent, cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.GetGarrisonedEntitiesCount()); + reserved.set(ent, cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.GetGarrisonedSlots()); return cmpGarrisonHolder.IsAllowedToGarrison(unit) && reserved.get(ent); }); 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 @@ -121,7 +121,7 @@ GarrisonHolder.prototype.IsFull = function() { - return this.GetGarrisonedEntitiesCount() >= this.GetCapacity(); + return this.GetGarrisonedSlots() >= this.GetCapacity(); }; GarrisonHolder.prototype.GetHealRate = function() @@ -162,6 +162,21 @@ return count; }; +GarrisonHolder.prototype.GetGarrisonedSlots = function() +{ + let count = 0; + for (let ent of this.entities) + { + let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); + if (cmpGarrisonable) + count += cmpGarrisonable.GetSize(); + let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); + if (cmpGarrisonHolder) + count += cmpGarrisonHolder.GetGarrisonedSlots(); + } + return count; +}; + GarrisonHolder.prototype.IsAllowedToGarrison = function(ent) { if (!this.IsGarrisoningAllowed()) @@ -244,11 +259,13 @@ return false; // Check the capacity - let extraCount = 0; + let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable); + let extraCount = cmpGarrisonable.GetSize(); + let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (cmpGarrisonHolder) - extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount(); - if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity()) + extraCount += cmpGarrisonHolder.GetGarrisonedSlots(); + if (this.GetGarrisonedSlots() + extraCount > this.GetCapacity()) return false; if (!this.timer && this.GetHealRate() > 0) Index: binaries/data/mods/public/simulation/components/Garrisonable.js =================================================================== --- binaries/data/mods/public/simulation/components/Garrisonable.js +++ binaries/data/mods/public/simulation/components/Garrisonable.js @@ -1,6 +1,15 @@ function Garrisonable() {} -Garrisonable.prototype.Schema = ""; +Garrisonable.prototype.Schema = + "Controls the garrisonability of an entity." + + "" + + "10" + + "" + + "" + + "" + + "" + + "" + + ""; Garrisonable.prototype.Init = function() { @@ -8,4 +17,9 @@ Garrisonable.prototype.Serialize = null; +Garrisonable.prototype.GetSize = function() +{ + return ApplyValueModificationsToEntity("Garrisonable/Size", +this.template.Size, this.entity); +}; + Engine.RegisterComponentType(IID_Garrisonable, "Garrisonable", Garrisonable); Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -335,10 +335,14 @@ "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), - "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() + "garrisonedSlots": cmpGarrisonHolder.GetGarrisonedSlots() }; - ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); + let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); + if (cmpGarrisonable) + ret.canGarrison = { + "size": cmpGarrisonable.GetSize() + } let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) 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 @@ -15,6 +15,7 @@ const garrisonHolderId = 15; const unitToGarrisonId = 24; const enemyUnitId = 34; +const largeUnitId = 35; const player = 1; const friendlyPlayer = 2; const enemyPlayer = 3; @@ -53,7 +54,7 @@ "GetPlayerByID": id => id }); -for (let i = 24; i <= 34; ++i) +for (let i = 24; i <= 35; ++i) { AddMock(i, IID_Identity, { "GetClassesList": () => "Infantry+Cavalry", @@ -73,7 +74,14 @@ "GetOwner": () => friendlyPlayer }); - AddMock(i, IID_Garrisonable, {}); + if (i == 35) + AddMock(i, IID_Garrisonable, { + "GetSize": () => 9 + }); + else + AddMock(i, IID_Garrisonable, { + "GetSize": () => 1 + }); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, @@ -135,6 +143,7 @@ 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.PerformGarrison(unitToGarrisonId), false); @@ -147,10 +156,14 @@ 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.Eject(largeUnitId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(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); @@ -167,5 +180,6 @@ 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.PerformGarrison(largeUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -580,7 +580,6 @@ "needsRepair": false, "needsHeal": true, "builder": true, - "canGarrison": false, "visibility": "visible", "isBarterMarket":true, "resourceTrickle": { Index: binaries/data/mods/public/simulation/templates/template_unit.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit.xml +++ binaries/data/mods/public/simulation/templates/template_unit.xml @@ -28,7 +28,9 @@ 2.5 - + + 1 + corpse