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/unit_actions.js =================================================================== --- binaries/data/mods/public/gui/session/unit_actions.js +++ binaries/data/mods/public/gui/session/unit_actions.js @@ -155,7 +155,7 @@ }, "getActionInfo": function(entState, targetState) { - if (!entState.attack || !targetState.hitpoints) + if (!targetState.hitpoints) return false; return { @@ -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) { 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 @@ -159,15 +159,17 @@ /** * @param {number} entity - The entityID to garrison. * @param {boolean} renamed - Whether the entity was renamed. + * @param {boolean} forced - Whether the entity needs to be forcefully garrisoned + * (i.e. ignoring prerequisites). * * @return {boolean} - Whether the entity was garrisoned. */ -GarrisonHolder.prototype.Garrison = function(entity, renamed = false) +GarrisonHolder.prototype.Garrison = function(entity, renamed = false, forced = false) { - if (!this.IsAllowedToGarrison(entity)) + if (!forced && !this.IsAllowedToGarrison(entity)) return false; - if (!this.HasEnoughHealth()) + if (!forced && !this.HasEnoughHealth()) return false; if (!this.timer && this.GetHealRate() > 0) 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 @@ -1862,10 +1862,24 @@ GuiInterface.prototype.CanAttack = function(player, data) { + let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI); + if (cmpUnitAI) + return cmpUnitAI.CanAttack(data.target, data.types || undefined); + let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; +GuiInterface.prototype.CanHeal = function(player, data) +{ + let cmpUnitAI = Engine.QueryInterface(data.entity, IID_UnitAI); + if (cmpUnitAI) + return cmpUnitAI.CanHeal(data.target); + + let cmpHeal = Engine.QueryInterface(data.entity, IID_Heal); + return cmpHeal && cmpHeal.CanHeal(data.target); +}; + /* * Returns batch build time. */ @@ -2014,6 +2028,7 @@ "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, + "CanHeal": 1, "GetBatchTime": 1, "IsMapRevealed": 1, Index: binaries/data/mods/public/simulation/components/TurretHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/TurretHolder.js +++ binaries/data/mods/public/simulation/components/TurretHolder.js @@ -20,11 +20,63 @@ }, "allowedClasses": points[point].AllowedClasses, "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. + */ + AutogarrisonTurrets() + { + if (!this.turretPoints) + return; + + for (let point of this.turretPoints) + if (point.template) + this.CreateTurret(point); + } + + /** + * Add a turret as specified in the template. + * This function creates an entity and places it on the turret point. + * + * @param {Object} turretPoint - A turret point to (re)create the predefined turret for. + * + * @return {boolean} - Whether the turret creation has succeeded. + */ + CreateTurret(turretPoint) + { + // This position is already occupied. + if (turretPoint.entity) + return false; + + let owner = INVALID_PLAYER; + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (cmpOwnership) + owner = cmpOwnership.GetOwner(); + + let ent = Engine.AddEntity(turretPoint.template); + let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (cmpEntOwnership) + cmpEntOwnership.SetOwner(owner); + + if (turretPoint.fixed) + return this.OccupyTurret(ent, turretPoint) || Engine.DestroyEntity(ent); + + // For now, require a GarrisonHolder if it is not a fixed turret. + // That is needed to be able to unload. + let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + return false; + + return cmpGarrisonHolder.Garrison(ent, false, true) || Engine.DestroyEntity(ent); + } + + /** * @return {Object[]} - An array of the turret points this entity has. */ GetTurretPoints() @@ -56,7 +108,7 @@ /** * Occupy a turret point with the given entity. * @param {number} entity - The entity to use. - * @param {Object} turretPoint - Optionally the specific turret point to occupy. + * @param {Object} requestedTurretPoint - Optionally the specific turret point to occupy. * * @return {boolean} - Whether the occupation was successful. */ @@ -80,7 +132,7 @@ turretPoint = requestedTurretPoint; } else - turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurret(entity, turret)); + turretPoint = this.turretPoints.find(turret => this.AllowedToOccupyTurret(entity, turret)); if (!turretPoint) return false; @@ -199,6 +251,36 @@ this.OccupyTurret(to, turretPoint); } + OnOwnershipChanged(msg) + { + // If we died, take fixed turrets with us. + if (msg.to == INVALID_PLAYER) + for (let point of this.turretPoints) + if (point.entity != null && point.fixed) + { + let cmpHealth = Engine.QueryInterface(point.entity, IID_Health); + if (cmpHealth) + cmpHealth.Kill(); + else + Engine.DestroyEntity(point.entity); + } + else + { + // If the turret point is forced and the garrisonholder did not die, + // transfer the turrets ownership as well. + for (let point of this.turretPoints) + if (point.entity != null && point.fixed) + { + let cmpEntOwnership = Engine.QueryInterface(point.entity, IID_Ownership) + if (cmpEntOwnership) + cmpEntOwnership.SetOwner(msg.to); + } + } + + if (msg.from == INVALID_PLAYER) + this.AutogarrisonTurrets(); + } + OnGarrisonedUnitsChanged(msg) { // Ignore renaming for that is handled seperately @@ -219,6 +301,16 @@ "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + 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 @@ -354,6 +354,35 @@ this.SetNextState("INDIVIDUAL.WALKING"); }, + "Order.Follow": function(msg) { + if (!this.AbleToMove()) + { + 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; + this.order.data.range = this.GetRange(this.order.data.iid); + + // We need a bit of leeway when following. + this.order.data.range.max *= 0.9; + + 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()) @@ -1600,6 +1629,48 @@ }, }, + "FOLLOWING": { + "enter": function() { + if (!this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max)) + { + this.FinishOrder(); + return true; + } + this.StartTimer(0, 500); + return false; + }, + + "leave": function() { + this.StopMoving(); + this.ResetSpeedMultiplier(); + this.StopTimer(); + }, + + "Timer": function() { + if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max)) + { + this.StopMoving(); + this.DelegateOrder(this.order.data); + } + else if (!this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max)) + { + this.FinishOrder(); + return true; + } + }, + + "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.CheckTargetRangeExplicit(this.order.data.target, this.order.data.range.min, this.order.data.range.max)) + { + this.TryMatchTargetSpeed(this.order.data.target, true); + this.DelegateOrder(this.order.data); + } + }, + }, + "WALKINGANDFIGHTING": { "enter": function() { if (!this.MoveTo(this.order.data)) @@ -3404,9 +3475,7 @@ UnitAI.prototype.IsTurret = function() { - if (!this.IsGarrisoned()) - return false; - var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); return cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY; }; @@ -5425,6 +5494,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); @@ -5545,7 +5626,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); }; /** @@ -6104,21 +6202,79 @@ UnitAI.prototype.GetRange = function(iid, type) { let component = Engine.QueryInterface(this.entity, iid); - if (!component) + if (component) + return component.GetRange(type); + + let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder); + if (!cmpTurretHolder) return undefined; - return component.GetRange(type); -} + let turretPoints = cmpTurretHolder.GetTurretPoints(); + if (!turretPoints) + return undefined; + + let result = { + "min": Infinity, + "max": -1, + "elevationBonus": 0 + }; + + for (let point of turretPoints) + { + 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 || 0); + } + + 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 cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder); + if (!cmpTurretHolder) + return false; + + let turretPoints = cmpTurretHolder.GetTurretPoints(); + if (!turretPoints) + return false; + + return turretPoints.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) @@ -6179,7 +6335,8 @@ return true; let cmpHeal = Engine.QueryInterface(this.entity, IID_Heal); - return cmpHeal && cmpHeal.CanHeal(target); + return cmpHeal ? cmpHeal.CanHeal(target) : + this.CanAnySubUnitPerform("Heal", target); }; /** @@ -6432,6 +6589,33 @@ }); }; +/** + * Add order to UnitAI components of all turrets. + * @param {Object} order - The order to delegate. + */ +UnitAI.prototype.DelegateOrder = function(order) +{ + if (!order || !order.type) + return; + + let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder); + if (!cmpTurretHolder) + return; + + for (let point of cmpTurretHolder.GetTurretPoints()) + { + 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/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,38 @@ - - 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/simulation2/components/CCmpPosition.cpp =================================================================== --- source/simulation2/components/CCmpPosition.cpp +++ source/simulation2/components/CCmpPosition.cpp @@ -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();