Index: binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml =================================================================== --- binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml +++ binaries/data/mods/public/art/actors/units/britons/hero_chariot_javelinist_boudicca_m.xml @@ -17,7 +17,6 @@ - Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -348,7 +348,7 @@ for (let state of unitEntStates) if (state.garrisonHolder) - groups.add(state.garrisonHolder.entities); + groups.add(state.garrisonHolder.hiddenEntities); return groups.getEntsGrouped(); }, 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 @@ -201,7 +201,7 @@ }, "getActionInfo": function(entState, targetState) { - if (!entState.attack || !targetState.hitpoints) + if (!targetState.hitpoints) return false; return { @@ -310,21 +310,17 @@ }, "getActionInfo": function(entState, targetState) { - if (!entState.heal || - !hasClass(targetState, "Unit") || !targetState.needsHeal || + if (!hasClass(targetState, "Unit") || !targetState.needsHeal || !playerCheck(entState, targetState, ["Player", "Ally"]) || entState.id == targetState.id) // Healers can't heal themselves. return false; - let unhealableClasses = entState.heal.unhealableClasses; - if (MatchesClassList(targetState.identity.classes, unhealableClasses)) - return false; - - let healableClasses = entState.heal.healableClasses; - if (!MatchesClassList(targetState.identity.classes, healableClasses)) - return false; - - return { "possible": true }; + return { + "possible": Engine.GuiInterfaceCall("CanHeal", { + "entity": entState.id, + "target": targetState.id + }) + }; }, "actionCheck": function(target, selection) { @@ -1113,7 +1109,12 @@ let count = 0; for (let entState of entStates) if (entState.garrisonHolder) - count += entState.garrisonHolder.entities.length; + for (let entity of entState.garrisonHolder.entities) + { + let state = GetEntityState(entity); + if (state.canGarrison && state.canGarrison.unloadable) + ++count; + } if (!count) return false; @@ -1221,7 +1222,7 @@ "getInfo": function(entStates) { if (entStates.every(entState => { - if (!entState.unitAI || !entState.turretParent) + if (!entState.unitAI || !entState.turretParent || !entState.canGarrison.unloadable) return true; let parent = GetEntityState(entState.turretParent); return !parent || !parent.garrisonHolder || parent.garrisonHolder.entities.indexOf(entState.id) == -1; Index: binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/Attack.js +++ binaries/data/mods/public/simulation/components/Attack.js @@ -331,12 +331,13 @@ */ Attack.prototype.GetFullAttackRange = function() { - let ret = { "min": Infinity, "max": 0 }; + let ret = { "min": Infinity, "max": 0, "elevationBonus": Infinity }; for (let type of this.GetAttackTypes()) { let range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); + ret.elevationBonus = Math.min(ret.elevationBonus, range.elevationBonus); } return ret; }; 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 @@ -36,6 +36,16 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + @@ -72,6 +82,7 @@ { let points = this.template.VisibleGarrisonPoints; for (let point in points) + { this.visibleGarrisonPoints.push({ "offset": { "x": +points[point].X, @@ -79,12 +90,58 @@ "z": +points[point].Z }, "angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null, - "entity": null + "entity": null, + "template": points[point].Template, + "fixed": points[point].Fixed && points[point].Fixed == "true" }); + } } }; /** + * Autogarrison the turrets as specified in the template. + * This function has to be called from ownership change for all components ought to be initialised. + */ +GarrisonHolder.prototype.AutogarrisonTurrets = function() +{ + if (!this.visibleGarrisonPoints) + return; + + for (let point of this.visibleGarrisonPoints) + if (point.template) + this.CreateTurret(point); +}; + +/** + * Add a turret as specified in the template. + * This function can be called from AutogarrisonTurrets + * or when retraining a turret. + * + * @param {Object} vgp - A visible garrison point to (re)create the predefined turret for. + * + * @return {boolean} - Whether the turret creation has succeeded. + */ +GarrisonHolder.prototype.CreateTurret = function(vgp) +{ + // This position is already occupied. + if (vgp.entity) + return false; + + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) + return false; + let owner = cmpOwnership.GetOwner(); + + let filter = vgp.fixed ? "fixedgarrison|" : ""; + let ent = Engine.AddEntity(filter + vgp.template); + let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (cmpEntOwnership) + cmpEntOwnership.SetOwner(owner); + + return this.Garrison(ent, vgp, true) || Engine.DestroyEntity(ent); +}; + +/** * @return {Object} max and min range at which entities can garrison the holder. */ GarrisonHolder.prototype.GetLoadingRange = function() @@ -106,6 +163,51 @@ }; /** + * Get the entity IDs of the entities which are not visibly garrisoned. + * Used in the GUI to display in the garrison panel. + * + * @return {number[]} - An array containing entity IDs. + */ +GarrisonHolder.prototype.GetHiddenGarrisonedEntities = function() +{ + return this.entities.filter(entity => !this.IsVisiblyGarrisoned(entity)); +}; + +/** + * Test whether the entity is visibly garrisoned. + * + * @param {number} entity - The entity ID of the entity to check. + * + * @return {boolean} - Whether the entity is visibly garrisoned. + */ +GarrisonHolder.prototype.IsVisiblyGarrisoned = function(entity) +{ + return this.visibleGarrisonPoints.some(vgp => vgp.entity == entity); +}; + +/** + * Get the visible garrison points of this entity. + * + * @return {Object[]} - An array containing visibly garrison points. + */ +GarrisonHolder.prototype.GetVisibleGarrisonPoints = function() +{ + return this.visibleGarrisonPoints; +}; + +/** + * Get the visible garrison point this entity occupies. + * + * @param {number} entity - The entity ID of the entity to check. + * + * @return {Object[]} - An array containing visibly garrison points. + */ +GarrisonHolder.prototype.GetVisibleGarrisonPoint = function(entity) +{ + return this.visibleGarrisonPoints.find(vgp => vgp.entity == entity); +}; + +/** * @return {Array} unit classes which can be garrisoned inside this * particular entity. Obtained from the entity's template. */ @@ -179,18 +281,20 @@ }; /** - * Garrison a unit inside. The timer for AutoHeal is started here. - * @param {number} vgpEntity - The visual garrison point that will be used. + * Garrison a unit inside. The timer for AutoHeal is started in PerformGarrison. + * @param {Object} 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. + * @param {boolean} forced - Whether the garrison is forced (e.g. a turret created on init). + * + * @return {boolean} Whether the entity was garrisoned. */ -GarrisonHolder.prototype.Garrison = function(entity, vgpEntity) +GarrisonHolder.prototype.Garrison = function(entity, vgpEntity, forced) { let cmpPosition = Engine.QueryInterface(entity, IID_Position); if (!cmpPosition) return false; - if (!this.PerformGarrison(entity)) + if (!this.PerformGarrison(entity, forced)) return false; let visibleGarrisonPoint = vgpEntity; @@ -213,7 +317,7 @@ // the current angle as it was used for garrisoning and thus quite often was from inside to // outside, except when garrisoning from outWorld where we take as default PI. let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position); - if (!vgpEntity && visibleGarrisonPoint.angle != null) + if (!vgpEntity && visibleGarrisonPoint.angle != null || forced) cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle); else if (!vgpEntity && !cmpPosition.IsInWorld()) cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI); @@ -223,7 +327,10 @@ cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset); let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); if (cmpUnitAI) + { cmpUnitAI.SetTurretStance(); + cmpUnitAI.SetGarrisoned(); + } } else cmpPosition.MoveOutOfWorld(); @@ -232,18 +339,22 @@ }; /** - * @return {boolean} Whether the entity was garrisonned. + * @param {number} entity - The entity ID to garrison. + * @param {boolean} forced - Whether the garrison has to be forced (e.g. turret creation on init). + * + * @return {boolean} Whether the entity was garrisoned. */ -GarrisonHolder.prototype.PerformGarrison = function(entity) +GarrisonHolder.prototype.PerformGarrison = function(entity, forced) { if (!this.HasEnoughHealth()) return false; - // Check if the unit is allowed to be garrisoned inside the building - if (!this.IsAllowedToGarrison(entity)) + // Check if the unit is allowed to be garrisoned inside this entity. + // If it is forced we don't care whether it is allowed or not. + if (!forced && !this.IsAllowedToGarrison(entity)) return false; - // Check the capacity + // Check the capacity. let extraCount = 0; let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (cmpGarrisonHolder) @@ -431,7 +542,7 @@ */ GarrisonHolder.prototype.Unload = function(entity, forced) { - return this.PerformEject([entity], forced); + return this.IsUnloadable(entity) && this.PerformEject([entity], forced); }; /** @@ -477,7 +588,7 @@ { let entities = this.entities.filter(ent => { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); - return cmpOwnership && cmpOwnership.GetOwner() == owner; + return cmpOwnership && cmpOwnership.GetOwner() == owner && this.IsUnloadable(ent); }); return this.PerformEject(entities, forced); }; @@ -489,7 +600,7 @@ */ GarrisonHolder.prototype.UnloadAll = function(forced) { - return this.PerformEject(this.entities.slice(), forced); + return this.PerformEject(this.entities.slice().filter(ent => this.IsUnloadable(ent)), forced); }; /** @@ -561,18 +672,39 @@ */ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg) { - // The ownership change may be on the garrisonholder + // 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 the visible garrison point is forced and the garrisonholder did not die, transfer the turrets ownership as well. + if (msg.to != INVALID_PLAYER) + for (let point of this.visibleGarrisonPoints) + if (point.entity != null && point.forced) + { + let entIndex = this.entities.indexOf(point.entity); + if (entIndex == -1) + continue; + let cmpEntOwnership = Engine.QueryInterface(point.entity, IID_Ownership) + if (cmpEntOwnership) + cmpEntOwnership.SetOwner(msg.to); + entities.splice(entIndex, 1); + } + if (entities.length) this.EjectOrKill(entities); + // If the garrison holder has just been created, autogarrison turrets. + // If there is a initGarrison, assume the turrets will already be created + // (e.g. by loading a map) and don't create new ones. + if (msg.from == INVALID_PLAYER && !this.initGarrison) + this.AutogarrisonTurrets(); + return; } - // or on some of its garrisoned units + // ...or on some of its garrisoned units. let entityIndex = this.entities.indexOf(msg.entity); if (entityIndex != -1) { @@ -666,6 +798,13 @@ if (cmpHealth) cmpHealth.Kill(); this.entities.splice(entityIndex, 1); + for (let vgp of this.visibleGarrisonPoints) + { + if (vgp.entity != entity) + continue; + vgp.entity = null; + break; + } killedEntities.push(entity); } @@ -681,7 +820,7 @@ */ GarrisonHolder.prototype.IsEjectable = function(entity) { - if (!this.entities.find(ent => ent == entity)) + if (!this.IsUnloadable(entity)) return false; let ejectableClasses = this.template.EjectClassesOnDestroy._string; @@ -701,6 +840,20 @@ }; /** + * Whether an entity is unloadable. + * @param {number} entity - The entity-ID to be tested. + * @return {boolean} - Whether the entity is unloadable. + */ +GarrisonHolder.prototype.IsUnloadable = function(entity) +{ + if (!this.entities.some(ent => ent == entity)) + return false; + + let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable); + return cmpGarrisonable && cmpGarrisonable.IsUnloadable(); +}; + +/** * Initialise the garrisoned units. */ GarrisonHolder.prototype.OnGlobalInitGame = function(msg) 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,11 +1,40 @@ -function Garrisonable() {} +class Garrisonable +{ + get Schema() + { + return "Controls the garrisonability of an entity." + + "" + + "true" + + "" + + "" + + "" + + ""; + } -Garrisonable.prototype.Schema = ""; + Init() + { + this.SetUnloadable(this.template.Unloadable == "true"); + } -Garrisonable.prototype.Init = function() -{ -}; + /** + * Gets the ability to ungarrison. + * + * @return {boolean} Whether ungarrisoning is allowed. + */ + IsUnloadable() + { + return this.unloadable; + } -Garrisonable.prototype.Serialize = null; + /** + * Sets the ability to ungarrison. + * + * @param {boolean} unloadable - Whether ungarrisoning is allowed. + */ + SetUnloadable(unloadable) + { + this.unloadable = unloadable; + } +} 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 @@ -360,13 +360,18 @@ if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), + "hiddenEntities": cmpGarrisonHolder.GetHiddenGarrisonedEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; - ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); + let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); + if (cmpGarrisonable) + ret.canGarrison = { + "unloadable": cmpGarrisonable.IsUnloadable() + } let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) @@ -1841,8 +1846,14 @@ GuiInterface.prototype.CanAttack = function(player, data) { - let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); - return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); + let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI); + return cmpUnitAI && cmpUnitAI.CanAttack(data.target, data.types || undefined); +}; + +GuiInterface.prototype.CanHeal = function(player, data) +{ + let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI); + return cmpUnitAI && cmpUnitAI.CanHeal(data.target); }; /* @@ -1991,6 +2002,7 @@ "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, + "CanHeal": 1, "GetBatchTime": 1, "IsMapRevealed": 1, Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -347,6 +347,32 @@ this.SetNextState("INDIVIDUAL.WALKING"); }, + "Order.Follow": function(msg) { + // Let players move captured domestic animals around. + if (this.IsAnimal() && !this.IsDomestic() || this.IsTurret()) + { + this.FinishOrder(); + return; + } + + // For packable units: + // 1. If packed, we can move. + // 2. If unpacked, we first need to pack, then follow case 1. + if (this.CanPack()) + { + this.PushOrderFront("Pack", { "force": true }); + return; + } + + // It's not too bad if we don't arrive at exactly the right position. + this.order.data.relaxed = true; + + if (this.IsAnimal()) + this.SetNextState("ANIMAL.WALKING"); + else + this.SetNextState("INDIVIDUAL.FOLLOWING"); + }, + "Order.PickupUnit": function(msg) { let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (!cmpGarrisonHolder || cmpGarrisonHolder.IsFull()) @@ -1585,6 +1611,50 @@ }, }, + "FOLLOWING": { + "enter": function() { + if (!this.MoveTo(this.order.data, this.order.data.iid)) + { + this.FinishOrder(); + return true; + } + this.StartTimer(0, 500); + return false; + }, + + "leave": function() { + this.StopMoving(); + this.ResetSpeedMultiplier(); + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckRange(this.order.data, this.order.data.iid || undefined)) + { + this.StopMoving(); + this.DelegateOrder(this.order.data); + } + else if (!this.MoveTo(this.order.data, this.order.data.iid || undefined)) + { + this.FinishOrder(); + return true; + } + + // Run after a fleeing target. + let cmpUnitAI = Engine.QueryInterface(this.order.data.target, IID_UnitAI); + if (cmpUnitAI && cmpUnitAI.IsFleeing()) + this.SetSpeedMultiplier(this.GetRunMultiplier()); + }, + + "MovementUpdate": function(msg) { + // If it looks like the path is failing, and we are close enough (3 tiles) + // stop anyways. This avoids pathing for an unreachable goal and reduces lag considerably. + if (msg.likelyFailure || msg.obstructed && this.RelaxedMaxRangeCheck(this.order.data, this.DefaultRelaxedMaxRange) || + this.CheckRange(this.order.data, this.order.data.iid || undefined)) + this.DelegateOrder(this.order.data); + }, + }, + "WALKINGANDFIGHTING": { "enter": function() { if (!this.MoveTo(this.order.data)) @@ -5267,6 +5337,18 @@ "allowCapture": allowCapture, }; + // This is the case when an entity that has visibly garrisoned entities that can + // attack, but the entity itself can't attack. + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack) + if (!cmpAttack) + { + order.type = "Attack"; + order.iid = IID_Attack; + + this.AddOrder("Follow", order, queued); + return; + } + this.RememberTargetPosition(order); this.AddOrder("Attack", order, queued); @@ -5384,7 +5466,24 @@ return; } - this.AddOrder("Heal", { "target": target, "force": true }, queued); + let order = { + "target": target, + "force": true + }; + + // This is the case when an entity that has visibly garrisoned entities that can + // heal, but the entity itself can't heal. + let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal) + if (!cmpHeal) + { + order.type = "Heal"; + order.iid = IID_Heal; + + this.AddOrder("Follow", order, queued); + return; + } + + this.AddOrder("Heal", order, queued); }; /** @@ -5926,21 +6025,79 @@ UnitAI.prototype.GetRange = function(iid, type) { let component = Engine.QueryInterface(this.entity, iid); - if (!component) - return undefined; + if (component) + return component.GetRange(type); + + let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + return false; + + let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints(); + if (!visibleGarrisonPoints) + return false; + + let result = { + "min": Infinity, + "max": -1, + "elevationBonus": 0 + }; + + for (let point of visibleGarrisonPoints) + { + if (!point.entity) + continue; + + component = Engine.QueryInterface(point.entity, iid); + if (!component) + continue; + + let range = component.GetRange(type); + result.min = Math.min(result.min, range.min); + result.max = Math.max(result.max, range.max) + point.offset.z; + result.elevationBonus = Math.min(result.elevationBonus, range.elevationBonus); + } + + return result.max == -1 ? undefined : result; +} + +/** + * Checks whether one of this entities turrets can perform the given action on the given target. + * + * @param {string} action - The desired action to check. + * @param {number} target - The entity ID of the target to perform the action upon. + * @param {string[]} types - An optional array of the desired types to use (e.g. "Capture" for attack). + * + * @return {boolean} Whether at least one subunit can perform the given action. + */ +UnitAI.prototype.CanAnySubUnitPerform = function(action, target, types) +{ + let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + return false; - return component.GetRange(type); + let visibleGarrisonPoints = cmpGarrisonHolder.GetVisibleGarrisonPoints(); + if (!visibleGarrisonPoints) + return false; + + return visibleGarrisonPoints.some(point => { + if (!point.entity) + return false; + + let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI); + return cmpUnitAI && cmpUnitAI["Can" + action](target, types); + }); } -UnitAI.prototype.CanAttack = function(target) +UnitAI.prototype.CanAttack = function(target, types) { // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) + // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - return cmpAttack && cmpAttack.CanAttack(target); + return cmpAttack ? cmpAttack.CanAttack(target, types) : + this.CanAnySubUnitPerform("Attack", target, types); }; UnitAI.prototype.CanGarrison = function(target) @@ -5996,14 +6153,15 @@ UnitAI.prototype.CanHeal = function(target) { // Formation controllers should always respond to commands - // (then the individual units can make up their own minds) + // (then the individual units can make up their own minds). if (this.IsFormationController()) return true; - // Verify that we're able to respond to Heal commands + // Verify that we're able to respond to Heal commands, + // or any of our visible garrison points can. var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); if (!cmpHeal) - return false; + return this.CanAnySubUnitPerform("Heal", target); // Verify that the target is alive if (!this.TargetIsAlive(target)) @@ -6262,6 +6420,33 @@ }); }; +/** + * Add order to UnitAI components of all visibly garrisoned members. + * @param {Object} order - The order to delegate. + */ +UnitAI.prototype.DelegateOrder = function(order) +{ + if (!order || !order.type) + return; + + let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + return; + + for (let point of cmpGarrisonHolder.GetVisibleGarrisonPoints()) + { + let cmpUnitAI = Engine.QueryInterface(point.entity, IID_UnitAI); + if (!cmpUnitAI) + continue; + + let orders = cmpUnitAI.GetOrders(); + if (!orders.length || + !orders[0].data || !orders[0].data.target || + orders[0].data.target != order.target || orders[0].type != order.type) + cmpUnitAI.AddOrder(order.type, order, false); + } +}; + UnitAI.prototype.UnitFsm = new FSM(UnitAI.prototype.UnitFsmSpec); Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI); Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -150,7 +150,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); - TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); + TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80, "elevationBonus": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { 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 @@ -13,6 +13,7 @@ const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; const unitToGarrisonId = 24; +const turretToGarrisonId = 35; const enemyUnitId = 34; const player = 1; const friendlyPlayer = 2; @@ -52,7 +53,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", @@ -72,7 +73,9 @@ "GetOwner": () => friendlyPlayer }); - AddMock(i, IID_Garrisonable, {}); + AddMock(i, IID_Garrisonable, { + "IsUnloadable": () => i != turretToGarrisonId + }); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, @@ -168,3 +171,7 @@ TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); + +// Check that a non-unloadable entity is not unloadable. +TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(turretToGarrisonId), true); +TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(turretToGarrisonId), false); 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 @@ -586,7 +586,6 @@ "needsRepair": false, "needsHeal": true, "builder": true, - "canGarrison": false, "visibility": "visible", "isBarterMarket":true, "resourceTrickle": { Index: binaries/data/mods/public/simulation/templates/special/filter/fixedgarrison.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/templates/special/filter/fixedgarrison.xml @@ -0,0 +1,9 @@ + + + + true + + + false + + 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 - + + true + corpse Index: binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml =================================================================== --- binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml +++ binaries/data/mods/public/simulation/templates/units/brit_hero_boudicca.xml @@ -1,20 +1,36 @@ - - units/heroes/brit_hero_boudicca - + 5.0 brit - Chariot + Chariot -Hero Boudicca Boudica female units/brit_hero_boudicca.png + + 1 + Unit + 0 + Infantry + 0 + 2 + + + + false + 0 + 1.4 + -2.5 + 0 + + + units/britons/hero_chariot_javelinist_boudicca_m.xml Index: source/graphics/MapReader.cpp =================================================================== --- source/graphics/MapReader.cpp +++ source/graphics/MapReader.cpp @@ -1040,10 +1040,8 @@ // TODO: other parts of the position } - CmpPtr cmpOwnership(sim, ent); - if (cmpOwnership) - cmpOwnership->SetOwner(PlayerID); - + // Should be before the ownership change due to autocreation of + // turrets in GarrisonHolder.js. if (!Garrison.empty()) { CmpPtr cmpGarrisonHolder(sim, ent); @@ -1054,6 +1052,10 @@ Garrison.clear(); } + CmpPtr cmpOwnership(sim, ent); + if (cmpOwnership) + cmpOwnership->SetOwner(PlayerID); + CmpPtr cmpObstruction(sim, ent); if (cmpObstruction) { Index: source/simulation2/components/CCmpPosition.cpp =================================================================== --- source/simulation2/components/CCmpPosition.cpp +++ source/simulation2/components/CCmpPosition.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -21,6 +21,7 @@ #include "ICmpPosition.h" #include "simulation2/MessageTypes.h" +#include "simulation2/serialization/SerializeTemplates.h" #include "ICmpTerrain.h" #include "ICmpTerritoryManager.h" @@ -226,6 +227,7 @@ serialize.NumberFixed_Unbounded("y", m_TurretPosition.Y); serialize.NumberFixed_Unbounded("z", m_TurretPosition.Z); } + SerializeSet()(serialize, "turrets", m_Turrets); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) @@ -262,6 +264,7 @@ deserialize.NumberFixed_Unbounded("y", m_TurretPosition.Y); deserialize.NumberFixed_Unbounded("z", m_TurretPosition.Z); } + SerializeSet()(deserialize, "turrets", m_Turrets); if (m_InWorld) UpdateXZRotation();