1
1
forked from 0ad/0ad

Move the ability to hold a turret to a separate file.

The logic concerning visible garrison points (i.e. turrets) is moved
from `GarrisonHolder` to a separate file.
This is logical because garrisoned != turreted, so this allows for
turrets that cannot be garrisoned (refs. D1958).
Also references #3488.

Differential Revision: D2367
Reviewed by: @wraitii
This was SVN commit r23856.
This commit is contained in:
Freagarach 2020-07-20 10:51:14 +00:00
parent e07f12bea6
commit 215c503e30
37 changed files with 588 additions and 392 deletions

View File

@ -30,13 +30,27 @@ BuildingAI.prototype.Init = function()
this.targetUnits = [];
};
BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
BuildingAI.prototype.OnGarrisonedUnitsChanged = function()
{
this.RecalculateProjectileCount();
};
BuildingAI.prototype.OnTurretsChanged = function()
{
this.RecalculateProjectileCount();
};
BuildingAI.prototype.RecalculateProjectileCount = function()
{
this.archersGarrisoned = 0;
let classes = this.template.GarrisonArrowClasses;
for (let ent of msg.added)
let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
for (let ent of cmpGarrisonHolder.GetEntities())
{
if (msg.visible[ent])
// Only count non-visible garrisoned entities towards extra arrows.
if (cmpTurretHolder && cmpTurretHolder.OccupiesTurret(ent))
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
@ -46,19 +60,6 @@ BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
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;
}
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)

View File

