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 @@ -1866,7 +1866,21 @@ GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); - return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); + if (cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined)) + return true; + + let cmpTurretHolder = Engine.QueryInterface(data.entity, IID_TurretHolder); + return cmpTurretHolder && cmpTurretHolder.CanAnySubunitPerform("Attack", data.target, data.types || undefined); +}; + +GuiInterface.prototype.CanHeal = function(player, data) +{ + let cmpHeal = Engine.QueryInterface(data.entity, IID_Heal); + if (cmpHeal && cmpHeal.CanHeal(data.target)) + return true; + + let cmpTurretHolder = Engine.QueryInterface(data.entity, IID_TurretHolder); + return cmpTurretHolder && cmpTurretHolder.CanAnySubunitPerform("Heal", data.target); }; /* @@ -2017,6 +2031,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,54 @@ }, "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" }); } /** + * Add a subunit 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 subunit for. + * + * @return {boolean} - Whether the turret creation has succeeded. + */ + CreateSubunit(turretPoint) + { + // This position is already occupied. + if (turretPoint.entity) + return false; + + let ent = Engine.AddEntity(turretPoint.template); + + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (cmpOwnership) + { + let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (cmpEntOwnership) + cmpEntOwnership.SetOwner(cmpOwnership.GetOwner()); + } + + if (turretPoint.fixed) + return this.OccupyTurretPoint(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) + { + error("For now we require a GarrisonHolder to use non-fixed subunits on entity " + this.entity + "."); + this.LeaveTurretPoint(ent, turretPoint); + Engine.DestroyEntity(ent); + return false; + } + + return cmpGarrisonHolder.Garrison(ent, false, true) || Engine.DestroyEntity(ent); + } + + /** * @return {Object[]} - An array of the turret points this entity has. */ GetTurretPoints() @@ -33,12 +76,25 @@ } /** + * @return {number[]} - An array of the entityIDs of the subunits this entity has. + */ + GetSubunits() + { + let subunits = []; + for (let turretPoint of this.turretPoints) + if (turretPoint.entity) + subunits.push(turretPoint.entity); + + return subunits; + } + + /** * @param {number} entity - The entity to check for. * @param {Object} turretPoint - The turret point to use. * * @return {boolean} - Whether the entity is allowed to occupy the specified turret point. */ - AllowedToOccupyTurret(entity, turretPoint) + AllowedToOccupyTurretPoint(entity, turretPoint) { if (!turretPoint || turretPoint.entity) return false; @@ -60,8 +116,11 @@ * * @return {boolean} - Whether the occupation was successful. */ - OccupyTurret(entity, requestedTurretPoint) + OccupyTurretPoint(entity, requestedTurretPoint) { + if (this.OccupiesTurretPoint(entity)) + return false; + let cmpPositionOccupant = Engine.QueryInterface(entity, IID_Position); if (!cmpPositionOccupant) return false; @@ -70,28 +129,26 @@ if (!cmpPositionSelf) return false; - if (this.OccupiesTurret(entity)) - return false; - let turretPoint; if (requestedTurretPoint) { - if (this.AllowedToOccupyTurret(entity, requestedTurretPoint)) + if (this.AllowedToOccupyTurretPoint(entity, requestedTurretPoint)) turretPoint = requestedTurretPoint; } else - turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurret(entity, turret)); + turretPoint = this.turretPoints.find(turret => this.AllowedToOccupyTurretPoint(entity, turret)); if (!turretPoint) return false; turretPoint.entity = entity; - // Angle of turrets: + + // Angle of subunits: // Renamed entities (turretPoint != undefined) should keep their angle. // Otherwise if an angle is given in the turretPoint, use it. // If no such angle given (usually walls for which outside/inside not well defined), we keep - // 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. + // the current angle as it was used for occupying and thus quite often was from inside to + // outside, except when occupying from outOfWorld where we take as default PI. if (!turretPoint && turretPoint.angle != null) cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + turretPoint.angle); else if (!turretPoint && !cmpPosition.IsInWorld()) @@ -121,13 +178,13 @@ } /** - * Remove the entity from a turret. + * Remove the entity from a turret point. * @param {number} entity - The specific entity to eject. - * @param {Object} turret - Optionally the turret to abandon. + * @param {Object} requestedTurretPoint - Optionally the turret point to abandon. * * @return {boolean} - Whether the entity was occupying a/the turret before. */ - LeaveTurret(entity, requestedTurretPoint) + LeaveTurretPoint(entity, requestedTurretPoint) { let turretPoint; if (requestedTurretPoint) @@ -169,11 +226,11 @@ /** * @param {number} entity - The entity's id. - * @param {Object} turret - Optionally the turret to check. + * @param {Object} requestedTurretPoint - Optionally the turret point to check. * * @return {boolean} - Whether the entity is positioned on a turret of this entity. */ - OccupiesTurret(entity, requestedTurretPoint) + OccupiesTurretPoint(entity, requestedTurretPoint) { return requestedTurretPoint ? requestedTurretPoint.entity == entity : this.turretPoints.some(turretPoint => turretPoint.entity == entity); @@ -183,12 +240,65 @@ * @param {number} entity - The entity's id. * @return {Object} - The turret this entity is positioned on, if applicable. */ - GetOccupiedTurret(entity) + GetOccupiedTurretPoint(entity) { return this.turretPoints.find(turretPoint => turretPoint.entity == entity); } /** + * Get the range of the subunits given the IID and optionally type. + * @param {number} iid - The IID to get the range for. + * @param {string} type - [Optional] + * @return {Object | undefined} - The range in the form + * { "min": number, "max": number } + * Object."elevationBonus": number may be present when iid == IID_Attack. + * Returns undefined when the entity does not have the requested component. + */ + GetSubunitsRange(iid, type) + { + let result = { + "min": Infinity, + "max": -1, + "elevationBonus": Infinity + }; + + for (let point of this.turretPoints) + { + if (!point.entity) + continue; + + let 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 subunits can perform the given action on the given target. + * Restricted to entities with UnitAI for now. + * + * @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. + */ + CanAnySubunitPerform(action, target, types) + { + return this.GetSubunits().some(entity => { + let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI); + return cmpUnitAI && cmpUnitAI["Can" + action](target, types); + }); + } + + /** * We process EntityRenamed here because we need to be sure that we receive * it after it is processed by GarrisonHolder.js. * ToDo: Make this not needed by fully separating TurretHolder from GarrisonHolder. @@ -200,13 +310,50 @@ */ SwapEntities(from, to) { - let turretPoint = this.GetOccupiedTurret(from); + let turretPoint = this.GetOccupiedTurretPoint(from); if (turretPoint) - this.LeaveTurret(from, turretPoint); + this.LeaveTurretPoint(from, turretPoint); let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder && cmpGarrisonHolder.IsGarrisoned(to)) - this.OccupyTurret(to, turretPoint); + this.OccupyTurretPoint(to, turretPoint); + } + + OnOwnershipChanged(msg) + { + // If we died, take fixed subunits with us. + // Other ownership changes are handled by GarrisonHolder.js. + 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); + } + } + + // We were created, create any subunits now. + // This has to be done here for Ownership ought to be initialised. + if (msg.from == INVALID_PLAYER) + for (let point of this.turretPoints) + if (point.template) + this.CreateSubunit(point); } OnGarrisonedUnitsChanged(msg) @@ -217,9 +364,9 @@ return; for (let entity of msg.removed) - this.LeaveTurret(entity); + this.LeaveTurretPoint(entity); for (let entity of msg.added) - this.OccupyTurret(entity); + this.OccupyTurretPoint(entity); } } @@ -229,6 +376,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 @@ -358,6 +358,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()) @@ -1608,6 +1637,45 @@ }, }, + "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(); + }, + + "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)) @@ -5472,6 +5540,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); @@ -5592,7 +5672,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); }; /** @@ -6170,21 +6267,42 @@ UnitAI.prototype.GetRange = function(iid, type) { let component = Engine.QueryInterface(this.entity, iid); - if (!component) - return undefined; + if (component) + return component.GetRange(type); - return component.GetRange(type); -} + let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder); + if (cmpTurretHolder) + return cmpTurretHolder.GetSubunitsRange(iid, type); -UnitAI.prototype.CanAttack = function(target) + return undefined; +}; + +/** + * 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); + return cmpTurretHolder && + cmpTurretHolder.CanAnySubunitPerform(action, target, types); +}; + +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) @@ -6245,7 +6363,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); }; /** @@ -6498,6 +6617,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/components/tests/test_TurretHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js @@ -1,4 +1,5 @@ Engine.LoadHelperScript("Player.js"); +Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/TurretHolder.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("TurretHolder.js"); @@ -91,51 +92,73 @@ }); // Test visible garrisoning restrictions. -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[0]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[0]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[1]), false); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(archerID, cmpTurretHolder.turretPoints[2]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[0]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[1]), false); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(cavID, cmpTurretHolder.turretPoints[2]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[0]), true); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[1]), false); -TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurret(infID, cmpTurretHolder.turretPoints[2]), false); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[0]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[1]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[2]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[0]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[1]), false); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(archerID, cmpTurretHolder.turretPoints[2]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1]), false); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[0]), true); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[1]), false); +TS_ASSERT_EQUALS(cmpTurretHolder.AllowedToOccupyTurretPoint(infID, cmpTurretHolder.turretPoints[2]), false); // Test that one cannot leave a turret that is not occupied. -TS_ASSERT(!cmpTurretHolder.LeaveTurret(archerID)); +TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(archerID)); // Test occupying a turret. -TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID)); -TS_ASSERT(cmpTurretHolder.OccupyTurret(archerID)); -TS_ASSERT(cmpTurretHolder.OccupiesTurret(archerID)); +TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID)); +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID)); +TS_ASSERT(cmpTurretHolder.OccupiesTurretPoint(archerID)); // We're not occupying a turret that we can't occupy. -TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID, cmpTurretHolder.turretPoints[1])); -TS_ASSERT(!cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[1])); -TS_ASSERT(!cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[0])); -TS_ASSERT(cmpTurretHolder.OccupyTurret(cavID, cmpTurretHolder.turretPoints[2])); +TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID, cmpTurretHolder.turretPoints[1])); +TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[1])); +TS_ASSERT(!cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[0])); +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(cavID, cmpTurretHolder.turretPoints[2])); // Leave turrets. -TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID)); -TS_ASSERT(!cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[1])); -TS_ASSERT(cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[2])); +TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID)); +TS_ASSERT(!cmpTurretHolder.LeaveTurretPoint(cavID, cmpTurretHolder.turretPoints[1])); +TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(cavID, cmpTurretHolder.turretPoints[2])); // Test renaming. AddMock(turretHolderID, IID_GarrisonHolder, { "IsGarrisoned": () => true }); -TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2])); +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[2])); cmpTurretHolder.SwapEntities(siegeEngineID, archerID); -TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID)); -TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID)); +TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(siegeEngineID)); +TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID)); // Renaming into an entity not allowed on the same turret point hides us. -TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1])); +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(siegeEngineID, cmpTurretHolder.turretPoints[1])); cmpTurretHolder.SwapEntities(siegeEngineID, archerID); -TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID)); -TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID)); +TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(siegeEngineID)); +TS_ASSERT(!cmpTurretHolder.OccupiesTurretPoint(archerID)); + +// Test getting the range of subunits. +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID)); +AddMock(archerID, IID_Attack, { + "GetRange": () => { return { "min": 0, "max": 5, "elevationBonus": 2 }; } +}); +TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetSubunitsRange(IID_Attack), { + "min": 0, + "max": 5 + cmpTurretHolder.GetOccupiedTurretPoint(archerID).offset.z, + "elevationBonus": 2 +}); +TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID)); + +// Test getting the actions of subunits. +TS_ASSERT(cmpTurretHolder.OccupyTurretPoint(archerID)); +AddMock(archerID, IID_UnitAI, { + "CanAttack": (target) => target == 90 +}); +TS_ASSERT(cmpTurretHolder.CanAnySubunitPerform("Attack", 90, undefined)); +TS_ASSERT(!cmpTurretHolder.CanAnySubunitPerform("Attack", 91, undefined)); +DeleteMock(archerID, IID_UnitAI); +TS_ASSERT(cmpTurretHolder.LeaveTurretPoint(archerID)); // ToDo: Ownership changes are handled by GarrisonHolder.js. 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,18 +1,24 @@ - - units/heroes/brit_hero_boudicca - + 6.0 + + 1 + Unit + 0 + Infantry + 0 + 2 + brit female Boudicca Boudica - Chariot + Chariot -Hero units/brit_hero_boudicca.png @@ -22,6 +28,18 @@ + + + + + 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();