Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25219)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25220)
@@ -1,431 +1,438 @@
function Foundation() {}
Foundation.prototype.Schema =
"";
Foundation.prototype.Init = function()
{
// Foundations are initially 'uncommitted' and do not block unit movement at all
// (to prevent players exploiting free foundations to confuse enemy units).
// The first builder to reach the uncommitted foundation will tell friendly units
// and animals to move out of the way, then will commit the foundation and enable
// its obstruction once there's nothing in the way.
this.committed = false;
this.builders = new Map(); // Map of builder entities to their work per second
this.totalBuilderRate = 0; // Total amount of work the builders do each second
this.buildMultiplier = 1; // Multiplier for the amount of work builders do
this.buildTimePenalty = 0.7; // Penalty for having multiple builders
this.previewEntity = INVALID_ENTITY;
};
Foundation.prototype.Serialize = function()
{
let ret = Object.assign({}, this);
ret.previewEntity = INVALID_ENTITY;
return ret;
};
Foundation.prototype.Deserialize = function(data)
{
this.Init();
Object.assign(this, data);
};
Foundation.prototype.OnDeserialized = function()
{
this.CreateConstructionPreview();
};
Foundation.prototype.InitialiseConstruction = function(template)
{
this.finalTemplateName = template;
// Remember the cost here, so if it changes after construction begins (from auras or technologies)
// we will use the correct values to refund partial construction costs.
let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
if (!cmpCost)
error("A foundation, from " + template + ", must have a cost component to know the build time");
this.costs = cmpCost.GetResourceCosts();
this.maxProgress = 0;
this.initialised = true;
};
/**
* Moving the revelation logic from Build to here makes the building sink if
* it is attacked.
*/
Foundation.prototype.OnHealthChanged = function(msg)
{
let cmpPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
if (cmpPosition)
cmpPosition.SetConstructionProgress(this.GetBuildProgress());
Engine.PostMessage(this.entity, MT_FoundationProgressChanged, { "to": this.GetBuildPercentage() });
};
/**
* Returns the current build progress in a [0,1] range.
*/
Foundation.prototype.GetBuildProgress = function()
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (!cmpHealth)
return 0;
return cmpHealth.GetHitpoints() / cmpHealth.GetMaxHitpoints();
};
Foundation.prototype.GetBuildPercentage = function()
{
return Math.floor(this.GetBuildProgress() * 100);
};
/**
* @return {number[]} - An array containing the entity IDs of assigned builders.
*/
Foundation.prototype.GetBuilders = function()
{
return Array.from(this.builders.keys());
};
Foundation.prototype.GetNumBuilders = function()
{
return this.builders.size;
};
Foundation.prototype.IsFinished = function()
{
return (this.GetBuildProgress() == 1.0);
};
Foundation.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to != INVALID_PLAYER && this.previewEntity != INVALID_ENTITY)
{
let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
if (cmpPreviewOwnership)
cmpPreviewOwnership.SetOwner(msg.to);
return;
}
if (msg.to != INVALID_PLAYER || !this.initialised)
return;
if (this.previewEntity != INVALID_ENTITY)
{
Engine.DestroyEntity(this.previewEntity);
this.previewEntity = INVALID_ENTITY;
}
if (this.IsFinished())
return;
let cmpPlayer = QueryPlayerIDInterface(msg.from);
let cmpStatisticsTracker = QueryPlayerIDInterface(msg.from, IID_StatisticsTracker);
// Refund a portion of the construction cost, proportional
// to the amount of build progress remaining.
for (let r in this.costs)
{
let scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress));
if (scaled)
{
if (cmpPlayer)
cmpPlayer.AddResource(r, scaled);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled);
}
}
};
/**
* @param {number[]} builders - An array containing the entity IDs of builders to assign.
*/
Foundation.prototype.AddBuilders = function(builders)
{
let changed = false;
for (let builder of builders)
changed = this.AddBuilderHelper(builder) || changed;
if (changed)
this.HandleBuildersChanged();
};
/**
* @param {number} builderEnt - The entity to add.
* @return {boolean} - Whether the addition was successful.
*/
Foundation.prototype.AddBuilderHelper = function(builderEnt)
{
if (this.builders.has(builderEnt))
return false;
let cmpBuilder = Engine.QueryInterface(builderEnt, IID_Builder) ||
Engine.QueryInterface(this.entity, IID_AutoBuildable);
if (!cmpBuilder)
return false;
let buildRate = cmpBuilder.GetRate();
this.builders.set(builderEnt, buildRate);
this.totalBuilderRate += buildRate;
return true;
};
/**
* @param {number} builderEnt - The entity to add.
*/
Foundation.prototype.AddBuilder = function(builderEnt)
{
if (this.AddBuilderHelper(builderEnt))
this.HandleBuildersChanged();
};
/**
* @param {number} builderEnt - The entity to remove.
*/
Foundation.prototype.RemoveBuilder = function(builderEnt)
{
if (!this.builders.has(builderEnt))
return;
this.totalBuilderRate -= this.builders.get(builderEnt);
this.builders.delete(builderEnt);
this.HandleBuildersChanged();
};
/**
* This has to be called whenever the number of builders change.
*/
Foundation.prototype.HandleBuildersChanged = function()
{
this.SetBuildMultiplier();
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SetVariable("numbuilders", this.GetNumBuilders());
Engine.PostMessage(this.entity, MT_FoundationBuildersChanged, { "to": this.GetBuilders() });
};
/**
* The build multiplier is a penalty that is applied to each builder.
* For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10.
*/
Foundation.prototype.CalculateBuildMultiplier = function(num)
{
// Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
return num < 2 ? 1 : Math.pow(num, this.buildTimePenalty) / num;
};
Foundation.prototype.SetBuildMultiplier = function()
{
this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders());
};
Foundation.prototype.GetBuildTime = function()
{
let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime();
let rate = this.totalBuilderRate * this.buildMultiplier;
let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1);
return {
// Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
"timeRemaining": rate ? timeLeft / rate : 0,
"timeRemainingNew": timeLeft / rateNew
};
};
/**
* @return {boolean} - Whether the foundation has been committed sucessfully.
*/
Foundation.prototype.Commit = function()
{
if (this.committed)
return false;
let cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction && cmpObstruction.GetBlockMovementFlag(true))
{
for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction())
Engine.DestroyEntity(ent);
let collisions = cmpObstruction.GetEntitiesBlockingConstruction();
if (collisions.length)
{
for (let ent of collisions)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.LeaveFoundation(this.entity);
// TODO: What if an obstruction has no UnitAI?
}
// TODO: maybe we should tell the builder to use a special
// animation to indicate they're waiting for people to get
// out the way
return false;
}
}
// The obstruction always blocks new foundations/construction,
// but we've temporarily allowed units to walk all over it
// (via CCmpTemplateManager). Now we need to remove that temporary
// blocker-disabling, so that we'll perform standard unit blocking instead.
if (cmpObstruction)
cmpObstruction.SetDisableBlockMovementPathfinding(false, false, -1);
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ConstructionStarted", {
"foundation": this.entity,
"template": this.finalTemplateName
});
let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpFoundationVisual)
cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0);
this.committed = true;
this.CreateConstructionPreview();
return true;
};
/**
* Perform some number of seconds of construction work.
* Returns true if the construction is completed.
*/
Foundation.prototype.Build = function(builderEnt, work)
{
// Do nothing if we've already finished building
// (The entity will be destroyed soon after completion so
// this won't happen much.)
if (this.IsFinished())
return;
if (!this.committed && !this.Commit())
return;
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (!cmpHealth)
{
error("Foundation " + this.entity + " does not have a health component.");
return;
}
let deltaHP = work * this.GetBuildRate() * this.buildMultiplier;
if (deltaHP > 0)
cmpHealth.Increase(deltaHP);
// Update the total builder rate.
this.totalBuilderRate += work - this.builders.get(builderEnt);
this.builders.set(builderEnt, work);
// Remember our max progress for partial refund in case of destruction.
this.maxProgress = Math.max(this.maxProgress, this.GetBuildProgress());
if (this.maxProgress >= 1.0)
{
let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
let building = ChangeEntityTemplate(this.entity, this.finalTemplateName);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building);
PlaySound("constructed", building);
Engine.PostMessage(this.entity, MT_ConstructionFinished,
{ "entity": this.entity, "newentity": building });
for (let builder of this.GetBuilders())
{
let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI);
if (cmpUnitAIBuilder)
cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": building });
}
}
};
Foundation.prototype.GetBuildRate = function()
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
// Return infinity for instant structure conversion
return cmpHealth.GetMaxHitpoints() / cmpCost.GetBuildTime();
};
/**
* Create preview entity and copy various parameters from the foundation.
*/
Foundation.prototype.CreateConstructionPreview = function()
{
if (this.previewEntity)
{
Engine.DestroyEntity(this.previewEntity);
this.previewEntity = INVALID_ENTITY;
}
if (!this.committed)
return;
let cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpFoundationVisual || !cmpFoundationVisual.HasConstructionPreview())
return;
this.previewEntity = Engine.AddLocalEntity("construction|"+this.finalTemplateName);
let cmpFoundationOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
let cmpPreviewOwnership = Engine.QueryInterface(this.previewEntity, IID_Ownership);
if (cmpFoundationOwnership && cmpPreviewOwnership)
cmpPreviewOwnership.SetOwner(cmpFoundationOwnership.GetOwner());
// TODO: the 'preview' would be invisible if it doesn't have the below component,
// Maybe it makes more sense to simply delete it then?
// Initially hide the preview underground
let cmpPreviewPosition = Engine.QueryInterface(this.previewEntity, IID_Position);
let cmpFoundationPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPreviewPosition && cmpFoundationPosition)
{
let rot = cmpFoundationPosition.GetRotation();
cmpPreviewPosition.SetYRotation(rot.y);
cmpPreviewPosition.SetXZRotation(rot.x, rot.z);
let pos = cmpFoundationPosition.GetPosition2D();
cmpPreviewPosition.JumpTo(pos.x, pos.y);
cmpPreviewPosition.SetConstructionProgress(this.GetBuildProgress());
}
let cmpPreviewVisual = Engine.QueryInterface(this.previewEntity, IID_Visual);
if (cmpPreviewVisual && cmpFoundationVisual)
{
cmpPreviewVisual.SetActorSeed(cmpFoundationVisual.GetActorSeed());
cmpPreviewVisual.SelectAnimation("scaffold", false, 1.0);
}
};
+Foundation.prototype.OnEntityRenamed = function(msg)
+{
+ let cmpFoundationNew = Engine.QueryInterface(msg.newentity, IID_Foundation);
+ if (cmpFoundationNew)
+ cmpFoundationNew.AddBuilders(this.GetBuilders());
+};
+
function FoundationMirage() {}
FoundationMirage.prototype.Init = function(cmpFoundation)
{
this.numBuilders = cmpFoundation.GetNumBuilders();
this.buildTime = cmpFoundation.GetBuildTime();
};
FoundationMirage.prototype.GetNumBuilders = function() { return this.numBuilders; };
FoundationMirage.prototype.GetBuildTime = function() { return this.buildTime; };
Engine.RegisterGlobal("FoundationMirage", FoundationMirage);
Foundation.prototype.Mirage = function()
{
let mirage = new FoundationMirage();
mirage.Init(this);
return mirage;
};
Engine.RegisterComponentType(IID_Foundation, "Foundation", Foundation);
Index: ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25219)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Repairable.js (revision 25220)
@@ -1,173 +1,180 @@
function Repairable() {}
Repairable.prototype.Schema =
"Deals with repairable structures and units." +
"" +
"2.0" +
"" +
"" +
"" +
"";
Repairable.prototype.Init = function()
{
this.builders = new Map(); // Map of builder entities to their work per second
this.totalBuilderRate = 0; // Total amount of work the builders do each second
this.buildMultiplier = 1; // Multiplier for the amount of work builders do
this.buildTimePenalty = 0.7; // Penalty for having multiple builders
this.repairTimeRatio = +this.template.RepairTimeRatio;
};
/**
* Returns the current build progress in a [0,1] range.
*/
Repairable.prototype.GetBuildProgress = function()
{
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (!cmpHealth)
return 0;
var hitpoints = cmpHealth.GetHitpoints();
var maxHitpoints = cmpHealth.GetMaxHitpoints();
return hitpoints / maxHitpoints;
};
/**
* Returns the current builders.
*
* @return {number[]} - An array containing the entity IDs of assigned builders.
*/
Repairable.prototype.GetBuilders = function()
{
return Array.from(this.builders.keys());
};
Repairable.prototype.GetNumBuilders = function()
{
return this.builders.size;
};
/**
* Adds an array of builders.
*
* @param {number[]} - An array containing the entity IDs of builders to assign.
*/
Repairable.prototype.AddBuilders = function(builders)
{
for (let builder of builders)
this.AddBuilder(builder);
};
Repairable.prototype.AddBuilder = function(builderEnt)
{
if (this.builders.has(builderEnt))
return;
this.builders.set(builderEnt, Engine.QueryInterface(builderEnt, IID_Builder).GetRate());
this.totalBuilderRate += this.builders.get(builderEnt);
this.SetBuildMultiplier();
};
Repairable.prototype.RemoveBuilder = function(builderEnt)
{
if (!this.builders.has(builderEnt))
return;
this.totalBuilderRate -= this.builders.get(builderEnt);
this.builders.delete(builderEnt);
this.SetBuildMultiplier();
};
/**
* The build multiplier is a penalty that is applied to each builder.
* For example, ten women build at a combined rate of 10^0.7 = 5.01 instead of 10.
*/
Repairable.prototype.CalculateBuildMultiplier = function(num)
{
// Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
return num < 2 ? 1 : Math.pow(num, this.buildTimePenalty) / num;
};
Repairable.prototype.SetBuildMultiplier = function()
{
this.buildMultiplier = this.CalculateBuildMultiplier(this.GetNumBuilders());
};
Repairable.prototype.GetBuildTime = function()
{
let timeLeft = (1 - this.GetBuildProgress()) * Engine.QueryInterface(this.entity, IID_Cost).GetBuildTime() * this.repairTimeRatio;
let rate = this.totalBuilderRate * this.buildMultiplier;
// The rate if we add another woman to the repairs
let rateNew = (this.totalBuilderRate + 1) * this.CalculateBuildMultiplier(this.GetNumBuilders() + 1);
return {
// Avoid division by zero, in particular 0/0 = NaN which isn't reliably serialized
"timeRemaining": rate ? timeLeft / rate : 0,
"timeRemainingNew": timeLeft / rateNew
};
};
// TODO: should we have resource costs?
Repairable.prototype.Repair = function(builderEnt, rate)
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
if (!cmpHealth || !cmpCost)
return;
let damage = cmpHealth.GetMaxHitpoints() - cmpHealth.GetHitpoints();
if (damage <= 0)
return;
// Calculate the amount of hitpoints that will be added (using diminishing rate when several builders)
let work = rate * this.buildMultiplier * this.GetRepairRate();
let amount = Math.min(damage, work);
cmpHealth.Increase(amount);
// Update the total builder rate
this.totalBuilderRate += rate - this.builders.get(builderEnt);
this.builders.set(builderEnt, rate);
// If we repaired all the damage, send a message to entities to stop repairing this building
if (amount >= damage)
{
Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": this.entity });
// Inform the builders that repairing has finished.
// This not done by listening to a global message due to performance.
for (let builder of this.GetBuilders())
{
let cmpUnitAIBuilder = Engine.QueryInterface(builder, IID_UnitAI);
if (cmpUnitAIBuilder)
cmpUnitAIBuilder.ConstructionFinished({ "entity": this.entity, "newentity": this.entity });
}
}
};
Repairable.prototype.GetRepairRate = function()
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
let cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
let repairTime = this.repairTimeRatio * cmpCost.GetBuildTime();
return repairTime ? cmpHealth.GetMaxHitpoints() / repairTime : 1;
};
+Repairable.prototype.OnEntityRenamed = function(msg)
+{
+ let cmpRepairableNew = Engine.QueryInterface(msg.newentity, IID_Repairable);
+ if (cmpRepairableNew)
+ cmpRepairableNew.AddBuilders(this.GetBuilders());
+};
+
function RepairableMirage() {}
RepairableMirage.prototype.Init = function(cmpRepairable)
{
this.numBuilders = cmpRepairable.GetNumBuilders();
this.buildTime = cmpRepairable.GetBuildTime();
};
RepairableMirage.prototype.GetNumBuilders = function() { return this.numBuilders; };
RepairableMirage.prototype.GetBuildTime = function() { return this.buildTime; };
Engine.RegisterGlobal("RepairableMirage", RepairableMirage);
Repairable.prototype.Mirage = function()
{
let mirage = new RepairableMirage();
mirage.Init(this);
return mirage;
};
Engine.RegisterComponentType(IID_Repairable, "Repairable", Repairable);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 25219)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 25220)
@@ -1,289 +1,284 @@
// 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());
}
// Prevent spawning subunits on occupied positions.
let cmpTurretHolder = Engine.QueryInterface(oldEnt, IID_TurretHolder);
let cmpNewTurretHolder = Engine.QueryInterface(newEnt, IID_TurretHolder);
if (cmpTurretHolder && cmpNewTurretHolder)
for (let entity of cmpTurretHolder.GetEntities())
cmpNewTurretHolder.SetReservedTurretPoint(cmpTurretHolder.GetOccupiedTurretPointName(entity));
let owner;
let cmpTerritoryDecay = Engine.QueryInterface(newEnt, IID_TerritoryDecay);
if (cmpTerritoryDecay && cmpTerritoryDecay.HasTerritoryOwnership() && cmpNewPosition)
{
let pos = cmpNewPosition.GetPosition2D();
let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
owner = cmpTerritoryManager.GetOwner(pos.x, pos.y);
}
else
{
let cmpOwnership = Engine.QueryInterface(oldEnt, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
}
let cmpNewOwnership = Engine.QueryInterface(newEnt, IID_Ownership);
if (cmpNewOwnership)
cmpNewOwnership.SetOwner(owner);
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());
-
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 });
// UnitAI generally needs other components to be properly initialised.
let cmpUnitAI = Engine.QueryInterface(oldEnt, IID_UnitAI);
let cmpNewUnitAI = Engine.QueryInterface(newEnt, IID_UnitAI);
if (cmpUnitAI && cmpNewUnitAI)
{
let pos = cmpUnitAI.GetHeldPosition();
if (pos)
cmpNewUnitAI.SetHeldPosition(pos.x, pos.z);
cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
let guarded = cmpUnitAI.IsGuardOf();
if (guarded)
{
let cmpGuarded = Engine.QueryInterface(guarded, IID_Guard);
if (cmpGuarded)
{
cmpGuarded.RenameGuard(oldEnt, newEnt);
cmpNewUnitAI.SetGuardOf(guarded);
}
}
}
if (cmpPosition && cmpPosition.IsInWorld())
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(oldEnt);
return newEnt;
}
/**
* Copy over the obstruction control group IDs.
* This is needed to ensure that when a group of structures with the same
* control groups is replaced by a new entity, they remains in the same control group(s).
* This is the mechanism that is used to e.g. enable wall pieces to be built closely
* together, ignoring their mutual obstruction shapes (since they would
* otherwise be prevented from being built so closely together).
*/
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.Unload(ent);
if (!cmpNewGarrison)
continue;
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (!cmpGarrisonable)
continue;
cmpGarrisonable.Garrison(newEnt);
}
}
Engine.RegisterGlobal("ChangeEntityTemplate", ChangeEntityTemplate);
Engine.RegisterGlobal("ObstructionsBlockingTemplateChange", ObstructionsBlockingTemplateChange);