Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 27482) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 27483) @@ -1,460 +1,454 @@ 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.buildTimeModifier = +this.template.BuildTimeModifier; 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.buildTimeModifier) / 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("OnConstructionStarted", { "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); - // Make sure the foundation object is the same as the final object. - const cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); - const cmpBuildingVisual = Engine.QueryInterface(building, IID_Visual); - if (cmpVisual && cmpBuildingVisual) - cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed()); - const cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); const cmpBuildingIdentity = Engine.QueryInterface(building, IID_Identity); if (cmpIdentity && cmpBuildingIdentity) { const oldPhenotype = cmpIdentity.GetPhenotype(); if (cmpBuildingIdentity.GetPhenotype() !== oldPhenotype) { cmpBuildingIdentity.SetPhenotype(oldPhenotype); if (cmpVisualCorpse) cmpVisualCorpse.RecomputeActorName(); } } 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/helpers/Transform.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 27482) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Transform.js (revision 27483) @@ -1,293 +1,298 @@ // 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; } Engine.ProfileStart("Transform"); + const cmpVisual = Engine.QueryInterface(oldEnt, IID_Visual); + const cmpNewVisual = Engine.QueryInterface(newEnt, IID_Visual); + if (cmpVisual && cmpNewVisual) + cmpNewVisual.SetActorSeed(cmpVisual.GetActorSeed()); + 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 cmpPromotion = Engine.QueryInterface(oldEnt, IID_Promotion); let cmpNewPromotion = Engine.QueryInterface(newEnt, IID_Promotion); if (cmpPromotion && cmpNewPromotion) { cmpPromotion.SetPromotedEntity(newEnt); 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.SwitchToStance(cmpUnitAI.GetStanceName()); 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.ProfileStop(); 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);