Index: ps/trunk/binaries/data/mods/public/maps/random/extinct_volcano_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/extinct_volcano_triggers.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/maps/random/extinct_volcano_triggers.js (revision 25019)
@@ -1,191 +1,189 @@
/**
* Whether to log the water levels and which units became killed or transformed to visual actors.
*/
var debugLog = false;
/**
* Whether to rise the water to the maximum level in a minute or two.
*/
var debugWaterRise = false;
/**
* Duration in minutes for which the notification will be shown that states that the water will rise soon.
*/
var waterRiseNotificationDuration = 1;
/**
* Time in minutes between increases of the water level.
* If the water rises too fast, the hills are of no strategic importance,
* building structures would be pointless.
*
* At height 27, most trees are not gatherable anymore and enemies not reachable.
* At height 37 most hills are barely usable.
*
* At min 30 stuff at the ground level should not be gatherable anymore.
* At min 45 CC should be destroyed.
*
* Notice regular and military docks will raise with the water!
*/
var waterIncreaseTime = [0.5, 1];
/**
* Number of meters the waterheight increases each step.
* Each time the water level is changed, the pathfinder grids have to be recomputed.
* Therefore raising the level should occur as rarely as possible, i.e. have the value
* as big as possible, but as small as needed to keep it visually authentic.
*/
var waterLevelIncreaseHeight = 1;
/**
* At which height to stop increasing the water level.
* Since players can survive on ships, don't endlessly raise the water.
*/
var maxWaterLevel = 70;
/**
* Let buildings, relics and siege engines become actors, but kill organic units.
*/
var drownClass = "Organic";
/**
* Maximum height that units and structures can be submerged before drowning or becoming destructed.
*/
var drownHeight = 1;
/**
* One of these warnings is printed some minutes before the water level starts to rise.
*/
var waterWarningTexts = [
markForTranslation("It keeps on raining, we will have to evacuate soon!"),
markForTranslation("The rivers are standing high, we need to find a safe place!"),
markForTranslation("We have to find dry ground, our lands will drown soon!"),
markForTranslation("The lakes start swallowing the land, we have to find shelter!")
];
/**
* Units to be garrisoned in the wooden towers.
*/
var garrisonedUnits = "units/rome/champion_infantry_swordsman_02";
Trigger.prototype.RaisingWaterNotification = function()
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
"message": pickRandom(waterWarningTexts),
"translateMessage": true
}, waterRiseNotificationDuration * 60 * 1000);
};
Trigger.prototype.DebugLog = function(txt)
{
if (!debugLog)
return;
print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
};
Trigger.prototype.GarrisonWoodenTowers = function()
{
for (let gaiaEnt of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0))
{
let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass("Tower"))
continue;
let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
continue;
- for (let newEnt of TriggerHelper.SpawnUnits(gaiaEnt, garrisonedUnits, cmpGarrisonHolder.GetCapacity(), 0))
- if (Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt))
- Engine.QueryInterface(newEnt, IID_UnitAI).Autogarrison(gaiaEnt);
+ TriggerHelper.SpawnGarrisonedUnits(gaiaEnt, garrisonedUnits, cmpGarrisonHolder.GetCapacity(), 0);
}
};
Trigger.prototype.RaiseWaterLevelStep = function()
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let time = cmpTimer.GetTime();
let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
let newLevel = cmpWaterManager.GetWaterLevel() + waterLevelIncreaseHeight;
cmpWaterManager.SetWaterLevel(newLevel);
this.DebugLog("Raising water level to " + Math.round(newLevel) + " took " + (cmpTimer.GetTime() - time));
if (newLevel < maxWaterLevel)
this.DoAfterDelay((debugWaterRise ? 10 : randFloat(...waterIncreaseTime) * 60) * 1000, "RaiseWaterLevelStep", {});
else
this.DebugLog("Water reached final level");
let actorTemplates = {};
let killedTemplates = {};
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let ent of cmpRangeManager.GetGaiaAndNonGaiaEntities())
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
if (pos.y + drownHeight >= newLevel)
continue;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (!cmpIdentity)
continue;
let templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
// Animals and units drown
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth && cmpIdentity.HasClass(drownClass))
{
cmpHealth.Kill();
if (debugLog)
killedTemplates[templateName] = (killedTemplates[templateName] || 0) + 1;
continue;
}
// Resources and buildings become actors
// Do not use ChangeEntityTemplate for performance and
// because we don't need nor want the effects of MT_EntityRenamed
let cmpVisualActor = Engine.QueryInterface(ent, IID_Visual);
if (!cmpVisualActor)
continue;
let height = cmpPosition.GetHeightOffset();
let rot = cmpPosition.GetRotation();
let actorTemplate = cmpTemplateManager.GetTemplate(templateName).VisualActor.Actor;
let seed = cmpVisualActor.GetActorSeed();
Engine.DestroyEntity(ent);
let newEnt = Engine.AddEntity("actor|" + actorTemplate);
Engine.QueryInterface(newEnt, IID_Visual).SetActorSeed(seed);
let cmpNewPos = Engine.QueryInterface(newEnt, IID_Position);
cmpNewPos.JumpTo(pos.x, pos.z);
cmpNewPos.SetHeightOffset(height);
cmpNewPos.SetXZRotation(rot.x, rot.z);
cmpNewPos.SetYRotation(rot.y);
if (debugLog)
actorTemplates[templateName] = (actorTemplates[templateName] || 0) + 1;
}
this.DebugLog("Checking entities took " + (cmpTimer.GetTime() - time));
this.DebugLog("Killed: " + uneval(killedTemplates));
this.DebugLog("Converted to actors: " + uneval(actorTemplates));
};
{
let waterRiseTime = debugWaterRise ? 0 : (InitAttributes.settings.SeaLevelRiseTime || 0);
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.GarrisonWoodenTowers();
cmpTrigger.DoAfterDelay((waterRiseTime - waterRiseNotificationDuration) * 60 * 1000, "RaisingWaterNotification", {});
cmpTrigger.DoAfterDelay(waterRiseTime * 60 * 1000, "RaiseWaterLevelStep", {});
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/AlertRaiser.js (revision 25019)
@@ -1,141 +1,141 @@
function AlertRaiser() {}
AlertRaiser.prototype.Schema =
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"";
AlertRaiser.prototype.Init = function()
{
// Store the last time the alert was used so players can't lag the game by raising alerts repeatedly.
this.lastTime = 0;
};
AlertRaiser.prototype.UnitFilter = function(unit)
{
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), this.template.List._string);
};
AlertRaiser.prototype.RaiseAlert = function()
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (cmpTimer.GetTime() == this.lastTime)
return;
this.lastTime = cmpTimer.GetTime();
PlaySound("alert_raise", this.entity);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return;
let owner = cmpOwnership.GetOwner();
let cmpPlayer = QueryOwnerInterface(this.entity);
let mutualAllies = cmpPlayer ? cmpPlayer.GetMutualAllies() : [owner];
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Store the number of available garrison spots so that units don't try to garrison in buildings that will be full
let reserved = new Map();
let units = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.RaiseAlertRange, [owner], IID_UnitAI, true).filter(ent => this.UnitFilter(ent));
for (let unit of units)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
let holder = cmpRangeManager.ExecuteQuery(unit, 0, +this.template.SearchRange, mutualAllies, IID_GarrisonHolder, true).find(ent => {
// Ignore moving garrison holders
if (Engine.QueryInterface(ent, IID_UnitAI))
return false;
// Ensure that the garrison holder is within range of the alert raiser
if (+this.template.EndOfAlertRange > 0 && PositionHelper.DistanceBetweenEntities(this.entity, ent) > +this.template.EndOfAlertRange)
return false;
if (!cmpUnitAI.CheckTargetVisible(ent))
return false;
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (!reserved.has(ent))
reserved.set(ent, cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.OccupiedSlots());
return cmpGarrisonHolder.IsAllowedToGarrison(unit) && reserved.get(ent);
});
if (holder)
{
reserved.set(holder, reserved.get(holder) - 1);
cmpUnitAI.ReplaceOrder("Garrison", { "target": holder, "force": true });
}
else
// If no available spots, stop moving
cmpUnitAI.ReplaceOrder("Stop", { "force": true });
}
};
AlertRaiser.prototype.EndOfAlert = function()
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (cmpTimer.GetTime() == this.lastTime)
return;
this.lastTime = cmpTimer.GetTime();
PlaySound("alert_end", this.entity);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
return;
let owner = cmpOwnership.GetOwner();
let cmpPlayer = QueryOwnerInterface(this.entity);
let mutualAllies = cmpPlayer ? cmpPlayer.GetMutualAllies() : [owner];
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// Units that are not garrisoned should go back to work
let units = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.EndOfAlertRange, [owner], IID_UnitAI, true).filter(ent => this.UnitFilter(ent));
for (let unit of units)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (cmpUnitAI.HasWorkOrders() && cmpUnitAI.ShouldRespondToEndOfAlert())
cmpUnitAI.BackToWork();
else if (cmpUnitAI.ShouldRespondToEndOfAlert())
// Stop rather than continue to try to garrison
cmpUnitAI.ReplaceOrder("Stop", { "force": true });
}
// Units that are garrisoned should ungarrison and go back to work
let holders = cmpRangeManager.ExecuteQuery(this.entity, 0, +this.template.EndOfAlertRange, mutualAllies, IID_GarrisonHolder, true);
if (Engine.QueryInterface(this.entity, IID_GarrisonHolder))
holders.push(this.entity);
for (let holder of holders)
{
if (Engine.QueryInterface(holder, IID_UnitAI))
continue;
let cmpGarrisonHolder = Engine.QueryInterface(holder, IID_GarrisonHolder);
let units = cmpGarrisonHolder.GetEntities().filter(ent => {
let cmpOwner = Engine.QueryInterface(ent, IID_Ownership);
return cmpOwner && cmpOwner.GetOwner() == owner && this.UnitFilter(ent);
});
for (let unit of units)
- if (cmpGarrisonHolder.PerformEject([unit], false))
+ if (cmpGarrisonHolder.Unload(unit))
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (cmpUnitAI.HasWorkOrders())
cmpUnitAI.BackToWork();
else
// Stop rather than walk to the rally point
cmpUnitAI.ReplaceOrder("Stop", { "force": true });
}
}
};
Engine.RegisterComponentType(IID_AlertRaiser, "AlertRaiser", AlertRaiser);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GarrisonHolder.js (revision 25019)
@@ -1,644 +1,606 @@
function GarrisonHolder() {}
GarrisonHolder.prototype.Schema =
"" +
"" +
"" +
"" +
"" +
"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()
{
this.entities = [];
this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity);
};
/**
* @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.allowedClasses;
};
GarrisonHolder.prototype.GetCapacity = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
};
GarrisonHolder.prototype.IsFull = function()
{
return this.OccupiedSlots() >= 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)
{
if (!this.allowGarrisoning)
this.allowGarrisoning = new Map();
this.allowGarrisoning.set(callerID, allow);
};
/**
* @return {boolean} - Whether (un)garrisoning is allowed.
*/
GarrisonHolder.prototype.IsGarrisoningAllowed = function()
{
return !this.allowGarrisoning ||
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.OccupiedSlots = function()
{
let count = 0;
for (let ent of this.entities)
{
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
count += cmpGarrisonable.TotalSize();
}
return count;
};
GarrisonHolder.prototype.IsAllowedToGarrison = function(entity)
{
if (!this.IsGarrisoningAllowed())
return false;
if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
return false;
let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
if (!cmpGarrisonable || this.OccupiedSlots() + cmpGarrisonable.TotalSize() > this.GetCapacity())
return false;
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), this.allowedClasses);
};
/**
* @param {number} entity - The entityID to garrison.
* @return {boolean} - Whether the entity was garrisoned.
*/
GarrisonHolder.prototype.Garrison = function(entity)
{
if (!this.IsAllowedToGarrison(entity))
return false;
if (!this.HasEnoughHealth())
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();
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
"added": [entity],
"removed": []
});
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, renamed = false)
-{
- let entityIndex = this.entities.indexOf(entity);
- // Error: invalid entity ID, usually it's already been ejected
- if (entityIndex == -1)
- return false;
-
- let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
- if (!cmpGarrisonable || !cmpGarrisonable.UnGarrison(forced, renamed))
- return false;
-
- this.entities.splice(entityIndex, 1);
- Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
- "added": [],
- "removed": [entity]
- });
-
- return true;
-};
-
-/**
* @param {number} entity - EntityID to find the spawn position for.
* @param {boolean} forced - Optionally whether the spawning is forced.
* @return {Vector3D} - An appropriate spawning position.
*/
GarrisonHolder.prototype.GetSpawnPosition = function(entity, forced)
{
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 null;
// 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();
}
return pos;
};
/**
- * 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.
+ * @param {number} entity - The entity ID of the entity to eject.
+ * @param {boolean} forced - Whether eject is forced (e.g. if building is destroyed).
+ * @return {boolean} Whether the entity was ejected.
*/
-GarrisonHolder.prototype.PerformEject = function(entities, forced)
+GarrisonHolder.prototype.Eject = function(entity, 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.GetSize() : 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.GetSize() : 0;
- }
- }
- }
+ let entityIndex = this.entities.indexOf(entity);
+ // Error: invalid entity ID, usually it's already been ejected, assume success.
+ if (entityIndex == -1)
+ return true;
- this.OrderWalkToRallyPoint(ejectedEntities);
+ this.entities.splice(entityIndex, 1);
this.UpdateGarrisonFlag();
+ Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
+ "added": [],
+ "removed": [entity]
+ });
- return success;
+ return true;
};
/**
- * Order entities to walk to the rally point.
- * @param {Array} entities - An array containing all the ids of the entities.
+ * @param {number} entity - The entity ID of the entity to order to the rally point.
*/
-GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities)
+GarrisonHolder.prototype.OrderToRallyPoint = function(entity)
{
- 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);
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ if (!cmpOwnership)
+ return;
+
+ let cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
+ if (!cmpEntOwnership || cmpOwnership.GetOwner() != cmpEntOwnership.GetOwner())
+ return;
+
+ let commands = GetRallyPointCommands(cmpRallyPoint, [entity]);
// 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.
+ * Tell unit to unload from this entity.
+ * @param {number} entity - The entity to unload.
* @return {boolean} Whether the command was successful.
*/
-GarrisonHolder.prototype.Unload = function(entity, forced)
+GarrisonHolder.prototype.Unload = function(entity)
+{
+ let cmpGarrisonable = Engine.QueryInterface(entity, IID_Garrisonable);
+ return cmpGarrisonable && cmpGarrisonable.UnGarrison();
+};
+
+/**
+ * Tell units to unload from this entity.
+ * @param {number[]} entities - The entities to unload.
+ * @return {boolean} - Whether all unloads were successful.
+ */
+GarrisonHolder.prototype.UnloadEntities = function(entities)
{
- return this.PerformEject([entity], forced);
+ let success = true;
+ for (let entity of entities)
+ if (!this.Unload(entity))
+ success = false;
+ return success;
};
/**
- * Unload one or all units that match a template and owner from
- * the garrisoning entity and order them to move to the rally point.
+ * Unload one or all units that match a template and owner from us.
* @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)
+GarrisonHolder.prototype.UnloadTemplate = function(template, owner, all)
{
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);
+ return this.UnloadEntities(entities);
};
/**
* 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)
+GarrisonHolder.prototype.UnloadAllByOwner = function(owner)
{
let entities = this.entities.filter(ent => {
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
return cmpOwnership && cmpOwnership.GetOwner() == owner;
});
- return this.PerformEject(entities, forced);
+ return this.UnloadEntities(entities);
};
/**
* 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)
+GarrisonHolder.prototype.UnloadAll = function()
{
- return this.PerformEject(this.entities.slice(), forced);
+ return this.UnloadEntities(this.entities.slice());
};
/**
* 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);
delete this.timer;
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]
});
this.UpdateGarrisonFlag();
}
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)
{
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
{
let 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);
+ this.UnloadEntities(ejectables);
}
// 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();
+ else
+ Engine.DestroyEntity(entity);
this.entities.splice(entityIndex, 1);
killedEntities.push(entity);
}
if (killedEntities.length)
{
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, {
"added": [],
"removed": killedEntities
});
this.UpdateGarrisonFlag();
}
};
/**
* 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 cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
cmpGarrisonable.Autogarrison(this.entity);
}
this.initGarrison = undefined;
};
GarrisonHolder.prototype.OnValueModification = function(msg)
{
if (msg.component != "GarrisonHolder")
return;
if (msg.valueNames.indexOf("GarrisonHolder/List/_string") !== -1)
{
this.allowedClasses = ApplyValueModificationsToEntity("GarrisonHolder/List/_string", this.template.List._string, this.entity);
this.EjectOrKill(this.entities.filter(entity => !this.IsAllowedToGarrison(entity)));
}
if (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);
delete this.timer;
}
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/Garrisonable.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Garrisonable.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Garrisonable.js (revision 25019)
@@ -1,186 +1,196 @@
function Garrisonable() {}
Garrisonable.prototype.Schema =
"Controls the garrisonability of an entity." +
"" +
"10" +
"" +
"" +
"" +
"";
Garrisonable.prototype.Init = function()
{
};
/**
* @return {number} - The number of slots this unit takes in a garrisonHolder.
*/
Garrisonable.prototype.UnitSize = function()
{
return ApplyValueModificationsToEntity("Garrisonable/Size", +this.template.Size, this.entity);
};
/**
* Calculates the number of slots this unit takes in a garrisonHolder by
* adding the number of garrisoned slots to the equation.
*
* @return {number} - The number of slots this unit and its garrison takes in a garrisonHolder.
*/
Garrisonable.prototype.TotalSize = function()
{
let size = this.UnitSize();
let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
size += cmpGarrisonHolder.OccupiedSlots();
return size;
};
/**
* @return {number} - The entity ID of the entity this entity is garrisoned in.
*/
Garrisonable.prototype.HolderID = function()
{
return this.holder || INVALID_ENTITY;
};
/**
* @param {number} - The entity ID to check.
* @return {boolean} - Whether we can garrison.
*/
Garrisonable.prototype.CanGarrison = function(entity)
{
+ if (this.holder)
+ return false;
+
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
return cmpGarrisonHolder && cmpGarrisonHolder.IsAllowedToGarrison(this.entity);
};
/**
* @param {number} entity - The entity ID of the entity this entity is being garrisoned in.
* @return {boolean} - Whether garrisoning succeeded.
*/
Garrisonable.prototype.Garrison = function(entity, renamed = false)
{
- if (this.holder)
+ if (!this.CanGarrison(entity))
return false;
let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Garrison(this.entity))
return false;
this.holder = entity;
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.PauseProduction();
let cmpAura = Engine.QueryInterface(this.entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.ApplyGarrisonAura(entity);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
cmpPosition.MoveOutOfWorld();
if (renamed)
return true;
let cmpTurretHolder = Engine.QueryInterface(entity, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.OccupyTurret(this.entity);
return true;
};
/**
* Called on game init when the entity was part of init garrison.
* @param {number} entity - The entityID to autogarrison.
* @return {boolean} - Whether garrisoning succeeded.
*/
Garrisonable.prototype.Autogarrison = function(entity)
{
if (!this.Garrison(entity))
return false;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.Autogarrison(this.entity);
return true;
};
/**
* @param {boolean} forced - Optionally whether the spawning is forced.
* @param {boolean} renamed - Optionally whether the ungarrisoning is due to renaming.
* @return {boolean} - Whether the ungarrisoning succeeded.
*/
Garrisonable.prototype.UnGarrison = function(forced = false, renamed = false)
{
+ if (!this.holder)
+ return true;
+
+ let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
+ if (!cmpGarrisonHolder)
+ return false;
+
+ let pos = cmpGarrisonHolder.GetSpawnPosition(this.entity, forced);
+ if (!pos)
+ return false;
+
+ if (!cmpGarrisonHolder.Eject(this.entity, forced))
+ return false;
+
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
{
- let pos;
- let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
- if (cmpGarrisonHolder)
- pos = cmpGarrisonHolder.GetSpawnPosition(this.entity, forced);
-
- if (!pos)
- return false;
-
cmpPosition.JumpTo(pos.x, pos.z);
cmpPosition.SetHeightOffset(0);
-
- let cmpHolderPosition = Engine.QueryInterface(this.holder, IID_Position);
- if (cmpHolderPosition)
- cmpPosition.SetYRotation(cmpHolderPosition.GetPosition().horizAngleTo(pos));
}
+ let cmpHolderPosition = Engine.QueryInterface(this.holder, IID_Position);
+ if (cmpHolderPosition)
+ cmpPosition.SetYRotation(cmpHolderPosition.GetPosition().horizAngleTo(pos));
+
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.Ungarrison();
let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.UnpauseProduction();
let cmpAura = Engine.QueryInterface(this.entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.RemoveGarrisonAura(this.holder);
if (renamed)
return true;
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.LeaveTurret(this.entity);
+ cmpGarrisonHolder.OrderToRallyPoint(this.entity);
+
delete this.holder;
return true;
};
Garrisonable.prototype.OnEntityRenamed = function(msg)
{
if (!this.holder)
return;
let cmpGarrisonHolder = Engine.QueryInterface(this.holder, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
- // ToDo: Clean this by using cmpGarrisonable to ungarrison.
- cmpGarrisonHolder.Eject(msg.entity, true, true);
+ this.UnGarrison(true, true);
let cmpGarrisonable = Engine.QueryInterface(msg.newentity, IID_Garrisonable);
if (cmpGarrisonable)
cmpGarrisonable.Garrison(this.holder, true);
}
// We process EntityRenamed of turrets 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.
let cmpTurretHolder = Engine.QueryInterface(this.holder, IID_TurretHolder);
if (cmpTurretHolder)
cmpTurretHolder.SwapEntities(msg.entity, msg.newentity);
delete this.holder;
};
Engine.RegisterComponentType(IID_Garrisonable, "Garrisonable", Garrisonable);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 25019)
@@ -1,1001 +1,996 @@
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"Allows the building to train new units and research technologies" +
"" +
"0.7" +
"" +
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
"";
ProductionQueue.prototype.ProgressInterval = 1000;
ProductionQueue.prototype.MaxQueueSize = 16;
ProductionQueue.prototype.Init = function()
{
this.nextID = 1;
this.queue = [];
// Queue items are:
// {
// "id": 1,
// "player": 1, // who paid for this batch; we need this to cope with refunds cleanly
// "unitTemplate": "units/example",
// "count": 10,
// "neededSlots": 3, // number of population slots missing for production to begin
// "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total
// "population": 1, // population per unit, multiply by count to get total
// "productionStarted": false, // true iff we have reserved population
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
//
// {
// "id": 1,
// "player": 1, // who paid for this research; we need this to cope with refunds cleanly
// "technologyTemplate": "example_tech",
// "resources": { "wood": 100, ... }, // resources needed for research
// "productionStarted": false, // true iff production has started
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
this.timer = undefined; // this.ProgressInterval msec timer, active while the queue is non-empty
this.paused = false;
this.entityCache = [];
this.spawnNotified = false;
};
/*
* Returns list of entities that can be trained by this building.
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return Array.from(this.entitiesMap.values());
};
/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
ProductionQueue.prototype.CalculateEntitiesMap = function()
{
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
if (!this.template.Entities)
return;
let string = this.template.Entities._string;
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpPlayer = QueryOwnerInterface(this.entity);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
/**
* Process tokens:
* - process token modifiers (this is a bit tricky).
* - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
* - remove disabled entities
* - upgrade templates where necessary
* This also updates currently queued production (it's more convenient to do it here).
*/
let removeAllQueuedTemplate = (token) => {
let queue = clone(this.queue);
let template = this.entitiesMap.get(token);
for (let item of queue)
if (item.unitTemplate && item.unitTemplate === template)
this.RemoveBatch(item.id);
};
let updateAllQueuedTemplate = (token, updateTo) => {
let template = this.entitiesMap.get(token);
for (let item of this.queue)
if (item.unitTemplate && item.unitTemplate === template)
item.unitTemplate = updateTo;
};
let toks = string.split(/\s+/);
for (let tok of addedTokens)
toks.push(tok);
let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
let rawToken = token;
if (!(token in addedDict))
{
// This is a bit wasteful but I can't think of a simpler/better way.
// The list of token is unlikely to be a performance bottleneck anyways.
token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity);
token = token.split(/\s+/);
if (token.every(tok => addedTokens.indexOf(tok) !== -1))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = token[0];
}
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
if (cmpIdentity)
token = token.replace(/\{native\}/g, cmpIdentity.GetCiv());
if (cmpPlayer)
token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv());
// Filter out disabled and invalid entities.
if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = this.GetUpgradedTemplate(token);
entMap.set(rawToken, token);
updateAllQueuedTemplate(rawToken, token);
return entMap;
}, new Map());
};
/*
* Returns the upgraded template name if necessary.
*/
ProductionQueue.prototype.GetUpgradedTemplate = function(templateName)
{
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
let requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
/*
* Returns list of technologies that can be researched by this building.
*/
ProductionQueue.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
let string = this.template.Technologies._string;
string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity);
if (!string)
return [];
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return [];
let techs = string.split(/\s+/);
// Replace the civ specific technologies.
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
}
// Remove any technologies that can't be researched by this civ.
techs = techs.filter(tech =>
cmpTechnologyManager.CheckTechnologyRequirements(
DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
true));
let techList = [];
// Stores the tech which supersedes the key.
let superseded = {};
let disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons.
// Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
for (let tech of techs)
{
if (disabledTechnologies && disabledTechnologies[tech])
continue;
let template = TechnologyTemplates.Get(tech);
if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
techList.push(tech);
else
superseded[template.supersedes] = tech;
}
// Now make researched/in progress techs invisible.
for (let i in techList)
{
let tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech];
techList[i] = tech;
}
let ret = [];
// This inserts the techs into the correct positions to line up the technology pairs.
for (let i = 0; i < techList.length; ++i)
{
let tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
let template = TechnologyTemplates.Get(tech);
if (template.top)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
else
ret[i] = tech;
}
return ret;
};
ProductionQueue.prototype.GetTechCostMultiplier = function()
{
let techCostMultiplier = {};
for (let res in this.template.TechCostMultiplier)
techCostMultiplier[res] = ApplyValueModificationsToEntity(
"ProductionQueue/TechCostMultiplier/" + res,
+this.template.TechCostMultiplier[res],
this.entity);
return techCostMultiplier;
};
ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
let template = TechnologyTemplates.Get(tech);
if (template.top)
return cmpTechnologyManager.IsTechnologyResearched(template.top) ||
cmpTechnologyManager.IsInProgress(template.top) ||
cmpTechnologyManager.IsTechnologyResearched(template.bottom) ||
cmpTechnologyManager.IsInProgress(template.bottom);
return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
*/
ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata)
{
// TODO: there should probably be a limit on the number of queued batches.
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits).
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
if (!this.queue.length)
{
let cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade);
if (cmpUpgrade && cmpUpgrade.IsUpgrading())
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Entity is being upgraded. Cannot start production."),
"translateMessage": true
});
return;
}
}
if (this.queue.length < this.MaxQueueSize)
{
if (type == "unit")
{
if (!Number.isInteger(count) || count <= 0)
{
error("Invalid batch count " + count);
return;
}
// Find the template data so we can determine the build costs.
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return;
if (template.Promotion &&
!ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template))
{
this.AddBatch(template.Promotion.Entity, type, count, metadata);
return;
}
// We need the costs after tech modifications.
// Obviously we don't have the entities yet, so we must use template data.
let costs = {};
let totalCosts = {};
for (let res in template.Cost.Resources)
{
costs[res] = ApplyValueModificationsToTemplate(
"Cost/Resources/" + res,
+template.Cost.Resources[res],
cmpPlayer.GetPlayerID(),
template);
totalCosts[res] = Math.floor(count * costs[res]);
}
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(totalCosts))
return;
// Update entity count in the EntityLimits component.
if (template.TrainingRestrictions)
{
let unitCategory = template.TrainingRestrictions.Category;
let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(templateName, count);
}
let buildTime = ApplyValueModificationsToTemplate(
"Cost/BuildTime",
+template.Cost.BuildTime,
cmpPlayer.GetPlayerID(),
template);
// Apply a time discount to larger batches.
let time = this.GetBatchTime(count) * buildTime * 1000;
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"resources": costs,
"population": ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
cmpPlayer.GetPlayerID(),
template),
"productionStarted": false,
"timeTotal": time,
"timeRemaining": time
});
// Call the related trigger event.
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("TrainingQueued", {
"playerid": cmpPlayer.GetPlayerID(),
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"trainerEntity": this.entity
});
}
else if (type == "technology")
{
if (!TechnologyTemplates.Has(templateName))
return;
if (!this.GetTechnologiesList().some(tech =>
tech &&
(tech == templateName ||
tech.pair &&
(tech.top == templateName || tech.bottom == templateName))))
{
error("This entity cannot research " + templateName);
return;
}
let template = TechnologyTemplates.Get(templateName);
let techCostMultiplier = this.GetTechCostMultiplier();
let cost = {};
if (template.cost)
for (let res in template.cost)
cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]);
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(cost))
return;
// Tell the technology manager that we have started researching this so that people can't research the same
// thing twice.
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(templateName, this.entity);
if (!this.queue.length)
{
cmpTechnologyManager.StartedResearch(templateName, false);
this.SetAnimation("researching");
}
let time = techCostMultiplier.time * (template.researchTime || 0) * 1000;
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"count": 1,
"technologyTemplate": templateName,
"resources": cost,
"productionStarted": false,
"timeTotal": time,
"timeRemaining": time
});
// Call the related trigger event.
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ResearchQueued", {
"playerid": cmpPlayer.GetPlayerID(),
"technologyTemplate": templateName,
"researcherEntity": this.entity
});
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return;
}
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
// If this is the first item in the queue, start the timer.
if (!this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, {});
}
}
else
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("The production queue is full."),
"translateMessage": true,
});
}
};
/*
* Removes an existing batch of units from the production queue.
* Refunds resource costs and population reservations.
*/
ProductionQueue.prototype.RemoveBatch = function(id)
{
// Destroy any cached entities (those which didn't spawn for some reason).
for (let ent of this.entityCache)
Engine.DestroyEntity(ent);
this.entityCache = [];
for (let i = 0; i < this.queue.length; ++i)
{
// Find the item to remove.
let item = this.queue[i];
if (item.id != id)
continue;
// Update entity count in the EntityLimits component.
if (item.unitTemplate)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(item.unitTemplate);
if (template.TrainingRestrictions)
{
let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(item.unitTemplate, -item.count);
}
}
// Refund the resource cost for this batch.
let totalCosts = {};
let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
for (let r in item.resources)
{
totalCosts[r] = Math.floor(item.count * item.resources[r]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]);
}
let cmpPlayer = QueryPlayerIDInterface(item.player);
if (cmpPlayer)
{
cmpPlayer.AddResources(totalCosts);
// Remove reserved population slots if necessary.
if (item.productionStarted && item.unitTemplate)
cmpPlayer.UnReservePopulationSlots(item.population * item.count);
}
// Mark the research as stopped if we cancel it.
if (item.technologyTemplate)
{
// item.player is used as this.entity's owner may be invalid (deletion, etc.)
let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StoppedResearch(item.technologyTemplate, true);
this.SetAnimation("idle");
}
// Remove from the queue.
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty.)
this.queue.splice(i, 1);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
return;
}
};
ProductionQueue.prototype.SetAnimation = function(name)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation(name, false, 1);
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
return this.queue.map(item => ({
"id": item.id,
"unitTemplate": item.unitTemplate,
"technologyTemplate": item.technologyTemplate,
"count": item.count,
"neededSlots": item.neededSlots,
"progress": 1 - (item.timeRemaining / (item.timeTotal || 1)),
"timeRemaining": item.timeRemaining,
"metadata": item.metadata
}));
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
// Empty the production queue and refund all the resource costs
// to the player. (This is to avoid players having to micromanage their
// buildings' queues when they're about to be destroyed or captured.)
while (this.queue.length)
this.RemoveBatch(this.queue[0].id);
};
/*
* Returns batch build time.
*/
ProductionQueue.prototype.GetBatchTime = function(batchSize)
{
// TODO: work out what equation we should use here.
return Math.pow(batchSize, ApplyValueModificationsToEntity(
"ProductionQueue/BatchTimeModifier",
+this.template.BatchTimeModifier,
this.entity));
};
ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
if (msg.from != INVALID_PLAYER)
{
// Unset flag that previous owner's training may be blocked.
let cmpPlayer = QueryPlayerIDInterface(msg.from);
if (cmpPlayer && this.queue.length)
cmpPlayer.UnBlockTraining();
}
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesMap();
// Reset the production queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
// an enemy building, and then loads of the enemy's civ's soldiers get
// created from it. Also it means we don't have to worry about
// updating the reserved pop slots.)
this.ResetQueue();
};
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnDestroy = function()
{
// Reset the queue to refund any resources.
this.ResetQueue();
if (this.timer)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/*
* This function creates the entities and places them in world if possible
* and returns the number of successfully created entities.
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
*/
ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata)
{
let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
let createdEnts = [];
let spawnedEnts = [];
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed.
if (!this.entityCache.length)
for (let i = 0; i < count; ++i)
this.entityCache.push(Engine.AddEntity(templateName));
- let cmpAutoGarrison;
+ let autoGarrison;
if (cmpRallyPoint)
{
let data = cmpRallyPoint.GetData()[0];
if (data && data.target && data.target == this.entity && data.command == "garrison")
- cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
+ autoGarrison = true;
}
for (let i = 0; i < count; ++i)
{
let ent = this.entityCache[0];
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
- if (cmpAutoGarrison)
+ if (autoGarrison)
{
+ let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
// Temporary owner affectation needed for GarrisonHolder checks.
cmpNewOwnership.SetOwnerQuiet(cmpOwnership.GetOwner());
- garrisoned = cmpAutoGarrison.Garrison(ent);
+ garrisoned = cmpGarrisonable && cmpGarrisonable.Autogarrison(this.entity);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
- if (garrisoned)
- {
- let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
- if (cmpUnitAI)
- cmpUnitAI.Autogarrison(this.entity);
- }
- else
+ if (!garrisoned)
{
let pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
if (cmpPosition)
cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
spawnedEnts.push(ent);
}
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged function,
// i.e. we replace a 'trained' entity by 'alive' one.
// Must be done after spawn check so EntityLimits decrements only if unit spawns.
if (cmpPlayerEntityLimits)
{
let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
}
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
if (!createdEnts.length)
PlaySound("trained", ent);
this.entityCache.shift();
createdEnts.push(ent);
}
- if (spawnedEnts.length && !cmpAutoGarrison)
+ if (spawnedEnts.length && !autoGarrison)
{
// If a rally point is set, walk towards it (in formation) using a suitable command based on where the
// rally point is placed.
if (cmpRallyPoint)
{
let rallyPos = cmpRallyPoint.GetPositions()[0];
if (rallyPos)
{
let commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts);
for (let com of commands)
ProcessCommand(cmpOwnership.GetOwner(), com);
}
}
}
if (createdEnts.length)
Engine.PostMessage(this.entity, MT_TrainingFinished, {
"entities": createdEnts,
"owner": cmpOwnership.GetOwner(),
"metadata": metadata
});
return createdEnts.length;
};
/*
* Increments progress on the first batch in the production queue, and blocks the
* queue if population limit is reached or some units failed to spawn.
*/
ProductionQueue.prototype.ProgressTimeout = function(data)
{
// Check if the production is paused (eg the entity is garrisoned)
if (this.paused)
return;
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
// Allocate available time to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second).
let time = this.ProgressInterval;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
while (time > 0 && this.queue.length)
{
let item = this.queue[0];
if (!item.productionStarted)
{
// If the item is a unit then do population checks.
if (item.unitTemplate)
{
// If something change population cost.
let template = cmpTemplateManager.GetTemplate(item.unitTemplate);
item.population = ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
item.player,
template);
// Batch's training hasn't started yet.
// Try to reserve the necessary population slots.
item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count);
if (item.neededSlots)
{
// Not enough slots available - don't train this batch now
// (we'll try again on the next timeout).
cmpPlayer.BlockTraining();
break;
}
cmpPlayer.UnBlockTraining();
}
if (item.technologyTemplate)
{
// Mark the research as started.
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StartedResearch(item.technologyTemplate, true);
else
warn("Failed to start researching " + item.technologyTemplate + ": No TechnologyManager available.");
this.SetAnimation("researching");
}
item.productionStarted = true;
if (item.unitTemplate)
Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
}
// If we won't finish the batch now, just update its timer.
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
// send a message for the AIs.
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
break;
}
if (item.unitTemplate)
{
let numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata);
if (numSpawned == item.count)
{
// All entities spawned, this batch finished.
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
time -= item.timeRemaining;
this.queue.shift();
// Unset flag that training is blocked.
cmpPlayer.UnBlockTraining();
this.spawnNotified = false;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
}
else
{
if (numSpawned > 0)
{
// Training is only partially finished.
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
item.count -= numSpawned;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
}
// Some entities failed to spawn.
// Set flag that training is blocked.
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Can't find free space to spawn trained units"),
"translateMessage": true
});
this.spawnNotified = true;
}
break;
}
}
else if (item.technologyTemplate)
{
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.ResearchTechnology(item.technologyTemplate);
else
warn("Failed to stop researching " + item.technologyTemplate + ": No TechnologyManager available.");
this.SetAnimation("idle");
let template = TechnologyTemplates.Get(item.technologyTemplate);
if (template && template.soundComplete)
{
let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
}
time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {});
}
}
// If the queue's empty, delete the timer, else repeat it.
if (!this.queue.length)
{
this.timer = undefined;
// Unset flag that training is blocked.
// (This might happen when the player unqueues all batches.)
cmpPlayer.UnBlockTraining();
}
else
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, data);
}
};
ProductionQueue.prototype.PauseProduction = function()
{
this.timer = undefined;
this.paused = true;
};
ProductionQueue.prototype.UnpauseProduction = function()
{
this.paused = false;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, {});
};
ProductionQueue.prototype.OnValueModification = function(msg)
{
// If the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list.
if (msg.component != "Promotion" && (msg.component != "ProductionQueue" ||
!msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/"))))
return;
if (msg.entities.indexOf(this.entity) === -1)
return;
// This also updates the queued production if necessary.
this.CalculateEntitiesMap();
// Inform the GUI that it'll need to recompute the selection panel.
// TODO: it would be better to only send the message if something actually changing
// for the current production queue.
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
ProductionQueue.prototype.HasQueuedProduction = function()
{
return this.queue.length > 0;
};
ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
{
// If the disabled templates of the player is changed,
// update the entities list so that this is reflected there.
this.CalculateEntitiesMap();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
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 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 25019)
@@ -1,297 +1,292 @@
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("Garrisonable.js");
Engine.LoadComponentScript("GarrisonHolder.js");
const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33];
const garrisonHolderId = 15;
const unitToGarrisonId = 24;
const enemyUnitId = 34;
const largeUnitId = 35;
const player = 1;
const friendlyPlayer = 2;
const enemyPlayer = 3;
let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
- "Max": 10,
+ "Max": "10",
"List": { "_string": "Infantry+Cavalry" },
- "EjectHealth": 0.1,
+ "EjectHealth": "0.1",
"EjectClassesOnDestroy": { "_string": "Infantry" },
- "BuffHeal": 1,
- "LoadingRange": 2.1,
+ "BuffHeal": "1",
+ "LoadingRange": "2.1",
"Pickup": false
});
-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 <= 35; ++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
});
if (i == largeUnitId)
AddMock(i, IID_Garrisonable, {
"UnitSize": () => 9,
"TotalSize": () => 9,
- "Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(i, renamed),
- "UnGarrison": () => true
+ "Garrison": (entity) => cmpGarrisonHolder.Garrison(i),
+ "UnGarrison": () => cmpGarrisonHolder.Eject(i)
});
else
AddMock(i, IID_Garrisonable, {
"UnitSize": () => 1,
"TotalSize": () => 1,
- "Garrison": entity => true,
- "UnGarrison": () => true
+ "Garrison": entity => cmpGarrisonHolder.Garrison(i),
+ "UnGarrison": () => cmpGarrisonHolder.Eject(i)
});
AddMock(i, IID_Position, {
"GetHeightOffset": () => 0,
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {}
});
}
AddMock(33, IID_Identity, {
"GetClassesList": () => ["Infantry", "Cavalry"],
"GetSelectionGroupName": () => "spart_infantry_archer_a"
});
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.Garrison(largeUnitId), true);
TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true);
- TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(largeUnitId), true);
- TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(unitToGarrisonId), true);
+ TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(largeUnitId), true);
+ TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(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.Garrison(largeUnitId), false);
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_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, 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_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer), 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_EQUALS(cmpGarrisonHolder.Unload(25), true);
+ TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(null, 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.Garrison(largeUnitId), false);
TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
};
-// No health component yet.
+// No health component yet.Pick
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(largeUnitId), true);
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
});
testGarrisonAllowed();
// Test entity renaming.
let siegeEngineId = 44;
AddMock(siegeEngineId, IID_Identity, {
"GetClassesList": () => ["Siege"]
});
let archerId = 45;
AddMock(archerId, IID_Identity, {
"GetClassesList": () => ["Infantry", "Ranged"]
});
let originalClassList = "Infantry+Ranged Siege Cavalry";
cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", {
"Max": 10,
"List": { "_string": originalClassList },
"EjectHealth": 0.1,
"EjectClassesOnDestroy": { "_string": "Infantry" },
"BuffHeal": 1,
"LoadingRange": 2.1,
"Pickup": false
});
let traderId = 32;
AddMock(traderId, IID_Identity, {
"GetClassesList": () => ["Trader"]
});
AddMock(siegeEngineId, IID_Position, {
"GetHeightOffset": () => 0,
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {}
});
let currentSiegePlayer = player;
AddMock(siegeEngineId, IID_Ownership, {
"GetOwner": () => currentSiegePlayer
});
AddMock(siegeEngineId, IID_Garrisonable, {
"UnitSize": () => 1,
"TotalSize": () => 1,
"Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(siegeEngineId, renamed),
"UnGarrison": () => true
});
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),
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
"SetHeightOffset": height => {}
});
let currentCavalryPlayer = player;
AddMock(cavalryId, IID_Ownership, {
"GetOwner": () => currentCavalryPlayer
});
AddMock(cavalryId, IID_Garrisonable, {
"UnitSize": () => 1,
"TotalSize": () => 1,
"Garrison": (entity, renamed) => cmpGarrisonHolder.Garrison(cavalryId, renamed),
"UnGarrison": () => true
});
TS_ASSERT(cmpGarrisonHolder.Garrison(cavalryId));
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 1);
// Eject enemy units.
currentCavalryPlayer = enemyPlayer;
cmpGarrisonHolder.OnGlobalOwnershipChanged({
"entity": cavalryId,
"to": enemyPlayer
});
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0);
let oldApplyValueModificationsToEntity = ApplyValueModificationsToEntity;
TS_ASSERT(cmpGarrisonHolder.Garrison(siegeEngineId));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [siegeEngineId]);
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (valueName, currentValue, entity) => {
if (valueName !== "GarrisonHolder/List/_string")
return valueName;
return HandleTokens(currentValue, "-Siege Trader");
});
cmpGarrisonHolder.OnValueModification({
"component": "GarrisonHolder",
"valueNames": ["GarrisonHolder/List/_string"],
"entities": [garrisonHolderId]
});
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses().split(/\s+/), ["Infantry+Ranged", "Cavalry", "Trader"]);
// The new classes are now cached so we can restore the behavior.
Engine.RegisterGlobal("ApplyValueModificationsToEntity", oldApplyValueModificationsToEntity);
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
TS_ASSERT(!cmpGarrisonHolder.Garrison(siegeEngineId));
TS_ASSERT(cmpGarrisonHolder.Garrison(traderId));
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisonable.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisonable.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisonable.js (revision 25019)
@@ -1,39 +1,55 @@
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Garrisonable.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
const garrisonHolderID = 1;
const garrisonableID = 2;
AddMock(garrisonHolderID, IID_GarrisonHolder, {
- "Garrison": () => true
+ "Garrison": () => true,
+ "GetSpawnPosition": () => new Vector3D(0, 0, 0),
+ "IsAllowedToGarrison": () => true,
+ "OrderToRallyPoint": () => {},
+ "Eject": () => true
});
let size = 1;
let cmpGarrisonable = ConstructComponent(garrisonableID, "Garrisonable", {
"Size": size
});
-TS_ASSERT_EQUALS(cmpGarrisonable.UnitSize(garrisonHolderID), size);
-TS_ASSERT_EQUALS(cmpGarrisonable.TotalSize(garrisonHolderID), size);
+TS_ASSERT_EQUALS(cmpGarrisonable.UnitSize(), size);
+TS_ASSERT_EQUALS(cmpGarrisonable.TotalSize(), size);
let extraSize = 2;
AddMock(garrisonableID, IID_GarrisonHolder, {
"OccupiedSlots": () => extraSize
});
-TS_ASSERT_EQUALS(cmpGarrisonable.UnitSize(garrisonHolderID), size);
-TS_ASSERT_EQUALS(cmpGarrisonable.TotalSize(garrisonHolderID), size + extraSize);
+TS_ASSERT_EQUALS(cmpGarrisonable.UnitSize(), size);
+TS_ASSERT_EQUALS(cmpGarrisonable.TotalSize(), size + extraSize);
+
+// Test garrisoning.
TS_ASSERT(cmpGarrisonable.Garrison(garrisonHolderID));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), garrisonHolderID);
TS_ASSERT(!cmpGarrisonable.Garrison(garrisonHolderID));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), garrisonHolderID);
cmpGarrisonable.UnGarrison();
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), INVALID_ENTITY);
+
+// Test renaming.
+const newGarrisonableID = 3;
+let cmpGarrisonableNew = ConstructComponent(newGarrisonableID, "Garrisonable", {
+ "Size": 1
+});
+TS_ASSERT(cmpGarrisonable.Garrison(garrisonHolderID));
+cmpGarrisonable.OnEntityRenamed({ "entity": garrisonableID, "newentity": newGarrisonableID });
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonable.HolderID(), INVALID_ENTITY);
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonableNew.HolderID(), garrisonHolderID);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Garrisoning.js (revision 25019)
@@ -1,92 +1,196 @@
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Garrisonable.js");
Engine.LoadComponentScript("interfaces/GarrisonHolder.js");
Engine.LoadComponentScript("interfaces/Health.js");
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Garrisonable.js");
Engine.LoadComponentScript("GarrisonHolder.js");
+Engine.LoadComponentScript("TurretHolder.js");
const player = 1;
const enemyPlayer = 2;
const friendlyPlayer = 3;
const garrison = 10;
const holder = 11;
+let createGarrisonCmp = entity => {
+ AddMock(entity, IID_Identity, {
+ "GetClassesList": () => ["Ranged"],
+ "GetSelectionGroupName": () => "mace_infantry_archer_a"
+ });
+
+ AddMock(entity, IID_Ownership, {
+ "GetOwner": () => player
+ });
+
+ AddMock(entity, IID_Position, {
+ "GetHeightOffset": () => 0,
+ "GetPosition": () => new Vector3D(4, 3, 25),
+ "GetRotation": () => new Vector3D(4, 0, 6),
+ "JumpTo": (posX, posZ) => {},
+ "MoveOutOfWorld": () => {},
+ "SetHeightOffset": height => {},
+ "SetTurretParent": ent => {},
+ "SetYRotation": angle => {}
+ });
+
+ return ConstructComponent(entity, "Garrisonable", {
+ "Size": "1"
+ });
+};
+
AddMock(holder, IID_Footprint, {
"PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30),
"PickSpawnPoint": entity => new Vector3D(4, 3, 30)
});
AddMock(holder, 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
});
AddMock(garrison, IID_Identity, {
"GetClassesList": () => ["Ranged"],
"GetSelectionGroupName": () => "mace_infantry_archer_a"
});
AddMock(garrison, IID_Ownership, {
"GetOwner": () => player
});
AddMock(garrison, IID_Position, {
"GetHeightOffset": () => 0,
"GetPosition": () => new Vector3D(4, 3, 25),
"GetRotation": () => new Vector3D(4, 0, 6),
"JumpTo": (posX, posZ) => {},
"MoveOutOfWorld": () => {},
- "SetHeightOffset": height => {}
+ "SetHeightOffset": height => {},
+ "SetTurretParent": entity => {},
+ "SetYRotation": angle => {}
});
let cmpGarrisonable = ConstructComponent(garrison, "Garrisonable", {
"Size": "1"
});
let cmpGarrisonHolder = ConstructComponent(holder, "GarrisonHolder", {
"Max": "10",
"List": { "_string": "Ranged" },
"EjectHealth": "0.1",
"EjectClassesOnDestroy": { "_string": "Infantry" },
"BuffHeal": "1",
"LoadingRange": "2.1",
"Pickup": "false"
});
TS_ASSERT(cmpGarrisonable.Garrison(holder));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]);
cmpGarrisonable.OnEntityRenamed({
"entity": garrison,
"newentity": -1
});
TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0);
TS_ASSERT(cmpGarrisonable.Garrison(holder));
TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]);
+
+// Can't garrison twice.
+TS_ASSERT(!cmpGarrisonable.Garrison(holder));
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]);
+
+TS_ASSERT(cmpGarrisonHolder.Unload(garrison));
+TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0);
+
+// Test initGarrison.
+let entities = [21, 22, 23, 24];
+for (let entity of entities)
+ createGarrisonCmp(entity);
+cmpGarrisonHolder.SetInitGarrison(entities);
+cmpGarrisonHolder.OnGlobalInitGame();
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), entities);
+
+// They turned against us!
+AddMock(entities[0], IID_Ownership, {
+ "GetOwner": () => enemyPlayer
+});
+cmpGarrisonHolder.OnDiplomacyChanged();
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), entities.length - 1);
+
+TS_ASSERT(cmpGarrisonHolder.UnloadAll());
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
+
+// Turrets!
+AddMock(holder, IID_Position, {
+ "GetPosition": () => new Vector3D(4, 3, 25),
+ "GetRotation": () => new Vector3D(4, 0, 6)
+});
+
+let cmpTurretHolder = ConstructComponent(holder, "TurretHolder", {
+ "TurretPoints": {
+ "archer1": {
+ "X": "12.0",
+ "Y": "5.",
+ "Z": "6.0"
+ },
+ "archer2": {
+ "X": "15.0",
+ "Y": "5.0",
+ "Z": "6.0"
+ }
+ }
+});
+
+TS_ASSERT(cmpGarrisonable.Garrison(holder));
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [garrison]);
+TS_ASSERT(cmpTurretHolder.OccupiesTurret(garrison));
+TS_ASSERT(cmpGarrisonable.UnGarrison());
+TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []);
+TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), []);
+
+// Test renaming on a turret.
+// Ensure we test renaming from the second spot, not the first.
+const newGarrison = 31;
+let cmpGarrisonableNew = createGarrisonCmp(newGarrison);
+TS_ASSERT(cmpGarrisonableNew.Garrison(holder));
+TS_ASSERT(cmpGarrisonable.Garrison(holder));
+TS_ASSERT(cmpGarrisonableNew.UnGarrison());
+let previousTurret = cmpTurretHolder.GetOccupiedTurretName(garrison);
+cmpGarrisonable.OnEntityRenamed({
+ "entity": garrison,
+ "newentity": newGarrison
+});
+let newTurret = cmpTurretHolder.GetOccupiedTurretName(newGarrison);
+TS_ASSERT_UNEVAL_EQUALS(newTurret, previousTurret);
+TS_ASSERT(cmpGarrisonableNew.UnGarrison());
+
+// Test initTurrets.
+cmpTurretHolder.SetInitEntity("archer1", garrison);
+cmpTurretHolder.SetInitEntity("archer2", newGarrison);
+cmpTurretHolder.OnGlobalInitGame();
+TS_ASSERT_UNEVAL_EQUALS(cmpTurretHolder.GetEntities(), [garrison, newGarrison]);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 25018)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 25019)
@@ -1,266 +1,266 @@
// Helper functions to change an entity's template and check if the transformation is possible
// returns the ID of the new entity or INVALID_ENTITY.
function ChangeEntityTemplate(oldEnt, newTemplate)
{
// Done un/packing, copy our parameters to the final entity
var newEnt = Engine.AddEntity(newTemplate);
if (newEnt == INVALID_ENTITY)
{
error("Transform.js: Error replacing entity " + oldEnt + " for a '" + newTemplate + "'");
return INVALID_ENTITY;
}
var cmpPosition = Engine.QueryInterface(oldEnt, IID_Position);
var cmpNewPosition = Engine.QueryInterface(newEnt, IID_Position);
if (cmpPosition && cmpNewPosition)
{
if (cmpPosition.IsInWorld())
{
let pos = cmpPosition.GetPosition2D();
cmpNewPosition.JumpTo(pos.x, pos.y);
}
let rot = cmpPosition.GetRotation();
cmpNewPosition.SetYRotation(rot.y);
cmpNewPosition.SetXZRotation(rot.x, rot.z);
cmpNewPosition.SetHeightOffset(cmpPosition.GetHeightOffset());
}
var cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
var cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
if (cmpOwnership && cmpNewOwnership)
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
// Copy control groups
CopyControlGroups(oldEnt, newEnt);
// Rescale capture points
var cmpCapturable = Engine.QueryInterface(oldEnt, IID_Capturable);
var cmpNewCapturable = Engine.QueryInterface(newEnt, IID_Capturable);
if (cmpCapturable && cmpNewCapturable)
{
let scale = cmpCapturable.GetMaxCapturePoints() / cmpNewCapturable.GetMaxCapturePoints();
let newCapturePoints = cmpCapturable.GetCapturePoints().map(v => v / scale);
cmpNewCapturable.SetCapturePoints(newCapturePoints);
}
// Maintain current health level
var cmpHealth = Engine.QueryInterface(oldEnt, IID_Health);
var cmpNewHealth = Engine.QueryInterface(newEnt, IID_Health);
if (cmpHealth && cmpNewHealth)
{
var healthLevel = Math.max(0, Math.min(1, cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints()));
cmpNewHealth.SetHitpoints(cmpNewHealth.GetMaxHitpoints() * healthLevel);
}
let cmpBuilderList = QueryBuilderListInterface(oldEnt);
let cmpNewBuilderList = QueryBuilderListInterface(newEnt);
if (cmpBuilderList && cmpNewBuilderList)
cmpNewBuilderList.AddBuilders(cmpBuilderList.GetBuilders());
var cmpUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI);
var cmpNewUnitAI = Engine.QueryInterface(newEnt, IID_UnitAI);
if (cmpUnitAI && cmpNewUnitAI)
{
let pos = cmpUnitAI.GetHeldPosition();
if (pos)
cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
if (cmpUnitAI.GetStanceName())
cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
if (cmpUnitAI.IsGarrisoned())
cmpNewUnitAI.SetGarrisoned();
cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
if (cmpUnitAI.IsGuardOf())
{
let guarded = cmpUnitAI.IsGuardOf();
let cmpGuard = Engine.QueryInterface(guarded, IID_Guard);
if (cmpGuard)
{
cmpGuard.RenameGuard(oldEnt, newEnt);
cmpNewUnitAI.SetGuardOf(guarded);
}
}
}
let cmpPromotion = Engine.QueryInterface(oldEnt, IID_Promotion);
let cmpNewPromotion = Engine.QueryInterface(newEnt, IID_Promotion);
if (cmpPromotion && cmpNewPromotion)
cmpNewPromotion.IncreaseXp(cmpPromotion.GetCurrentXp());
let cmpResGatherer = Engine.QueryInterface(oldEnt, IID_ResourceGatherer);
let cmpNewResGatherer = Engine.QueryInterface(newEnt, IID_ResourceGatherer);
if (cmpResGatherer && cmpNewResGatherer)
{
let carriedResources = cmpResGatherer.GetCarryingStatus();
cmpNewResGatherer.GiveResources(carriedResources);
cmpNewResGatherer.SetLastCarriedType(cmpResGatherer.GetLastCarriedType());
}
// Maintain the list of guards
let cmpGuard = Engine.QueryInterface(oldEnt, IID_Guard);
let cmpNewGuard = Engine.QueryInterface(newEnt, IID_Guard);
if (cmpGuard && cmpNewGuard)
{
let entities = cmpGuard.GetEntities();
if (entities.length)
{
cmpNewGuard.SetEntities(entities);
for (let ent of entities)
{
let cmpEntUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpEntUnitAI)
cmpEntUnitAI.SetGuardOf(newEnt);
}
}
}
let cmpStatusEffectsReceiver = Engine.QueryInterface(oldEnt, IID_StatusEffectsReceiver);
let cmpNewStatusEffectsReceiver = Engine.QueryInterface(newEnt, IID_StatusEffectsReceiver);
if (cmpStatusEffectsReceiver && cmpNewStatusEffectsReceiver)
{
let activeStatus = cmpStatusEffectsReceiver.GetActiveStatuses();
for (let status in activeStatus)
{
let newStatus = activeStatus[status];
if (newStatus.Duration)
newStatus.Duration -= newStatus._timeElapsed;
cmpNewStatusEffectsReceiver.ApplyStatus({ [status]: newStatus }, newStatus.source.entity, newStatus.source.owner);
}
}
TransferGarrisonedUnits(oldEnt, newEnt);
Engine.PostMessage(oldEnt, MT_EntityRenamed, { "entity": oldEnt, "newentity": newEnt });
if (cmpPosition && cmpPosition.IsInWorld())
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(oldEnt);
return newEnt;
}
function CopyControlGroups(oldEnt, newEnt)
{
let cmpObstruction = Engine.QueryInterface(oldEnt, IID_Obstruction);
let cmpNewObstruction = Engine.QueryInterface(newEnt, IID_Obstruction);
if (cmpObstruction && cmpNewObstruction)
{
cmpNewObstruction.SetControlGroup(cmpObstruction.GetControlGroup());
cmpNewObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2());
}
}
function ObstructionsBlockingTemplateChange(ent, templateArg)
{
var previewEntity = Engine.AddEntity("preview|"+templateArg);
if (previewEntity == INVALID_ENTITY)
return true;
CopyControlGroups(ent, previewEntity);
var cmpBuildRestrictions = Engine.QueryInterface(previewEntity, IID_BuildRestrictions);
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
var cmpNewPosition = Engine.QueryInterface(previewEntity, IID_Position);
// Return false if no ownership as BuildRestrictions.CheckPlacement needs an owner and I have no idea if false or true is better
// Plus there are no real entities without owners currently.
if (!cmpBuildRestrictions || !cmpPosition || !cmpOwnership)
return DeleteEntityAndReturn(previewEntity, cmpPosition, null, null, cmpNewPosition, false);
var pos = cmpPosition.GetPosition2D();
var angle = cmpPosition.GetRotation();
// move us away to prevent our own obstruction from blocking the upgrade.
cmpPosition.MoveOutOfWorld();
cmpNewPosition.JumpTo(pos.x, pos.y);
cmpNewPosition.SetYRotation(angle.y);
var cmpNewOwnership = Engine.QueryInterface(previewEntity, IID_Ownership);
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
var checkPlacement = cmpBuildRestrictions.CheckPlacement();
if (checkPlacement && !checkPlacement.success)
return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(cmpTemplateManager.GetCurrentTemplateName(ent));
var newTemplate = cmpTemplateManager.GetTemplate(templateArg);
// Check if units are blocking our template change
if (template.Obstruction && newTemplate.Obstruction)
{
// This only needs to be done if the new template is strictly bigger than the old one
// "Obstructions" are annoying to test so just check.
if (newTemplate.Obstruction.Obstructions ||
newTemplate.Obstruction.Static && template.Obstruction.Static &&
(newTemplate.Obstruction.Static["@width"] > template.Obstruction.Static["@width"] ||
newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Static["@depth"]) ||
newTemplate.Obstruction.Static && template.Obstruction.Unit &&
(newTemplate.Obstruction.Static["@width"] > template.Obstruction.Unit["@radius"] ||
newTemplate.Obstruction.Static["@depth"] > template.Obstruction.Unit["@radius"]) ||
newTemplate.Obstruction.Unit && template.Obstruction.Unit &&
newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Unit["@radius"] ||
newTemplate.Obstruction.Unit && template.Obstruction.Static &&
(newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@width"] ||
newTemplate.Obstruction.Unit["@radius"] > template.Obstruction.Static["@depth"]))
{
var cmpNewObstruction = Engine.QueryInterface(previewEntity, IID_Obstruction);
if (cmpNewObstruction && cmpNewObstruction.GetBlockMovementFlag())
{
// Remove all obstructions at the new entity, especially animal corpses
for (let ent of cmpNewObstruction.GetEntitiesDeletedUponConstruction())
Engine.DestroyEntity(ent);
let collisions = cmpNewObstruction.GetEntitiesBlockingConstruction();
if (collisions.length)
return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, true);
}
}
}
return DeleteEntityAndReturn(previewEntity, cmpPosition, pos, angle, cmpNewPosition, false);
}
function DeleteEntityAndReturn(ent, cmpPosition, position, angle, cmpNewPosition, ret)
{
// prevent preview from interfering in the world
cmpNewPosition.MoveOutOfWorld();
if (position !== null)
{
cmpPosition.JumpTo(position.x, position.y);
cmpPosition.SetYRotation(angle.y);
}
Engine.DestroyEntity(ent);
return ret;
}
function TransferGarrisonedUnits(oldEnt, newEnt)
{
// Transfer garrisoned units if possible, or unload them
let cmpOldGarrison = Engine.QueryInterface(oldEnt, IID_GarrisonHolder);
if (!cmpOldGarrison || !cmpOldGarrison.GetEntities().length)
return;
let cmpNewGarrison = Engine.QueryInterface(newEnt, IID_GarrisonHolder);
let entities = cmpOldGarrison.GetEntities().slice();
for (let ent of entities)
{
- cmpOldGarrison.Eject(ent);
+ cmpOldGarrison.Unload(ent);
if (!cmpNewGarrison)
continue;
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (!cmpGarrisonable)
continue;
cmpGarrisonable.Autogarrison(newEnt);
}
}
Engine.RegisterGlobal("ChangeEntityTemplate", ChangeEntityTemplate);
Engine.RegisterGlobal("ObstructionsBlockingTemplateChange", ObstructionsBlockingTemplateChange);