@ -31,39 +31,6 @@ GarrisonHolder.prototype.Schema =
"<element name='Pickup' a:help='This garrisonHolder will move to pick up units to be garrisoned'>" +
"<data type='boolean'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='VisibleGarrisonPoints' a:help='Points that will be used to visibly garrison a unit'>" +
"<zeroOrMore>" +
"<element a:help='Element containing the offset coordinates'>" +
"<anyName/>" +
"<interleave>" +
"<element name='X'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Y'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Z'>" +
"<data type='decimal'/>" +
"</element>" +
"<optional>" +
"<element name='AllowedClasses' a:help='If specified, only entities matching the given classes will be able to use this visible garrison point.'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>"+
"<optional>" +
"<element name='Angle' a:help='Angle in degrees relative to the garrisonHolder direction'>" +
"<data type='decimal'/>" +
"</element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</zeroOrMore>" +
"</element>" +
"</optional>";
/**
@ -77,22 +44,15 @@ GarrisonHolder.prototype.Init = function()
this.entities = [];
this.timer = undefined;
this.allowGarrisoning = new Map();
this.visibleGarrisonPoints = [];
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
});
/**
* @param {number} entity - The entity to verify.
* @return {boolean} - Whether the given entity is garrisoned in this GarrisonHolder.
*/
GarrisonHolder.prototype.IsGarrisoned = function(entity)
{
return this.entities.indexOf(entity) != -1;
};
/**
@ -197,29 +157,12 @@ GarrisonHolder.prototype.IsAllowedToGarrison = function(entity)
};
/**
* @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.
* @param {number} entity - The entityID to garrison.
* @param {boolean} renamed - Whether the entity was renamed.
*
* @return {boolean} - Whether the entity was garrisoned.
*/
GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
GarrisonHolder.prototype.Garrison = function(entity, renamed = false)
{
if (!this.IsAllowedToGarrison(entity))
return false;
@ -227,10 +170,6 @@ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
if (!this.HasEnoughHealth())
return false;
let cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (!cmpPosition)
return false;
if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
@ -247,53 +186,14 @@ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.ApplyGarrisonAura(this.entity);
let visibleGarrisonPoint;
if (vgpEntity && this.AllowedToVisibleGarrisoning(entity, vgpEntity))
visibleGarrisonPoint = vgpEntity;
if (!visibleGarrisonPoint)
visibleGarrisonPoint = this.visibleGarrisonPoints.find(vgp => !vgp.entity && this.AllowedToVisibleGarrisoning(entity, vgp));
let isVisiblyGarrisoned = false;
if (visibleGarrisonPoint)
{
visibleGarrisonPoint.entity = entity;
// Angle of turrets:
// Renamed entities (vgpEntity != undefined) should keep their angle.
// Otherwise if an angle is given in the visibleGarrisonPoint, 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.
let cmpTurretPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!vgpEntity && visibleGarrisonPoint.angle != null)
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + visibleGarrisonPoint.angle);
else if (!vgpEntity && !cmpPosition.IsInWorld())
cmpPosition.SetYRotation(cmpTurretPosition.GetRotation().y + Math.PI);
let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetFacePointAfterMove(false);
cmpPosition.SetTurretParent(this.entity, visibleGarrisonPoint.offset);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
// Remove the unit's obstruction to avoid interfering with pathing.
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(false);
isVisiblyGarrisoned = true;
}
else
let cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (cmpPosition)
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,
}
"renamed": renamed
});
return true;
@ -303,9 +203,11 @@ GarrisonHolder.prototype.Garrison = function(entity, vgpEntity)
* Simply eject the unit from the garrisoning entity without moving it
* @param {number} entity - Id of the entity to be ejected.
* @param {boolean} forced - Whether eject is forced (i.e. if building is destroyed).
* @param {boolean} renamed - Whether eject was due to entity renaming.
*
* @return {boolean} Whether the entity was ejected.
*/
GarrisonHolder.prototype.Eject = function(entity, forced)
GarrisonHolder.prototype.Eject = function(entity, forced, renamed = false)
{
let entityIndex = this.entities.indexOf(entity);
// Error: invalid entity ID, usually it's already been ejected
@ -337,33 +239,13 @@ GarrisonHolder.prototype.Eject = function(entity, forced)
}
this.entities.splice(entityIndex, 1);
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)
continue;
cmpEntPosition.SetTurretParent(INVALID_ENTITY, new Vector3D());
let cmpEntUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpEntUnitMotion)
cmpEntUnitMotion.SetFacePointAfterMove(true);
if (cmpEntUnitAI)
cmpEntUnitAI.ResetTurretStance();
vgp.entity = null;
break;
}
// Reset the obstruction flags to template defaults.
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
if (cmpObstruction)
cmpObstruction.SetActive(true);
let cmpEntUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpEntUnitAI)
cmpEntUnitAI.Ungarrison();
@ -375,17 +257,21 @@ GarrisonHolder.prototype.Eject = function(entity, forced)
if (cmpEntAura && cmpEntAura.HasGarrisonAura())
cmpEntAura.RemoveGarrisonAura(this.entity);
cmpEntPosition.JumpTo(pos.x, pos.z);
cmpEntPosition.SetHeightOffset(0);
let cmpEntPosition = Engine.QueryInterface(entity, IID_Position);
if (cmpEntPosition)
{
cmpEntPosition.JumpTo(pos.x, pos.z);
cmpEntPosition.SetHeightOffset(0);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
cmpEntPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
}
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
"added": [],
"removed": [entity],
"visible": visible
"renamed": renamed
});
return true;
@ -628,16 +514,9 @@ GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
this.entities.splice(entityIndex, 1);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
"added": [],
"removed": [msg.entity],
"visible": {
[msg.entity]: this.IsVisiblyGarrisoned(msg.entity)
}
"removed": [msg.entity]
});
this.UpdateGarrisonFlag();
for (let point of this.visibleGarrisonPoints)
if (point.entity == msg.entity)
point.entity = null;
}
else if (msg.to == INVALID_PLAYER || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
this.EjectOrKill([msg.entity]);
@ -652,16 +531,19 @@ GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg)
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
let vgpRenamed;
for (let vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != msg.entity)
continue;
vgpRenamed = vgp;
break;
}
this.Eject(msg.entity, true);
this.Garrison(msg.newentity, vgpRenamed);
this.Eject(msg.entity, true, true);
this.Garrison(msg.newentity, true);
// TurretHolder is not subscribed to GarrisonChanged, so we must inform it explicitly.
// Otherwise a renaming entity may re-occupy another turret instead of its previous one,
// since the message does not know what turret point whas used, which is not wanted.
// Also ensure the TurretHolder receives the message after we process it.
// If it processes it before us we garrison a turret and subsequently
// are hidden by GarrisonHolder again.
// This could be fixed by not requiring a turret to be 'garrisoned'.
let cmpTurretHolder = Engine.QueryInterface(this.entity, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.SwapEntities(msg.entity, msg.newentity);
}
if (!this.initGarrison)
@ -722,26 +604,12 @@ GarrisonHolder.prototype.EjectOrKill = function(entities)
if (killedEntities.length)
{
let visibleEntitiesIds = {};
for (let ent of killedEntities)
visibleEntitiesIds[ent] = this.IsVisiblyGarrisoned(ent);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
"added": [],
"removed": killedEntities,
"visible": visibleEntitiesIds
"removed": killedEntities
});
this.UpdateGarrisonFlag();
}
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);
};
/**

View File

@ -359,6 +359,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);

View File

@ -0,0 +1,249 @@
/**
* This class holds the functions regarding entities being visible on
* another entity, but tied to their parents location.
* Currently renaming and changing ownership are still managed by GarrisonHolder.js,
* but in the future these components should be independent.
*/
class TurretHolder
{
Init()
{
this.turretPoints = [];
let points = this.template.TurretPoints;
for (let point in points)
this.turretPoints.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
});
}
/**
* @return {Object[]} - An array of the turret points this entity has.
*/
GetTurretPoints()
{
return this.turretPoints;
}
/**
* @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)
{
if (!turretPoint || turretPoint.entity)
return false;
if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
return false;
if (!turretPoint.allowedClasses)
return true;
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), turretPoint.allowedClasses._string);
}
/**
* 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.
*
* @return {boolean} - Whether the occupation was successful.
*/
OccupyTurret(entity, requestedTurretPoint)
{
let cmpPositionOccupant = Engine.QueryInterface(entity, IID_Position);
if (!cmpPositionOccupant)
return false;
let cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPositionSelf)
return false;
if (this.OccupiesTurret(entity))
return false;
let turretPoint;
if (requestedTurretPoint)
{
if (this.AllowedToOccupyTurret(entity, requestedTurretPoint))
turretPoint = requestedTurretPoint;
}
else
turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurret(entity, turret));
if (!turretPoint)
return false;
turretPoint.entity = entity;
// Angle of turrets:
// 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.
if (!turretPoint && turretPoint.angle != null)
cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + turretPoint.angle);
else if (!turretPoint && !cmpPosition.IsInWorld())
cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + Math.PI);
cmpPositionOccupant.SetTurretParent(this.entity, turretPoint.offset);
let cmpUnitMotion = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetFacePointAfterMove(false);
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
Engine.PostMessage(this.entity, MT_TurretsChanged, {
"added": [entity],
"removed": []
});
return true;
}
/**
* Remove the entity from a turret.
* @param {number} entity - The specific entity to eject.
* @param {Object} turret - Optionally the turret to abandon.
*
* @return {boolean} - Whether the entity was occupying a/the turret before.
*/
LeaveTurret(entity, requestedTurretPoint)
{
let turretPoint;
if (requestedTurretPoint)
{
if (requestedTurretPoint.entity == entity)
turretPoint = requestedTurretPoint;
}
else
turretPoint = this.turretPoints.find(turret => turret.entity == entity);
if (!turretPoint)
return false;
let cmpPositionEntity = Engine.QueryInterface(entity, IID_Position);
cmpPositionEntity.SetTurretParent(INVALID_ENTITY, new Vector3D());
let cmpUnitMotionEntity = Engine.QueryInterface(entity, IID_UnitMotion);
if (cmpUnitMotionEntity)
cmpUnitMotionEntity.SetFacePointAfterMove(true);
let cmpUnitAIEntity = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAIEntity)
cmpUnitAIEntity.ResetTurretStance();
turretPoint.entity = null;
Engine.PostMessage(this.entity, MT_TurretsChanged, {
"added": [],
"removed": [entity]
});
return true;
}
/**
* @param {number} entity - The entity's id.
* @param {Object} turret - Optionally the turret to check.
*
* @return {boolean} - Whether the entity is positioned on a turret of this entity.
*/
OccupiesTurret(entity, requestedTurretPoint)
{
return requestedTurretPoint ? requestedTurretPoint.entity == entity :
this.turretPoints.some(turretPoint => turretPoint.entity == entity);
}
/**
* @param {number} entity - The entity's id.
* @return {Object} - The turret this entity is positioned on, if applicable.
*/
GetOccupiedTurret(entity)
{
return this.turretPoints.find(turretPoint => turretPoint.entity == entity);
}
/**
* 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.
* That means an entity with TurretHolder should not need a GarrisonHolder
* for e.g. the garrisoning logic.
*
* @param {number} from - The entity to substitute.
* @param {number} to - The entity to subtitute with.
*/
SwapEntities(from, to)
{
let turretPoint = this.GetOccupiedTurret(from);
if (turretPoint)
this.LeaveTurret(from, turretPoint);
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder && cmpGarrisonHolder.IsGarrisoned(to))
this.OccupyTurret(to, turretPoint);
}
OnGarrisonedUnitsChanged(msg)
{
// Ignore renaming for that is handled seperately
// (i.e. called directly from GarrisonHolder.js).
if (msg.renamed)
return;
for (let entity of msg.removed)
this.LeaveTurret(entity);
for (let entity of msg.added)
this.OccupyTurret(entity);
}
}
TurretHolder.prototype.Schema =
"<element name='TurretPoints' a:help='Points that will be used to visibly garrison a unit.'>" +
"<oneOrMore>" +
"<element a:help='Element containing the offset coordinates.'>" +
"<anyName/>" +
"<interleave>" +
"<element name='X'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Y'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Z'>" +
"<data type='decimal'/>" +
"</element>" +
"<optional>" +
"<element name='AllowedClasses' a:help='If specified, only entities matching the given classes will be able to use this turret.'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>"+
"<optional>" +
"<element name='Angle' a:help='Angle in degrees relative to the turretHolder direction.'>" +
"<data type='decimal'/>" +
"</element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
"</element>";
Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);

