Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js (revision 23856)
@@ -1,387 +1,388 @@
// Number of rounds of firing per 2 seconds.
const roundCount = 10;
const attackType = "Ranged";
function BuildingAI() {}
BuildingAI.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
BuildingAI.prototype.Init = function()
{
this.currentRound = 0;
this.archersGarrisoned = 0;
this.arrowsLeft = 0;
this.targetUnits = [];
};
-BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
+BuildingAI.prototype.OnGarrisonedUnitsChanged = function()
{
- let classes = this.template.GarrisonArrowClasses;
-
- for (let ent of msg.added)
- {
- if (msg.visible[ent])
- continue;
+ this.RecalculateProjectileCount();
+};
- let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
- if (!cmpIdentity)
- continue;
+BuildingAI.prototype.OnTurretsChanged = function()
+{
+ this.RecalculateProjectileCount();
+};
- if (MatchesClassList(cmpIdentity.GetClassesList(), classes))
- ++this.archersGarrisoned;
- }
+BuildingAI.prototype.RecalculateProjectileCount = function()
+{
+ this.archersGarrisoned = 0;
+ let classes = this.template.GarrisonArrowClasses;
- for (let ent of msg.removed)
+ 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);
if (!cmpIdentity)
continue;
if (MatchesClassList(cmpIdentity.GetClassesList(), classes))
- --this.archersGarrisoned;
+ ++this.archersGarrisoned;
}
};
BuildingAI.prototype.OnOwnershipChanged = function(msg)
{
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
BuildingAI.prototype.OnDiplomacyChanged = function(msg)
{
if (!IsOwnedByPlayer(msg.player, this.entity))
return;
// Remove maybe now allied/neutral units
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
BuildingAI.prototype.OnDestroy = function()
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
// Clean up range queries
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
if (this.gaiaUnitsQuery)
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
};
/**
* React on Attack value modifications, as it might influence the range
*/
BuildingAI.prototype.OnValueModification = function(msg)
{
if (msg.component != "Attack")
return;
this.targetUnits = [];
this.SetupRangeQuery();
this.SetupGaiaRangeQuery();
};
/**
* Setup the Range Query to detect units coming in & out of range
*/
BuildingAI.prototype.SetupRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.enemyUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
this.enemyUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
var enemies = cmpPlayer.GetEnemies();
if (enemies.length && enemies[0] == 0)
enemies.shift(); // remove gaia
if (!enemies.length)
return;
var range = cmpAttack.GetRange(attackType);
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
};
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
BuildingAI.prototype.SetupGaiaRangeQuery = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.gaiaUnitsQuery)
{
cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
this.gaiaUnitsQuery = undefined;
}
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
return;
var range = cmpAttack.GetRange(attackType);
// This query is only interested in Gaia entities that can attack.
this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
[0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
};
/**
* Called when units enter or leave range
*/
BuildingAI.prototype.OnRangeUpdate = function(msg)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
// Target enemy units except non-dangerous animals
if (msg.tag == this.gaiaUnitsQuery)
{
msg.added = msg.added.filter(e => {
let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
});
}
else if (msg.tag != this.enemyUnitsQuery)
return;
// Add new targets
for (let entity of msg.added)
if (cmpAttack.CanAttack(entity))
this.targetUnits.push(entity);
// Remove targets outside of vision-range
for (let entity of msg.removed)
{
let index = this.targetUnits.indexOf(entity);
if (index > -1)
this.targetUnits.splice(index, 1);
}
if (this.targetUnits.length)
this.StartTimer();
};
BuildingAI.prototype.StartTimer = function()
{
if (this.timer)
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var attackTimers = cmpAttack.GetTimers(attackType);
this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
attackTimers.prepare, attackTimers.repeat / roundCount, null);
};
BuildingAI.prototype.GetDefaultArrowCount = function()
{
var arrowCount = +this.template.DefaultArrowCount;
return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity));
};
BuildingAI.prototype.GetMaxArrowCount = function()
{
if (!this.template.MaxArrowCount)
return Infinity;
let maxArrowCount = +this.template.MaxArrowCount;
return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity));
};
BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
{
var arrowMult = +this.template.GarrisonArrowMultiplier;
return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
};
BuildingAI.prototype.GetGarrisonArrowClasses = function()
{
var string = this.template.GarrisonArrowClasses;
if (string)
return string.split(/\s+/);
return [];
};
/**
* Returns the number of arrows which needs to be fired.
* DefaultArrowCount + Garrisoned Archers(ie., any unit capable
* of shooting arrows from inside buildings)
*/
BuildingAI.prototype.GetArrowCount = function()
{
let count = this.GetDefaultArrowCount() +
Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier());
return Math.min(count, this.GetMaxArrowCount());
};
BuildingAI.prototype.SetUnitAITarget = function(ent)
{
this.unitAITarget = ent;
if (ent)
this.StartTimer();
};
/**
* Fire arrows with random temporal distribution on prefered targets.
* Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
*/
BuildingAI.prototype.FireArrows = function()
{
if (!this.targetUnits.length && !this.unitAITarget)
{
if (!this.timer)
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return;
if (this.currentRound > roundCount - 1)
this.currentRound = 0;
if (this.currentRound == 0)
this.arrowsLeft = this.GetArrowCount();
let arrowsToFire = 0;
if (this.currentRound == roundCount - 1)
arrowsToFire = this.arrowsLeft;
else
arrowsToFire = Math.min(
randIntInclusive(0, 2 * this.GetArrowCount() / roundCount),
this.arrowsLeft
);
if (arrowsToFire <= 0)
{
++this.currentRound;
return;
}
// Add targets to a weighted list, to allow preferences
let targets = new WeightedList();
let maxPreference = this.MAX_PREFERENCE_BONUS;
let addTarget = function(target)
{
let preference = cmpAttack.GetPreference(target);
let weight = 1;
if (preference !== null && preference !== undefined)
weight += maxPreference / (1 + preference);
targets.push(target, weight);
};
// Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ
if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
addTarget(this.unitAITarget);
for (let target of this.targetUnits)
addTarget(target);
for (let i = 0; i < arrowsToFire; ++i)
{
let selectedIndex = targets.randomIndex();
let selectedTarget = targets.itemAt(selectedIndex);
if (selectedTarget && this.CheckTargetVisible(selectedTarget))
{
cmpAttack.PerformAttack(attackType, selectedTarget);
PlaySound("attack_" + attackType.toLowerCase(), this.entity);
continue;
}
// Could not attack target, retry
targets.removeAt(selectedIndex);
--i;
if (!targets.length())
{
this.arrowsLeft += arrowsToFire;
break;
}
}
this.arrowsLeft -= arrowsToFire;
this.currentRound++;
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
BuildingAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
// Entities that are hidden and miraged are considered visible
var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
return true;
// Either visible directly, or visible in fog
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden";
};
Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 23856)
@@ -1,809 +1,677 @@
function GarrisonHolder() {}
GarrisonHolder.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "tokens" +
- "" +
- "" +
- "" +
- ""+
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
- "" +
"";
/**
* Initialize GarrisonHolder Component
* Garrisoning when loading a map is set in the script of the map, by setting initGarrison
* which should contain the array of garrisoned entities.
*/
GarrisonHolder.prototype.Init = function()
{
// Garrisoned Units
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;
};
/**
* @return {Object} max and min range at which entities can garrison the holder.
*/
GarrisonHolder.prototype.GetLoadingRange = function()
{
return { "max": +this.template.LoadingRange, "min": 0 };
};
GarrisonHolder.prototype.CanPickup = function(ent)
{
if (!this.template.Pickup || this.IsFull())
return false;
let cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
};
GarrisonHolder.prototype.GetEntities = function()
{
return this.entities;
};
/**
* @return {Array} unit classes which can be garrisoned inside this
* particular entity. Obtained from the entity's template.
*/
GarrisonHolder.prototype.GetAllowedClasses = function()
{
return this.template.List._string;
};
GarrisonHolder.prototype.GetCapacity = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
};
GarrisonHolder.prototype.IsFull = function()
{
return this.GetGarrisonedEntitiesCount() >= this.GetCapacity();
};
GarrisonHolder.prototype.GetHealRate = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity);
};
/**
* Set this entity to allow or disallow garrisoning in the entity.
* Every component calling this function should do it with its own ID, and as long as one
* component doesn't allow this entity to garrison, it can't be garrisoned
* When this entity already contains garrisoned soldiers,
* these will not be able to ungarrison until the flag is set to true again.
*
* This more useful for modern-day features. For example you can't garrison or ungarrison
* a driving vehicle or plane.
* @param {boolean} allow - Whether the entity should be garrisonable.
*/
GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID)
{
this.allowGarrisoning.set(callerID, allow);
};
GarrisonHolder.prototype.IsGarrisoningAllowed = function()
{
return Array.from(this.allowGarrisoning.values()).every(allow => allow);
};
GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function()
{
let count = this.entities.length;
for (let ent of this.entities)
{
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
count += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
}
return count;
};
GarrisonHolder.prototype.IsAllowedToGarrison = function(entity)
{
if (!this.IsGarrisoningAllowed())
return false;
if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
return false;
let extraCount = 0;
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity())
return false;
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return false;
let entityClasses = cmpIdentity.GetClassesList();
return MatchesClassList(entityClasses, this.template.List._string) && !!Engine.QueryInterface(entity, IID_Garrisonable);
};
/**
- * @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;
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);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
this.entities.push(entity);
this.UpdateGarrisonFlag();
let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.PauseProduction();
let cmpAura = Engine.QueryInterface(entity, IID_Auras);
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;
};
/**
* 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
if (entityIndex == -1)
return false;
// Find spawning location
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
// If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities
// TODO: should use passability classes to be more generic
let pos;
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
// Error: couldn't find space satisfying the unit's passability criteria
if (!forced)
return false;
// If ejection is forced, we need to continue, so use center of the building
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
pos = cmpPosition.GetPosition();
}
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();
let cmpEntProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpEntProductionQueue)
cmpEntProductionQueue.UnpauseProduction();
let cmpEntAura = Engine.QueryInterface(entity, IID_Auras);
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;
};
/**
* Ejects units and orders them to move to the rally point. If an ejection
* with a given obstruction radius has failed, we won't try anymore to eject
* entities with a bigger obstruction as that is compelled to also fail.
* @param {Array} entities - An array containing the ids of the entities to eject.
* @param {boolean} forced - Whether eject is forced (ie: if building is destroyed).
* @return {boolean} Whether the entities were ejected.
*/
GarrisonHolder.prototype.PerformEject = function(entities, forced)
{
if (!this.IsGarrisoningAllowed() && !forced)
return false;
let ejectedEntities = [];
let success = true;
let failedRadius;
let radius;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
for (let entity of entities)
{
if (failedRadius !== undefined)
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
radius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
if (radius >= failedRadius)
continue;
}
if (this.Eject(entity, forced))
{
let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner())
ejectedEntities.push(entity);
}
else
{
success = false;
if (failedRadius !== undefined)
failedRadius = Math.min(failedRadius, radius);
else
{
let cmpObstruction = Engine.QueryInterface(entity, IID_Obstruction);
failedRadius = cmpObstruction ? cmpObstruction.GetUnitRadius() : 0;
}
}
}
this.OrderWalkToRallyPoint(ejectedEntities);
this.UpdateGarrisonFlag();
return success;
};
/**
* Order entities to walk to the rally point.
* @param {Array} entities - An array containing all the ids of the entities.
*/
GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (!cmpRallyPoint || !cmpRallyPoint.GetPositions()[0])
return;
let commands = GetRallyPointCommands(cmpRallyPoint, entities);
// Ignore the rally point if it is autogarrison
if (commands[0].type == "garrison" && commands[0].target == this.entity)
return;
for (let command of commands)
ProcessCommand(cmpOwnership.GetOwner(), command);
};
/**
* Unload unit from the garrisoning entity and order them
* to move to the rally point.
* @return {boolean} Whether the command was successful.
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
return this.PerformEject([entity], forced);
};
/**
* Unload one or all units that match a template and owner from
* the garrisoning entity and order them to move to the rally point.
* @param {string} template - Type of units that should be ejected.
* @param {number} owner - Id of the player whose units should be ejected.
* @param {boolean} all - Whether all units should be ejected.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all, forced)
{
let entities = [];
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let entity of this.entities)
{
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
// Units with multiple ranks are grouped together.
let name = cmpIdentity.GetSelectionGroupName() || cmpTemplateManager.GetCurrentTemplateName(entity);
if (name != template || owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner())
continue;
entities.push(entity);
// If 'all' is false, only ungarrison the first matched unit.
if (!all)
break;
}
return this.PerformEject(entities, forced);
};
/**
* Unload all units, that belong to certain player
* and order all own units to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @param {number} owner - Id of the player whose units should be ejected.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAllByOwner = function(owner, forced)
{
let entities = this.entities.filter(ent => {
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
return cmpOwnership && cmpOwnership.GetOwner() == owner;
});
return this.PerformEject(entities, forced);
};
/**
* Unload all units from the entity and order them to move to the rally point.
* @param {boolean} forced - Whether unload is forced.
* @return {boolean} Whether the unloading was successful.
*/
GarrisonHolder.prototype.UnloadAll = function(forced)
{
return this.PerformEject(this.entities.slice(), forced);
};
/**
* Used to check if the garrisoning entity's health has fallen below
* a certain limit after which all garrisoned units are unloaded.
*/
GarrisonHolder.prototype.OnHealthChanged = function(msg)
{
if (!this.HasEnoughHealth() && this.entities.length)
this.EjectOrKill(this.entities.slice());
};
GarrisonHolder.prototype.HasEnoughHealth = function()
{
// 0 is a valid value so explicitly check for undefined.
if (this.template.EjectHealth === undefined)
return true;
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
return !cmpHealth || cmpHealth.GetHitpoints() > Math.floor(+this.template.EjectHealth * cmpHealth.GetMaxHitpoints());
};
/**
* Called every second. Heals garrisoned units.
*/
GarrisonHolder.prototype.HealTimeout = function(data)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (!this.entities.length)
{
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
return;
}
for (let entity of this.entities)
{
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth && !cmpHealth.IsUnhealable())
cmpHealth.Increase(this.GetHealRate());
}
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
};
/**
* Updates the garrison flag depending whether something is garrisoned in the entity.
*/
GarrisonHolder.prototype.UpdateGarrisonFlag = function()
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetVariant("garrison", this.entities.length ? "garrisoned" : "ungarrisoned");
};
/**
* Cancel timer when destroyed.
*/
GarrisonHolder.prototype.OnDestroy = function()
{
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/**
* If a garrisoned entity is captured, or about to be killed (so its owner changes to '-1'),
* remove it from the building so we only ever contain valid entities.
*/
GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
{
// 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 (entities.length)
this.EjectOrKill(entities);
return;
}
// or on some of its garrisoned units
let entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
// If the entity is dead, remove it directly instead of ejecting the corpse
let cmpHealth = Engine.QueryInterface(msg.entity, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() == 0)
{
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]);
}
};
/**
* Update list of garrisoned entities if one gets renamed (e.g. by promotion).
*/
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)
return;
// Update the pre-game garrison because of SkirmishReplacement
if (msg.entity == this.entity)
{
let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
cmpGarrisonHolder.initGarrison = this.initGarrison;
}
else
{
entityIndex = this.initGarrison.indexOf(msg.entity);
if (entityIndex != -1)
this.initGarrison[entityIndex] = msg.newentity;
}
};
/**
* Eject all foreign garrisoned entities which are no more allied.
*/
GarrisonHolder.prototype.OnDiplomacyChanged = function()
{
this.EjectOrKill(this.entities.filter(ent => !IsOwnedByMutualAllyOfEntity(this.entity, ent)));
};
/**
* Eject or kill a garrisoned unit which can no more be garrisoned
* (garrisonholder's health too small or ownership changed).
*/
GarrisonHolder.prototype.EjectOrKill = function(entities)
{
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Eject the units which can be ejected (if not in world, it generally means this holder
// is inside a holder which kills its entities, so do not eject)
if (cmpPosition && cmpPosition.IsInWorld())
{
let ejectables = entities.filter(ent => this.IsEjectable(ent));
if (ejectables.length)
this.PerformEject(ejectables, false);
}
// And destroy all remaining entities
let killedEntities = [];
for (let entity of entities)
{
let entityIndex = this.entities.indexOf(entity);
if (entityIndex == -1)
continue;
let cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
this.entities.splice(entityIndex, 1);
killedEntities.push(entity);
}
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);
};
/**
* Whether an entity is ejectable.
* @param {number} entity - The entity-ID to be tested.
* @return {boolean} - Whether the entity is ejectable.
*/
GarrisonHolder.prototype.IsEjectable = function(entity)
{
if (!this.entities.find(ent => ent == entity))
return false;
let ejectableClasses = this.template.EjectClassesOnDestroy._string;
let entityClasses = Engine.QueryInterface(entity, IID_Identity).GetClassesList();
return MatchesClassList(entityClasses, ejectableClasses);
};
/**
* Sets the intitGarrison to the specified entities. Used by the mapreader.
*
* @param {number[]} entities - The entity IDs to garrison on init.
*/
GarrisonHolder.prototype.SetInitGarrison = function(entities)
{
this.initGarrison = clone(entities);
};
/**
* Initialise the garrisoned units.
*/
GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
{
if (!this.initGarrison)
return;
for (let ent of this.initGarrison)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.CanGarrison(this.entity) && this.Garrison(ent))
cmpUnitAI.Autogarrison(this.entity);
}
this.initGarrison = undefined;
};
GarrisonHolder.prototype.OnValueModification = function(msg)
{
if (msg.component != "GarrisonHolder" || msg.valueNames.indexOf("GarrisonHolder/BuffHeal") == -1)
return;
if (this.timer && this.GetHealRate() == 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
else if (!this.timer && this.GetHealRate() > 0)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
};
Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 23856)
@@ -1,2038 +1,2044 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"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);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_Resistance);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths("Damage");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"hp": cmpHeal.GetHP(),
"range": cmpHeal.GetRange().max,
"rate": cmpHeal.GetRate(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetTimer(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
aurasTemplate[name] = AuraTemplates.Get(name);
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player !== undefined ? data.player : player).GetNeededResources(data.cost);
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "populationBonus": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
return QueryPlayerIDInterface(player).GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
11.6
-
-
+
+
0
10.6
0
8
10.6
0
-8
10.6
0
4
10.6
0
-4
10.6
0
-
-
+
+
pers
Para
12.6
structures/persians/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_long.xml (revision 23856)
@@ -1,52 +1,52 @@
10.8
-
-
+
+
0
9.8
0
8
9.8
0
-8
9.8
0
4
9.8
0
-4
9.8
0
-
-
+
+
ptol
Teichos
11.8
structures/ptolemies/wall_long.xml
38
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_long.xml (revision 23856)
@@ -1,91 +1,91 @@
15
35
5
4
7
3
own neutral enemy
60
0
6.7
-
-
+
+
0
5.7
0
8
5.7
0
-8
5.7
0
4
5.7
0
-4
5.7
0
-
-
+
+
0.75
rome
structures/rome_wallset_siege
Siege Wall
Mūrus Circummūnītiōnis
SiegeWall
structures/siege_wall.png
A wooden and turf palisade buildable in enemy and neutral territories.
12
0
7.7
structures/rome_siege_wall_gate
80
0
upgrading
structures/romans/siege_wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_medium.xml (revision 23856)
@@ -1,42 +1,42 @@
11.4
-
-
+
+
0
10.4
0
4
10.4
0
-4
10.4
0
-
-
+
+
sele
Teichos
12.4
structures/seleucids/wall_medium.xml
21
Index: ps/trunk/binaries/data/mods/public/simulation/components/TurretHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TurretHolder.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TurretHolder.js (revision 23856)
@@ -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 =
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "tokens" +
+ "" +
+ "" +
+ "" +
+ ""+
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "";
+
+Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/TurretHolder.js (revision 23856)
@@ -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");
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 23856)
@@ -1,324 +1,245 @@
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");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33];
const garrisonHolderId = 15;
const unitToGarrisonId = 24;
const enemyUnitId = 34;
const player = 1;
const friendlyPlayer = 2;
const enemyPlayer = 3;
AddMock(garrisonHolderId, IID_Footprint, {
"PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30),
"PickSpawnPoint": entity => new Vector3D(4, 3, 30)
});
AddMock(garrisonHolderId, IID_Ownership, {
"GetOwner": () => player
});
AddMock(player, IID_Player, {
"IsAlly": id => id != enemyPlayer,
"IsMutualAlly": id => id != enemyPlayer,
"GetPlayerID": () => player
});
AddMock(friendlyPlayer, IID_Player, {
"IsAlly": id => true,
"IsMutualAlly": id => true,
"GetPlayerID": () => friendlyPlayer
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetTimeout": (ent, iid, funcname, time, data) => 1
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => id
});
for (let i = 24; i <= 34; ++i)
{
AddMock(i, IID_Identity, {
"GetClassesList": () => ["Infantry", "Cavalry"],
"GetSelectionGroupName": () => "mace_infantry_archer_a"
});
if (i < 28)
AddMock(i, IID_Ownership, {
"GetOwner": () => player
});
else if (i == 34)
AddMock(i, IID_Ownership, {
"GetOwner": () => enemyPlayer
});
else
AddMock(i, IID_Ownership, {
"GetOwner": () => friendlyPlayer
});
AddMock(i, IID_Garrisonable, {});
AddMock(i, 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 => {}
});
}
AddMock(33, IID_Identity, {
"GetClassesList": () => ["Infantry", "Cavalry"],
"GetSelectionGroupName": () => "spart_infantry_archer_a"
});
let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"Max": 10,
"List": { "_string": "Infantry+Cavalry" },
"EjectHealth": 0.1,
"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()
{
TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(enemyUnitId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(unitToGarrisonId), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true);
for (let entity of garrisonedEntitiesList)
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(entity), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, false, false), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27, 28, 29, 30, 31, 32]);
TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer, false), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27]);
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 4);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([25], false), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([], false), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 26, 27]);
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 3);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
};
// No health component yet.
testGarrisonAllowed();
AddMock(garrisonHolderId, IID_Health, {
"GetHitpoints": () => 50,
"GetMaxHitpoints": () => 600
});
cmpGarrisonHolder.AllowGarrisoning(true, "callerID1");
cmpGarrisonHolder.AllowGarrisoning(false, 5);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), false);
cmpGarrisonHolder.AllowGarrisoning(true, 5);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetLoadingRange(), { "max": 2.1, "min": 0 });
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetHealRate(), 1);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses(), "Infantry+Cavalry");
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetCapacity(), 10);
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0);
TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(enemyUnitId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(enemyUnitId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(unitToGarrisonId), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false);
AddMock(garrisonHolderId, IID_Health, {
"GetHitpoints": () => 600,
"GetMaxHitpoints": () => 600
});
// No eject health.
cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"Max": 10,
"List": { "_string": "Infantry+Cavalry" },
"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"]
});
let archerId = 45;
AddMock(archerId, IID_Identity, {
"GetClassesList": () => ["Infantry", "Ranged"]
});
-// Test visible garrisoning restrictions.
cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"Max": 10,
"List": { "_string": "Infantry+Ranged Siege Cavalry" },
"EjectHealth": 0.1,
"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;
AddMock(siegeEngineId, IID_Ownership, {
"GetOwner": () => currentSiegePlayer
});
AddMock(siegeEngineId, IID_Garrisonable, {});
let cavalryId = 46;
AddMock(cavalryId, IID_Identity, {
"GetClassesList": () => ["Infantry", "Ranged"]
});
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 => {}
});
let currentCavalryPlayer = player;
AddMock(cavalryId, IID_Ownership, {
"GetOwner": () => currentCavalryPlayer
});
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;
cmpGarrisonHolder.OnGlobalOwnershipChanged({
"entity": cavalryId,
"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);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js (revision 23856)
@@ -1,596 +1,597 @@
Engine.LoadHelperScript("ObstructionSnap.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/AlertRaiser.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Gate.js");
Engine.LoadComponentScript("interfaces/Guard.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/Loot.js");
Engine.LoadComponentScript("interfaces/Market.js");
Engine.LoadComponentScript("interfaces/Pack.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
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");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("GuiInterface.js");
Resources = {
"GetCodes": () => ["food", "metal", "stone", "wood"],
"GetNames": () => ({
"food": "Food",
"metal": "Metal",
"stone": "Stone",
"wood": "Wood"
}),
"GetResource": resource => ({
"aiAnalysisInfluenceGroup":
resource == "food" ? "ignore" :
resource == "wood" ? "abundant" : "sparse"
})
};
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
AddMock(SYSTEM_ENTITY, IID_Barter, {
"GetPrices": function() {
return {
"buy": { "food": 150 },
"sell": { "food": 25 }
};
},
"PlayerHasMarket": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_EndGameManager, {
"GetVictoryConditions": () => ["conquest", "wonder"],
"GetAlliedVictory": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetNumPlayers": function() { return 2; },
"GetPlayerByID": function(id) { TS_ASSERT(id === 0 || id === 1); return 100 + id; }
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"GetLosVisibility": function(ent, player) { return "visible"; },
"GetLosCircular": function() { return false; }
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"GetCurrentTemplateName": function(ent) { return "example"; },
"GetTemplate": function(name) { return ""; }
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"GetTime": function() { return 0; },
"SetTimeout": function(ent, iid, funcname, time, data) { return 0; }
});
AddMock(100, IID_Player, {
"GetName": function() { return "Player 1"; },
"GetCiv": function() { return "gaia"; },
"GetColor": function() { return { "r": 1, "g": 1, "b": 1, "a": 1 }; },
"CanControlAllUnits": function() { return false; },
"GetPopulationCount": function() { return 10; },
"GetPopulationLimit": function() { return 20; },
"GetMaxPopulation": function() { return 200; },
"GetResourceCounts": function() { return { "food": 100 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() { return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return false; },
"IsMutualAlly": function() { return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return true; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(100, IID_EntityLimits, {
"GetLimits": function() { return { "Foo": 10 }; },
"GetCounts": function() { return { "Foo": 5 }; },
"GetLimitChangers": function() {return { "Foo": {} }; }
});
AddMock(100, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(100, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
AddMock(101, IID_Player, {
"GetName": function() { return "Player 2"; },
"GetCiv": function() { return "mace"; },
"GetColor": function() { return { "r": 1, "g": 0, "b": 0, "a": 1 }; },
"CanControlAllUnits": function() { return true; },
"GetPopulationCount": function() { return 40; },
"GetPopulationLimit": function() { return 30; },
"GetMaxPopulation": function() { return 300; },
"GetResourceCounts": function() { return { "food": 200 }; },
"GetPanelEntities": function() { return []; },
"IsTrainingBlocked": function() { return false; },
"GetState": function() { return "active"; },
"GetTeam": function() { return -1; },
"GetLockTeams": function() {return false; },
"GetCheatsEnabled": function() { return false; },
"GetDiplomacy": function() { return [-1, 1]; },
"IsAlly": function() { return true; },
"IsMutualAlly": function() {return false; },
"IsNeutral": function() { return false; },
"IsEnemy": function() { return false; },
"GetDisabledTemplates": function() { return {}; },
"GetDisabledTechnologies": function() { return {}; },
"GetSpyCostMultiplier": function() { return 1; },
"HasSharedDropsites": function() { return false; },
"HasSharedLos": function() { return false; }
});
AddMock(101, IID_EntityLimits, {
"GetLimits": function() { return { "Bar": 20 }; },
"GetCounts": function() { return { "Bar": 0 }; },
"GetLimitChangers": function() {return { "Bar": {} }; }
});
AddMock(101, IID_TechnologyManager, {
"IsTechnologyResearched": tech => tech == "phase_village",
"GetQueuedResearch": () => new Map(),
"GetStartedTechs": () => new Set(),
"GetResearchedTechs": () => new Set(),
"GetClassCounts": () => ({}),
"GetTypeCountsByClass": () => ({})
});
AddMock(101, IID_StatisticsTracker, {
"GetBasicStatistics": function() {
return {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
};
},
"GetSequences": function() {
return {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
};
},
"IncreaseTrainedUnitsCounter": function() { return 1; },
"IncreaseConstructedBuildingsCounter": function() { return 1; },
"IncreaseBuiltCivCentresCounter": function() { return 1; }
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false
});
TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
"players": [
{
"name": "Player 1",
"civ": "gaia",
"color": { "r": 1, "g": 1, "b": 1, "a": 1 },
"controlsAll": false,
"popCount": 10,
"popLimit": 20,
"popMax": 200,
"panelEntities": [],
"resourceCounts": { "food": 100 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [false, false],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [true, true],
"entityLimits": { "Foo": 10 },
"entityCounts": { "Foo": 5 },
"entityLimitChangers": { "Foo": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 42],
"buildingsConstructed": [1, 3],
"buildingsCaptured": [3, 7],
"buildingsLost": [3, 10],
"civCentresBuilt": [4, 10],
"resourcesGathered": {
"food": [5, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [1, 20],
"lootCollected": [0, 2],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
},
{
"name": "Player 2",
"civ": "mace",
"color": { "r": 1, "g": 0, "b": 0, "a": 1 },
"controlsAll": true,
"popCount": 40,
"popLimit": 30,
"popMax": 300,
"panelEntities": [],
"resourceCounts": { "food": 200 },
"trainingBlocked": false,
"state": "active",
"team": -1,
"teamsLocked": false,
"cheatsEnabled": false,
"disabledTemplates": {},
"disabledTechnologies": {},
"hasSharedDropsites": false,
"hasSharedLos": false,
"spyCostMultiplier": 1,
"phase": "village",
"isAlly": [true, true],
"isMutualAlly": [false, false],
"isNeutral": [false, false],
"isEnemy": [false, false],
"entityLimits": { "Bar": 20 },
"entityCounts": { "Bar": 0 },
"entityLimitChangers": { "Bar": {} },
"researchQueued": new Map(),
"researchStarted": new Set(),
"researchedTechs": new Set(),
"classCounts": {},
"typeCountsByClass": {},
"canBarter": false,
"barterPrices": {
"buy": { "food": 150 },
"sell": { "food": 25 }
},
"statistics": {
"resourcesGathered": {
"food": 100,
"wood": 0,
"metal": 0,
"stone": 0,
"vegetarianFood": 0
},
"percentMapExplored": 10
},
"sequences": {
"unitsTrained": [0, 10],
"unitsLost": [0, 9],
"buildingsConstructed": [0, 5],
"buildingsCaptured": [0, 7],
"buildingsLost": [0, 4],
"civCentresBuilt": [0, 1],
"resourcesGathered": {
"food": [0, 100],
"wood": [0, 0],
"metal": [0, 0],
"stone": [0, 0],
"vegetarianFood": [0, 0]
},
"treasuresCollected": [0, 0],
"lootCollected": [0, 0],
"percentMapExplored": [0, 10],
"teamPercentMapExplored": [0, 10],
"percentMapControlled": [0, 10],
"teamPercentMapControlled": [0, 10],
"peakPercentOfMapControlled": [0, 10],
"teamPeakPercentOfMapControlled": [0, 10]
}
}
],
"circularMap": false,
"timeElapsed": 0,
"victoryConditions": ["conquest", "wonder"],
"alliedVictory": false
});
AddMock(10, IID_Builder, {
"GetEntitiesList": function() {
return ["test1", "test2"];
},
});
AddMock(10, IID_Health, {
"GetHitpoints": function() { return 50; },
"GetMaxHitpoints": function() { return 60; },
"IsRepairable": function() { return false; },
"IsUnhealable": function() { return false; }
});
AddMock(10, IID_Identity, {
"GetClassesList": function() { return ["class1", "class2"]; },
"GetVisibleClassesList": function() { return ["class3", "class4"]; },
"GetRank": function() { return "foo"; },
"GetSelectionGroupName": function() { return "Selection Group Name"; },
"HasClass": function() { return true; },
"IsUndeletable": function() { return false; },
"HasSomeFormation": function() { return false; },
"GetFormationsList": function() { return []; },
});
AddMock(10, IID_Position, {
"GetTurretParent": function() { return INVALID_ENTITY; },
"GetPosition": function() {
return { "x": 1, "y": 2, "z": 3 };
},
"IsInWorld": function() {
return true;
}
});
AddMock(10, IID_ResourceTrickle, {
"GetTimer": () => 1250,
"GetRates": () => ({ "food": 2, "wood": 3, "stone": 5, "metal": 9 })
});
// Note: property order matters when using TS_ASSERT_UNEVAL_EQUALS,
// because uneval preserves property order. So make sure this object
// matches the ordering in GuiInterface.
TS_ASSERT_UNEVAL_EQUALS(cmp.GetEntityState(-1, 10), {
"id": 10,
"player": INVALID_PLAYER,
"template": "example",
"identity": {
"rank": "foo",
"classes": ["class1", "class2"],
"visibleClasses": ["class3", "class4"],
"selectionGroupName": "Selection Group Name",
"canDelete": true,
"hasSomeFormation": false,
"formations": [],
},
"position": { "x": 1, "y": 2, "z": 3 },
"hitpoints": 50,
"maxHitpoints": 60,
"needsRepair": false,
"needsHeal": true,
"builder": true,
"canGarrison": false,
"visibility": "visible",
"isBarterMarket": true,
"resourceTrickle": {
"interval": 1250,
"rates": { "food": 2, "wood": 3, "stone": 5, "metal": 9 }
}
});
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TurretHolder.js (revision 23856)
@@ -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.
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
9.0
-
-
+
+
0
11.5
0
8
11.5
0
-8
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
athen
Teichos
13.5
structures/hellenes/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/athen_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
12.5
-
-
+
+
0
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
athen
Teichos
13.5
structures/hellenes/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
10.3
-
-
+
+
0
9.3
0
8
9.3
0
-8
9.3
0
4
9.3
0
-4
9.3
0
-
-
+
+
brit
Rate
11.3
structures/britons/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/brit_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
10.3
-
-
+
+
0
9.3
0
4
9.3
0
-4
9.3
0
-
-
+
+
brit
Rate
11.3
structures/britons/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_long.xml (revision 23856)
@@ -1,50 +1,50 @@
13
-
-
+
+
0
12
0
8
12
0
-8
12
0
4
12
0
-4
12
0
-
-
+
+
cart
Homah
14
structures/carthaginians/wall_long.xml
structures/carthaginians/fndn_wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/cart_wall_medium.xml (revision 23856)
@@ -1,40 +1,40 @@
13
-
-
+
+
0
12
0
4
12
0
-4
12
0
-
-
+
+
cart
Homah
14
structures/carthaginians/wall_medium.xml
structures/carthaginians/fndn_wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
10.3
-
-
+
+
0
9.3
0
8
9.3
0
-8
9.3
0
4
9.3
0
-4
9.3
0
-
-
+
+
gaul
Rate
11.3
structures/gauls/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/gaul_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
10.3
-
-
+
+
0
9.3
0
4
9.3
0
-4
9.3
0
-
-
+
+
gaul
Rate
11.3
structures/gauls/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
10
-
-
+
+
0
9
0
8
9
0
-8
9
0
4
9
0
-4
9
0
-
-
+
+
iber
Zabal Horma
11
structures/iberians/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/iber_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
10
-
-
+
+
0
9
0
4
9
0
-4
9
0
-
-
+
+
iber
Zabal Horma
11
structures/iberians/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
12.6
-
-
+
+
0
11.6
0
8
11.6
0
-8
11.6
0
4
11.6
0
-4
11.6
0
-
-
+
+
kush
sbty
13.6
structures/kushites/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/kush_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
12.6
-
-
+
+
0
11.6
0
4
11.6
0
-4
11.6
0
-
-
+
+
kush
sbty
13.6
structures/kushites/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
12.5
-
-
+
+
0
11.5
0
8
11.5
0
-8
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
mace
Teichos
13.5
structures/hellenes/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/mace_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
12.5
-
-
+
+
0
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
mace
Teichos
13.5
structures/hellenes/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_tower_double.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_tower_double.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_tower_double.xml (revision 23856)
@@ -1,86 +1,88 @@
200
40
Rampart Tower
structures/maur_tower_double
Higher health tower with ramparts for up to 16 archers. Visibly garrisoned archers recieve a range and armor bonus. Only archers can garrison. Needs the murder holes tech to protect its foot.
maur
Udarka
phase_city
2
7
Infantry+Archer
20
-
+
+
+
212.50
212.52
212.5-2
012.52
012.5-2
-212.50
-212.52
-212.5-2
2.118.00
2.118.02.1
2.118.0-2.1
018.02.1
018.0-2.1
-2.118.00
-2.118.02.1
-2.118.0-2.1
-
-
+
+
1200
19.0
structures/mauryas/tower_double.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_long.xml (revision 23856)
@@ -1,67 +1,67 @@
36
0
10.5
-
-
+
+
0
9.5
0
8
9.5
0
-8
9.5
0
4
9.5
0
-4
9.5
0
-
-
+
+
maur
Shilabanda
7
0
18
60
0
structures/mauryas/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/maur_wall_medium.xml (revision 23856)
@@ -1,49 +1,49 @@
24
0
10.5
-
-
+
+
0
9.5
0
4
9.5
0
-4
9.5
0
-
-
+
+
maur
Shilabanda
5
0
18
structures/mauryas/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/pers_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
11.6
-
-
+
+
0
10.6
0
4
10.6
0
-4
10.6
0
-
-
+
+
pers
Para
12.6
structures/persians/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/ptol_wall_medium.xml (revision 23856)
@@ -1,42 +1,42 @@
10.8
-
-
+
+
0
9.8
0
4
9.8
0
-4
9.8
0
-
-
+
+
ptol
Teichos
11.8
structures/ptolemies/wall_medium.xml
25
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_siege_wall_medium.xml (revision 23856)
@@ -1,71 +1,71 @@
15
35
5
4
7
3
own neutral enemy
40
0
6.7
-
-
+
+
0
5.7
0
4
5.7
0
-4
5.7
0
-
-
+
+
0.75
rome
structures/rome_wallset_siege
Siege Wall
Mūrus Circummūnītiōnis
SiegeWall
structures/siege_wall.png
A wooden and turf palisade buildable in enemy and neutral territories.
8
0
7.7
structures/romans/siege_wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
9.9
-
-
+
+
0
8.9
0
4
8.9
0
-4
8.9
0
-
-
+
+
rome
Moenia
10.9
structures/romans/wall_medium.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
12.5
-
-
+
+
0
11.5
0
8
11.5
0
-8
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
spart
Teichos
13.5
structures/hellenes/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wall_long.xml (revision 23856)
@@ -1,49 +1,49 @@
9.9
-
-
+
+
0
8.9
0
8
8.9
0
-8
8.9
0
4
8.9
0
-4
8.9
0
-
-
+
+
rome
Moenia
10.9
structures/romans/wall_long.xml
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_long.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_long.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/sele_wall_long.xml (revision 23856)
@@ -1,52 +1,52 @@
11.4
-
-
+
+
0
10.4
0
8
10.4
0
-8
10.4
0
4
10.4
0
-4
10.4
0
-
-
+
+
sele
Teichos
12.4
structures/seleucids/wall_long.xml
34
Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_medium.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_medium.xml (revision 23855)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/spart_wall_medium.xml (revision 23856)
@@ -1,39 +1,39 @@
12.5
-
-
+
+
0
11.5
0
4
11.5
0
-4
11.5
0
-
-
+
+
spart
Teichos
13.5
structures/hellenes/wall_medium.xml