View File

@ -0,0 +1,7 @@
Engine.RegisterInterface("TurretHolder");
/**
* Message of the form { "added": number[], "removed": number[] }
* sent from the TurretHolder component to the current entity whenever the turrets change.
*/
Engine.RegisterMessageType("TurretsChanged");

View File

@ -2,6 +2,7 @@ Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Health.js");
@ -73,11 +74,8 @@ for (let i = 24; i <= 34; ++i)
"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 => {}
});
}
@ -94,19 +92,7 @@ let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"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
}
}
"Pickup": false
});
let testGarrisonAllowed = function()
@ -182,23 +168,12 @@ cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"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
}
}
"Pickup": false
});
testGarrisonAllowed();
// Test entity renaming.
let siegeEngineId = 44;
AddMock(siegeEngineId, IID_Identity, {
"GetClassesList": () => ["Siege"]
@ -208,7 +183,6 @@ AddMock(archerId, IID_Identity, {
"GetClassesList": () => ["Infantry", "Ranged"]
});
// Test visible garrisoning restrictions.
cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"Max": 10,
"List": { "_string": "Infantry+Ranged Siege Cavalry" },
@ -216,55 +190,19 @@ cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"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" }
}
}
"Pickup": false
});
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;
@ -280,11 +218,8 @@ 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 => {}
});
@ -295,14 +230,11 @@ AddMock(cavalryId, IID_Ownership, {
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;
@ -311,14 +243,3 @@ cmpGarrisonHolder.OnGlobalOwnershipChanged({
"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);

View File

@ -30,6 +30,7 @@ Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");

View File

@ -0,0 +1,141 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("TurretHolder.js");
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => id
});
const player = 1;
const enemyPlayer = 2;
const alliedPlayer = 3;
const turretHolderID = 9;
const entitiesToTest = [10, 11, 12, 13];
AddMock(turretHolderID, IID_Ownership, {
"GetOwner": () => player
});
AddMock(turretHolderID, IID_Position, {
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"IsInWorld": () => true
});
for (let entity of entitiesToTest)
{
AddMock(entity, IID_Position, {
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"SetTurretParent": (parent, offset) => {},
"IsInWorld": () => true
});
AddMock(entity, IID_Ownership, {
"GetOwner": () => player
});
}
AddMock(player, IID_Player, {
"IsAlly": id => id != enemyPlayer,
"IsMutualAlly": id => id != enemyPlayer,
"GetPlayerID": () => player
});
AddMock(alliedPlayer, IID_Player, {
"IsAlly": id => true,
"IsMutualAlly": id => true,
"GetPlayerID": () => alliedPlayer
});
let cmpTurretHolder = ConstructComponent(turretHolderID, "TurretHolder", {
"TurretPoints": {
"archer1": {
"X": "12.0",
"Y": "5.",
"Z": "6.0"
},
"archer2": {
"X": "15.0",
"Y": "5.0",
"Z": "6.0",
"AllowedClasses": { "_string": "Siege Trader" }
},
"archer3": {
"X": "15.0",
"Y": "5.0",
"Z": "6.0",
"AllowedClasses": { "_string": "Siege Infantry+Ranged Infantry+Cavalry" }
}
}
});
let siegeEngineID = entitiesToTest[0];
AddMock(siegeEngineID, IID_Identity, {
"GetClassesList": () => ["Siege"]
});
let archerID = entitiesToTest[1];
AddMock(archerID, IID_Identity, {
"GetClassesList": () => ["Infantry", "Ranged"]
});
let cavID = entitiesToTest[2];
AddMock(cavID, IID_Identity, {
"GetClassesList": () => ["Infantry", "Cavalry"]
});
let infID = entitiesToTest[3];
AddMock(infID, IID_Identity, {
"GetClassesList": () => ["Infantry"]
});
// 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);
// Test that one cannot leave a turret that is not occupied.
TS_ASSERT(!cmpTurretHolder.LeaveTurret(archerID));
// Test occupying a turret.
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID));
TS_ASSERT(cmpTurretHolder.OccupyTurret(archerID));
TS_ASSERT(cmpTurretHolder.OccupiesTurret(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]));
// Leave turrets.
TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
TS_ASSERT(!cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[1]));
TS_ASSERT(cmpTurretHolder.LeaveTurret(cavID, cmpTurretHolder.turretPoints[2]));
// Test renaming.
AddMock(turretHolderID, IID_GarrisonHolder, {
"IsGarrisoned": () => true
});
TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[2]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
TS_ASSERT(cmpTurretHolder.LeaveTurret(archerID));
// Renaming into an entity not allowed on the same turret point hides us.
TS_ASSERT(cmpTurretHolder.OccupyTurret(siegeEngineID, cmpTurretHolder.turretPoints[1]));
cmpTurretHolder.SwapEntities(siegeEngineID, archerID);
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(siegeEngineID));
TS_ASSERT(!cmpTurretHolder.OccupiesTurret(archerID));
// ToDo: Ownership changes are handled by GarrisonHolder.js.

View File

@ -4,8 +4,8 @@
<Square width="37" depth="6"/>
<Height>9.0</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -31,8 +31,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>athen</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="6"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -21,8 +21,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>athen</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="7"/>
<Height>10.3</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.3</Y>
@ -31,8 +31,8 @@
<Y>9.3</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>brit</Civ>
<SpecificName>Rate</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="7"/>
<Height>10.3</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.3</Y>
@ -21,8 +21,8 @@
<Y>9.3</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>brit</Civ>
<SpecificName>Rate</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="8"/>
<Height>13</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>12</Y>
@ -31,8 +31,8 @@
<Y>12</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>cart</Civ>
<SpecificName>Homah</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="8"/>
<Height>13</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>12</Y>
@ -21,8 +21,8 @@
<Y>12</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>cart</Civ>
<SpecificName>Homah</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="7"/>
<Height>10.3</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.3</Y>
@ -31,8 +31,8 @@
<Y>9.3</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>gaul</Civ>
<SpecificName>Rate</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="7"/>
<Height>10.3</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.3</Y>
@ -21,8 +21,8 @@
<Y>9.3</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>gaul</Civ>
<SpecificName>Rate</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="8"/>
<Height>10</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9</Y>
@ -31,8 +31,8 @@
<Y>9</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>iber</Civ>
<SpecificName>Zabal Horma</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="8"/>
<Height>10</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9</Y>
@ -21,8 +21,8 @@
<Y>9</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>iber</Civ>
<SpecificName>Zabal Horma</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="6"/>
<Height>12.6</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.6</Y>
@ -31,8 +31,8 @@
<Y>11.6</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>kush</Civ>
<SpecificName>sbty</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="6"/>
<Height>12.6</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.6</Y>
@ -21,8 +21,8 @@
<Y>11.6</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>kush</Civ>
<SpecificName>sbty</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="6"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -31,8 +31,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>mace</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="6"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -21,8 +21,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>mace</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -23,7 +23,9 @@
<GarrisonHolder>
<List>Infantry+Archer</List>
<Max>20</Max>
<VisibleGarrisonPoints>
</GarrisonHolder>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>2</X><Y>12.5</Y><Z>0</Z>
</Archer1>
@ -72,8 +74,8 @@
<Archer16>
<X>-2.1</X><Y>18.0</Y><Z>-2.1</Z>
</Archer16>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Health>
<Max>1200</Max>
</Health>

View File

@ -10,8 +10,8 @@
<Square width="37" depth="5"/>
<Height>10.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.5</Y>
@ -37,8 +37,8 @@
<Y>9.5</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>maur</Civ>
<SpecificName>Shilabanda</SpecificName>

View File

@ -10,8 +10,8 @@
<Square width="25" depth="5"/>
<Height>10.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.5</Y>
@ -27,8 +27,8 @@
<Y>9.5</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>maur</Civ>
<SpecificName>Shilabanda</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="7"/>
<Height>11.6</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>10.6</Y>
@ -31,8 +31,8 @@
<Y>10.6</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>pers</Civ>
<SpecificName>Para</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="7"/>
<Height>11.6</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>10.6</Y>
@ -21,8 +21,8 @@
<Y>10.6</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>pers</Civ>
<SpecificName>Para</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="39" depth="6"/>
<Height>10.8</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.8</Y>
@ -31,8 +31,8 @@
<Y>9.8</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>ptol</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="26" depth="6"/>
<Height>10.8</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>9.8</Y>
@ -21,8 +21,8 @@
<Y>9.8</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>ptol</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -23,8 +23,8 @@
<Square width="37" depth="5"/>
<Height>6.7</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>5.7</Y>
@ -50,8 +50,8 @@
<Y>5.7</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Health>
<Max op="mul">0.75</Max>
</Health>

View File

@ -23,8 +23,8 @@
<Square width="25" depth="5"/>
<Height>6.7</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>5.7</Y>
@ -40,8 +40,8 @@
<Y>5.7</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Health>
<Max op="mul">0.75</Max>
</Health>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="9"/>
<Height>9.9</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>8.9</Y>
@ -31,8 +31,8 @@
<Y>8.9</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>rome</Civ>
<SpecificName>Moenia</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="9"/>
<Height>9.9</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>8.9</Y>
@ -21,8 +21,8 @@
<Y>8.9</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>rome</Civ>
<SpecificName>Moenia</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="35" depth="6"/>
<Height>11.4</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>10.4</Y>
@ -31,8 +31,8 @@
<Y>10.4</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>sele</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="22" depth="6"/>
<Height>11.4</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>10.4</Y>
@ -21,8 +21,8 @@
<Y>10.4</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>sele</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="37" depth="6"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -31,8 +31,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer5>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>spart</Civ>
<SpecificName>Teichos</SpecificName>

View File

@ -4,8 +4,8 @@
<Square width="25" depth="6"/>
<Height>12.5</Height>
</Footprint>
<GarrisonHolder>
<VisibleGarrisonPoints>
<TurretHolder>
<TurretPoints>
<Archer1>
<X>0</X>
<Y>11.5</Y>
@ -21,8 +21,8 @@
<Y>11.5</Y>
<Z>0</Z>
</Archer3>
</VisibleGarrisonPoints>
</GarrisonHolder>
</TurretPoints>
</TurretHolder>
<Identity>
<Civ>spart</Civ>
<SpecificName>Teichos</SpecificName>