Index: ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js (revision 25189) +++ ps/trunk/binaries/data/mods/public/maps/scenarios/unit_pushing_test.js (revision 25190) @@ -1,380 +1,380 @@ const REG_UNIT_TEMPLATE = "units/athen/infantry_spearman_b"; const FAST_UNIT_TEMPLATE = "units/athen/cavalry_swordsman_b"; const LARGE_UNIT_TEMPLATE = "units/brit/siege_ram"; const ELE_TEMPLATE = "units/cart/champion_elephant"; const ATTACKER = 2; var QuickSpawn = function(x, z, template, owner = 1) { let ent = Engine.AddEntity(template); let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpEntOwnership) cmpEntOwnership.SetOwner(owner); let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); cmpEntPosition.JumpTo(x, z); return ent; }; var Rotate = function(angle, ent) { let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); cmpEntPosition.SetYRotation(angle); return ent; }; var WalkTo = function(x, z, queued, ent, owner=1) { ProcessCommand(owner, { "type": "walk", "entities": Array.isArray(ent) ? ent : [ent], "x": x, "z": z, "queued": queued, "force": true, }); return ent; }; var FormationWalkTo = function(x, z, queued, ent, owner=1) { ProcessCommand(owner, { "type": "walk", "entities": Array.isArray(ent) ? ent : [ent], "x": x, "z": z, "queued": queued, "force": true, "formation": "special/formations/box" }); return ent; }; var Attack = function(target, ent) { let comm = { "type": "attack", "entities": Array.isArray(ent) ? ent : [ent], "target": target, "queued": true, "force": true, }; ProcessCommand(ATTACKER, comm); return ent; }; var Do = function(name, data, ent, owner = 1) { let comm = { "type": name, "entities": Array.isArray(ent) ? ent : [ent], "queued": false }; for (let k in data) comm[k] = data[k]; ProcessCommand(owner, comm); }; var experiments = {}; experiments.units_sparse_forest_of_units = { "spawn": (gx, gy) => { for (let i = -16; i <= 16; i += 8) for (let j = -16; j <= 16; j += 8) QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE)); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE)); } }; experiments.units_dense_forest_of_units = { "spawn": (gx, gy) => { for (let i = -16; i <= 16; i += 4) for (let j = -16; j <= 16; j += 4) QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE)); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE)); } }; experiments.units_superdense_forest_of_units = { "spawn": (gx, gy) => { for (let i = -6; i <= 6; i += 2) for (let j = -6; j <= 6; j += 2) QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy, REG_UNIT_TEMPLATE)); WalkTo(gx, gy + 100, true, QuickSpawn(gx, gy-10, LARGE_UNIT_TEMPLATE)); } }; experiments.building = { "spawn": (gx, gy) => { let target = QuickSpawn(gx + 20, gy + 20, "foundation|structures/athen/storehouse"); for (let i = 0; i < 8; ++i) Do("repair", { "target": target }, QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE)); let cmpFoundation = Engine.QueryInterface(target, IID_Foundation); - cmpFoundation.InitialiseConstruction(1, "structures/athen/storehouse"); + cmpFoundation.InitialiseConstruction("structures/athen/storehouse"); } }; experiments.collecting_tree = { "spawn": (gx, gy) => { let target = QuickSpawn(gx + 10, gy + 10, "gaia/tree/acacia"); let storehouse = QuickSpawn(gx - 10, gy - 10, "structures/athen/storehouse"); for (let i = 0; i < 8; ++i) Do("gather", { "target": target }, QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE)); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); // Make that tree essentially infinite. cmpModifiersManager.AddModifiers("inf_tree", { "ResourceSupply/Max": [{ "affects": ["Tree"], "replace": 50000 }], }, target); let cmpSupply = Engine.QueryInterface(target, IID_ResourceSupply); cmpSupply.SetAmount(50000); // Make the storehouse a territory root cmpModifiersManager.AddModifiers("root", { "TerritoryInfluence/Root": [{ "affects": ["Structure"], "replace": true }], }, storehouse); // Make units gather instantly cmpModifiersManager.AddModifiers("gatherrate", { "ResourceGatherer/BaseSpeed": [{ "affects": ["Unit"], "replace": 100 }], }, 3); // Player 1 is ent 3 } }; experiments.sep1 = { "spawn": (gx, gy) => {} }; experiments.battle = { "spawn": (gx, gy) => { for (let i = 0; i < 4; ++i) for (let j = 0; j < 8; ++j) { QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE); QuickSpawn(gx + i, gy + 50 + j, REG_UNIT_TEMPLATE, ATTACKER); } } }; experiments.sep2 = { "spawn": (gx, gy) => {} }; experiments.overlapping = { "spawn": (gx, gy) => { for (let i = 0; i < 20; ++i) QuickSpawn(gx, gy, REG_UNIT_TEMPLATE); } }; experiments.multicrossing = { "spawn": (gx, gy) => { for (let i = 0; i < 20; i += 2) for (let j = 0; j < 20; j += 2) WalkTo(gx+10, gy+70, false, QuickSpawn(gx + i, gy + j, REG_UNIT_TEMPLATE)); for (let i = 0; i < 20; i += 2) for (let j = 0; j < 20; j += 2) WalkTo(gx+10, gy, false, QuickSpawn(gx + i, gy + j + 70, REG_UNIT_TEMPLATE)); } }; experiments.elephant_formation = { "spawn": (gx, gy) => { let ents = []; for (let i = 0; i < 20; i += 4) for (let j = 0; j < 20; j += 4) ents.push(QuickSpawn(gx + i, gy + j, ELE_TEMPLATE)); FormationWalkTo(gx, gy+10, false, ents); } }; var perf_experiments = {}; // Perf check: put units everywhere, not moving. perf_experiments.Idle = { "spawn": () => { const spacing = 12; for (let x = 0; x < 20*16*4 - 20; x += spacing) for (let z = 0; z < 20*16*4 - 20; z += spacing) QuickSpawn(x, z, REG_UNIT_TEMPLATE); } }; // Perf check: put units everywhere, moving. perf_experiments.MovingAround = { "spawn": () => { const spacing = 24; for (let x = 0; x < 20*16*4 - 20; x += spacing) for (let z = 0; z < 20*16*4 - 20; z += spacing) { let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE); for (let i = 0; i < 5; ++i) { WalkTo(x + 4, z, true, ent); WalkTo(x + 4, z + 4, true, ent); WalkTo(x, z + 4, true, ent); WalkTo(x, z, true, ent); } } } }; // Perf check: fewer units moving more. perf_experiments.LighterMovingAround = { "spawn": () => { const spacing = 48; for (let x = 0; x < 20*16*4 - 20; x += spacing) for (let z = 0; z < 20*16*4 - 20; z += spacing) { let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE); for (let i = 0; i < 5; ++i) { WalkTo(x + 20, z, true, ent); WalkTo(x + 20, z + 20, true, ent); WalkTo(x, z + 20, true, ent); WalkTo(x, z, true, ent); } } } }; // Perf check: rows of units crossing each other. perf_experiments.BunchaCollisions = { "spawn": () => { const spacing = 64; for (let x = 0; x < 20*16*4 - 20; x += spacing) for (let z = 0; z < 20*16*4 - 20; z += spacing) { for (let i = 0; i < 10; ++i) { // Add a little variation to the spawning, or all clusters end up identical. let ent = QuickSpawn(x + i + randFloat(-0.5, 0.5), z + 20 * (i%2) + randFloat(-0.5, 0.5), REG_UNIT_TEMPLATE); for (let ii = 0; ii < 5; ++ii) { WalkTo(x + i, z + 20, true, ent); WalkTo(x + i, z, true, ent); } } } } }; // Massive moshpit of pushing. perf_experiments.LotsaLocalCollisions = { "spawn": () => { const spacing = 3; for (let x = 100; x < 200; x += spacing) for (let z = 100; z < 200; z += spacing) { let ent = QuickSpawn(x, z, REG_UNIT_TEMPLATE); for (let ii = 0; ii < 20; ++ii) WalkTo(randFloat(100, 200), randFloat(100, 200), true, ent); } } }; var woodcutting = (gx, gy) => { let dropsite = QuickSpawn(gx + 50, gy, "structures/athen/storehouse"); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("root", { "TerritoryInfluence/Root": [{ "affects": ["Structure"], "replace": true }], }, dropsite); // Make units gather faster cmpModifiersManager.AddModifiers("gatherrate", { "ResourceGatherer/BaseSpeed": [{ "affects": ["Unit"], "multiply": 3 }], }, 3); // Player 1 is ent 3 for (let i = 20; i <= 80; i += 10) for (let j = 20; j <= 50; j += 10) QuickSpawn(gx + i, gy + j, "gaia/tree/acacia"); for (let i = 10; i <= 90; i += 5) Do("gather-near-position", { "x": gx + i, "z": gy + 10, "resourceType": { "generic": "wood", "specific": "tree" }, "resourceTemplate": "gaia/tree/acacia" }, QuickSpawn(gx + i, gy, REG_UNIT_TEMPLATE)); }; perf_experiments.WoodCutting = { "spawn": () => { for (let i = 0; i < 8; i++) for (let j = 0; j < 8; j++) { woodcutting(20 + i*100, 20 + j*100); } } }; var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); Trigger.prototype.Setup = function() { let start = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // /* let gx = 100; let gy = 100; for (let key in experiments) { experiments[key].spawn(gx, gy); gx += 60; if (gx > 20*16*4-20) { gx = 20; gy += 100; } } /**/ /* let time = 0; for (let key in perf_experiments) { cmpTrigger.DoAfterDelay(1000 + time * 10000, "RunExperiment", { "exp": key }); time++; } /**/ }; Trigger.prototype.Cleanup = function() { warn("cleanup"); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ents = cmpRangeManager.GetEntitiesByPlayer(1).concat(cmpRangeManager.GetEntitiesByPlayer(2)); for (let ent of ents) Engine.DestroyEntity(ent); }; Trigger.prototype.RunExperiment = function(data) { warn("Start of " + data.exp); perf_experiments[data.exp].spawn(); cmpTrigger.DoAfterDelay(9500, "Cleanup", {}); }; Trigger.prototype.EndGame = function() { Engine.QueryInterface(4, IID_Player).SetState("defeated", "trigger"); Engine.QueryInterface(3, IID_Player).SetState("won", "trigger"); }; /* var cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); // Reduce player 1 vision range (or patrolling units reacct) cmpModifiersManager.AddModifiers("no_promotion", { "Vision/Range": [{ "affects": ["Unit"], "replace": 5 }], }, 3); // player 1 is ent 3 // Prevent promotions, messes up things. cmpModifiersManager.AddModifiers("no_promotion_A", { "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }], }, 3); cmpModifiersManager.AddModifiers("no_promotion_B", { "Promotion/RequiredXp": [{ "affects": ["Unit"], "replace": 50000 }], }, 4); // player 2 is ent 4 */ cmpTrigger.DoAfterDelay(4000, "Setup", {}); cmpTrigger.DoAfterDelay(300000, "EndGame", {}); Index: ps/trunk/binaries/data/mods/public/simulation/components/Cost.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 25189) +++ ps/trunk/binaries/data/mods/public/simulation/components/Cost.js (revision 25190) @@ -1,86 +1,85 @@ function Cost() {} Cost.prototype.Schema = "Specifies the construction/training costs of this entity." + "" + "1" + "20.0" + "" + "50" + "0" + "0" + "25" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + ""; Cost.prototype.Init = function() { this.populationCost = +this.template.Population; }; Cost.prototype.GetPopCost = function() { return this.populationCost; }; Cost.prototype.GetBuildTime = function() { return ApplyValueModificationsToEntity("Cost/BuildTime", +this.template.BuildTime, this.entity); }; -Cost.prototype.GetResourceCosts = function(owner) +Cost.prototype.GetResourceCosts = function() { - if (!owner) + let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership) { - let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); - if (!cmpOwnership) - error("GetResourceCosts called without valid ownership"); - else - owner = cmpOwnership.GetOwner(); + error("GetResourceCosts called without valid ownership on " + this.entity + "."); + return {}; } let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let entityTemplateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); let entityTemplate = cmpTemplateManager.GetTemplate(entityTemplateName); + let owner = cmpOwnership.GetOwner(); let costs = {}; for (let res in this.template.Resources) costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +this.template.Resources[res], owner, entityTemplate); return costs; }; Cost.prototype.OnValueModification = function(msg) { if (msg.component != "Cost") return; // Foundations shouldn't have a pop cost. var cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) return; // update the population costs var newPopCost = Math.round(ApplyValueModificationsToEntity("Cost/Population", +this.template.Population, this.entity)); var popCostDifference = newPopCost - this.populationCost; this.populationCost = newPopCost; var cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; if (popCostDifference) cmpPlayer.AddPopulation(popCostDifference); }; Engine.RegisterComponentType(IID_Cost, "Cost", Cost); Index: ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25189) +++ ps/trunk/binaries/data/mods/public/simulation/components/Foundation.js (revision 25190) @@ -1,553 +1,551 @@ 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(owner, template) +Foundation.prototype.InitialiseConstruction = function(template) { this.finalTemplateName = template; - // We need to know the owner in OnDestroy, but at that point the entity has already been - // decoupled from its owner, so we need to remember it in here (and assume it won't change) - this.owner = owner; - // 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 + // we will use the correct values to refund partial construction costs. let cmpCost = Engine.QueryInterface(this.entity, IID_Cost); if (!cmpCost) - error("A foundation must have a cost component to know the build time"); + error("A foundation, from " + template + ", must have a cost component to know the build time"); - this.costs = cmpCost.GetResourceCosts(owner); + 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) { // Gradually reveal the final building preview 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() { var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) return 0; var hitpoints = cmpHealth.GetHitpoints(); var maxHitpoints = cmpHealth.GetMaxHitpoints(); return hitpoints / maxHitpoints; }; Foundation.prototype.GetBuildPercentage = function() { return Math.floor(this.GetBuildProgress() * 100); }; /** * Returns the current builders. * * @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) return; // Refund a portion of the construction cost, proportional to the amount of build progress remaining if (!this.initialised) // this happens if the foundation was destroyed because the player had insufficient resources return; if (this.previewEntity != INVALID_ENTITY) { Engine.DestroyEntity(this.previewEntity); this.previewEntity = INVALID_ENTITY; } if (this.IsFinished()) return; - let cmpPlayer = QueryPlayerIDInterface(this.owner); + let cmpPlayer = QueryPlayerIDInterface(msg.from); + if (!cmpPlayer) + return; for (var r in this.costs) { var scaled = Math.ceil(this.costs[r] * (1.0 - this.maxProgress)); if (scaled) { cmpPlayer.AddResource(r, scaled); - var cmpStatisticsTracker = QueryPlayerIDInterface(this.owner, IID_StatisticsTracker); + var cmpStatisticsTracker = QueryPlayerIDInterface(msg.from, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -scaled); } } }; /** * Adds an array of builders. * * @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(); }; /** * Adds a single builder to this entity. * * @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; }; /** * Adds a builder to the counter. * * @param {number} builderEnt - The entity to add. */ Foundation.prototype.AddBuilder = function(builderEnt) { if (this.AddBuilderHelper(builderEnt)) this.HandleBuildersChanged(); }; 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.builders.size); 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; // The rate if we add another woman to the foundation. 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 }; }; /** * 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.GetBuildProgress() == 1.0) return; var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); // If there are any units in the way, ask them to move away and return early from this method. if (cmpObstruction && cmpObstruction.GetBlockMovementFlag()) { // Remove animal corpses for (let ent of cmpObstruction.GetEntitiesDeletedUponConstruction()) Engine.DestroyEntity(ent); let collisions = cmpObstruction.GetEntitiesBlockingConstruction(); if (collisions.length) { for (var ent of collisions) { var 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; } } // Handle the initial 'committing' of the foundation if (!this.committed) { // 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); // Call the related trigger event var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("ConstructionStarted", { "foundation": this.entity, "template": this.finalTemplateName }); // Switch foundation to scaffold variant var cmpFoundationVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpFoundationVisual) cmpFoundationVisual.SelectAnimation("scaffold", false, 1.0); this.committed = true; this.CreateConstructionPreview(); } // Add an appropriate proportion of hitpoints var cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (!cmpHealth) { error("Foundation " + this.entity + " does not have a health component."); return; } var 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); var progress = this.GetBuildProgress(); // Remember our max progress for partial refund in case of destruction this.maxProgress = Math.max(this.maxProgress, progress); if (progress >= 1.0) { // Finished construction // Create the real entity var building = Engine.AddEntity(this.finalTemplateName); // Copy various parameters from the foundation var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); var cmpBuildingVisual = Engine.QueryInterface(building, IID_Visual); if (cmpVisual && cmpBuildingVisual) cmpBuildingVisual.SetActorSeed(cmpVisual.GetActorSeed()); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { error("Foundation " + this.entity + " does not have a position in-world."); Engine.DestroyEntity(building); return; } var cmpBuildingPosition = Engine.QueryInterface(building, IID_Position); if (!cmpBuildingPosition) { error("New building " + building + " has no position component."); Engine.DestroyEntity(building); return; } var pos = cmpPosition.GetPosition2D(); cmpBuildingPosition.JumpTo(pos.x, pos.y); var rot = cmpPosition.GetRotation(); cmpBuildingPosition.SetYRotation(rot.y); cmpBuildingPosition.SetXZRotation(rot.x, rot.z); // TODO: should add a ICmpPosition::CopyFrom() instead of all this var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); var cmpBuildingRallyPoint = Engine.QueryInterface(building, IID_RallyPoint); if(cmpRallyPoint && cmpBuildingRallyPoint) { var rallyCoords = cmpRallyPoint.GetPositions(); var rallyData = cmpRallyPoint.GetData(); for (var i = 0; i < rallyCoords.length; ++i) { cmpBuildingRallyPoint.AddPosition(rallyCoords[i].x, rallyCoords[i].z); cmpBuildingRallyPoint.AddData(rallyData[i]); } } // ---------------------------------------------------------------------- var owner; var cmpTerritoryDecay = Engine.QueryInterface(building, IID_TerritoryDecay); if (cmpTerritoryDecay && cmpTerritoryDecay.HasTerritoryOwnership()) { let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); owner = cmpTerritoryManager.GetOwner(pos.x, pos.y); } else { let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership) { error("Foundation " + this.entity + " has no ownership."); Engine.DestroyEntity(building); return; } owner = cmpOwnership.GetOwner(); } var cmpBuildingOwnership = Engine.QueryInterface(building, IID_Ownership); if (!cmpBuildingOwnership) { error("New Building " + building + " has no ownership."); Engine.DestroyEntity(building); return; } cmpBuildingOwnership.SetOwner(owner); /* Copy over the obstruction control group IDs from the foundation entities. This is needed to ensure that when a foundation is completed and replaced by a new entity, it remains in the same control group(s) as any other foundation entities that may surround it. 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). If the control groups are not copied over, the new entity will default to a new control group containing only itself, and will hence block construction of any surrounding foundations that it was previously in the same control group with. Note that this will result in the completed building entities having control group IDs that equal entity IDs of old (and soon to be deleted) foundation entities. This should not have any consequences, however, since the control group IDs are only meant to be unique identifiers, which is still true when reusing the old ones. */ var cmpBuildingObstruction = Engine.QueryInterface(building, IID_Obstruction); if (cmpObstruction && cmpBuildingObstruction) { cmpBuildingObstruction.SetControlGroup(cmpObstruction.GetControlGroup()); cmpBuildingObstruction.SetControlGroup2(cmpObstruction.GetControlGroup2()); } var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) cmpPlayerStatisticsTracker.IncreaseConstructedBuildingsCounter(building); var cmpBuildingHealth = Engine.QueryInterface(building, IID_Health); if (cmpBuildingHealth) cmpBuildingHealth.SetHitpoints(progress * cmpBuildingHealth.GetMaxHitpoints()); PlaySound("constructed", building); Engine.PostMessage(this.entity, MT_ConstructionFinished, { "entity": this.entity, "newentity": building }); Engine.PostMessage(this.entity, MT_EntityRenamed, { "entity": this.entity, "newentity": building }); // 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": building }); } Engine.DestroyEntity(this.entity); } }; 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); } }; 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/tests/test_Foundation.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Foundation.js (revision 25189) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Foundation.js (revision 25190) @@ -1,268 +1,267 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AutoBuildable.js"); Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/Cost.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Population.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("AutoBuildable.js"); Engine.LoadComponentScript("Foundation.js"); Engine.LoadComponentScript("Timer.js"); let player = 1; let playerEnt = 3; let foundationEnt = 20; let previewEnt = 21; let newEnt = 22; let finalTemplate = "structures/athen/civil_centre.xml"; function testFoundation(...mocks) { ResetState(); let foundationHP = 1; let maxHP = 100; let rot = new Vector3D(1, 2, 3); let pos = new Vector2D(4, 5); let cmpFoundation; AddMock(SYSTEM_ENTITY, IID_Trigger, { "CallEvent": () => {}, }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt, }); AddMock(SYSTEM_ENTITY, IID_TerritoryManager, { "GetOwner": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); return player; }, }); Engine.RegisterGlobal("PlaySound", (name, source) => { TS_ASSERT_EQUALS(name, "constructed"); TS_ASSERT_EQUALS(source, newEnt); }); Engine.RegisterGlobal("MT_EntityRenamed", "entityRenamed"); AddMock(foundationEnt, IID_Cost, { "GetBuildTime": () => 50, "GetResourceCosts": () => ({ "wood": 100 }), }); AddMock(foundationEnt, IID_Health, { "GetHitpoints": () => foundationHP, "GetMaxHitpoints": () => maxHP, "Increase": hp => { foundationHP = Math.min(foundationHP + hp, maxHP); cmpFoundation.OnHealthChanged(); }, }); AddMock(foundationEnt, IID_Obstruction, { "GetBlockMovementFlag": () => true, "GetEntitiesBlockingConstruction": () => [], "GetEntitiesDeletedUponConstruction": () => [], "SetDisableBlockMovementPathfinding": () => {}, }); AddMock(foundationEnt, IID_Ownership, { "GetOwner": () => player, }); AddMock(foundationEnt, IID_Position, { "GetPosition2D": () => pos, "GetRotation": () => rot, "SetConstructionProgress": () => {}, "IsInWorld": () => true, }); AddMock(previewEnt, IID_Ownership, { "SetOwner": owner => { TS_ASSERT_EQUALS(owner, player); }, }); AddMock(previewEnt, IID_Position, { "JumpTo": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); }, "SetConstructionProgress": p => {}, "SetYRotation": r => { TS_ASSERT_EQUALS(r, rot.y); }, "SetXZRotation": (rx, rz) => { TS_ASSERT_EQUALS(rx, rot.x); TS_ASSERT_EQUALS(rz, rot.z); }, }); AddMock(newEnt, IID_Ownership, { "SetOwner": owner => { TS_ASSERT_EQUALS(owner, player); }, }); AddMock(newEnt, IID_Position, { "JumpTo": (x, y) => { TS_ASSERT_EQUALS(x, pos.x); TS_ASSERT_EQUALS(y, pos.y); }, "SetYRotation": r => { TS_ASSERT_EQUALS(r, rot.y); }, "SetXZRotation": (rx, rz) => { TS_ASSERT_EQUALS(rx, rot.x); TS_ASSERT_EQUALS(rz, rot.z); }, }); for (let mock of mocks) AddMock(...mock); // INITIALISE Engine.AddLocalEntity = function(template) { TS_ASSERT_EQUALS(template, "construction|" + finalTemplate); return previewEnt; }; cmpFoundation = ConstructComponent(foundationEnt, "Foundation", {}); - cmpFoundation.InitialiseConstruction(player, finalTemplate); + cmpFoundation.InitialiseConstruction(finalTemplate); - TS_ASSERT_EQUALS(cmpFoundation.owner, player); TS_ASSERT_EQUALS(cmpFoundation.finalTemplateName, finalTemplate); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 0); TS_ASSERT_EQUALS(cmpFoundation.initialised, true); // BUILDER COUNT, BUILD RATE, TIME REMAINING AddMock(10, IID_Builder, { "GetRate": () => 1.0 }); AddMock(11, IID_Builder, { "GetRate": () => 1.0 }); let twoBuilderMultiplier = Math.pow(2, cmpFoundation.buildTimePenalty) / 2; let threeBuilderMultiplier = Math.pow(3, cmpFoundation.buildTimePenalty) / 3; TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(1), 1); TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(2), twoBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.CalculateBuildMultiplier(3), threeBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.GetBuildRate(), 2); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 0); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 0); cmpFoundation.AddBuilder(10); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 1); // Foundation starts with 1 hp, so there's 50 * 99/100 = 49.5 seconds left. TS_ASSERT_UNEVAL_EQUALS(cmpFoundation.GetBuildTime(), { 'timeRemaining': 49.5, 'timeRemainingNew': 49.5 / (2 * twoBuilderMultiplier) }); cmpFoundation.AddBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 2); TS_ASSERT_UNEVAL_EQUALS(cmpFoundation.GetBuildTime(), { 'timeRemaining': 49.5 / (2 * twoBuilderMultiplier), 'timeRemainingNew': 49.5 / (3 * threeBuilderMultiplier) }); cmpFoundation.AddBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 2); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, twoBuilderMultiplier); cmpFoundation.RemoveBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); cmpFoundation.RemoveBuilder(11); TS_ASSERT_EQUALS(cmpFoundation.GetNumBuilders(), 1); TS_ASSERT_EQUALS(cmpFoundation.buildMultiplier, 1); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 1); // COMMIT FOUNDATION TS_ASSERT_EQUALS(cmpFoundation.committed, false); let work = 5; cmpFoundation.Build(10, work); TS_ASSERT_EQUALS(cmpFoundation.committed, true); TS_ASSERT_EQUALS(foundationHP, 1 + work * cmpFoundation.GetBuildRate() * cmpFoundation.buildMultiplier); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, foundationHP / maxHP); TS_ASSERT_EQUALS(cmpFoundation.totalBuilderRate, 5); // FINISH CONSTRUCTION Engine.AddEntity = function(template) { TS_ASSERT_EQUALS(template, finalTemplate); return newEnt; }; cmpFoundation.Build(10, 1000); TS_ASSERT_EQUALS(cmpFoundation.maxProgress, 1); TS_ASSERT_EQUALS(foundationHP, maxHP); } testFoundation(); testFoundation([foundationEnt, IID_Visual, { "SetVariable": (key, num) => { TS_ASSERT_EQUALS(key, "numbuilders"); TS_ASSERT(num == 1 || num == 2); }, "SelectAnimation": (name, once, speed) => name, "HasConstructionPreview": () => true, }]); testFoundation([newEnt, IID_TerritoryDecay, { "HasTerritoryOwnership": () => true, }]); testFoundation([playerEnt, IID_StatisticsTracker, { "IncreaseConstructedBuildingsCounter": ent => { TS_ASSERT_EQUALS(ent, newEnt); }, }]); // Test autobuild feature. const foundationEnt2 = 42; let turnLength = 0.2; let currentFoundationHP = 1; let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); AddMock(foundationEnt2, IID_Cost, { "GetBuildTime": () => 50, "GetResourceCosts": () => ({ "wood": 100 }), }); const cmpAutoBuildingFoundation = ConstructComponent(foundationEnt2, "Foundation", {}); AddMock(foundationEnt2, IID_Health, { "GetHitpoints": () => currentFoundationHP, "GetMaxHitpoints": () => 100, "Increase": hp => { currentFoundationHP = Math.min(currentFoundationHP + hp, 100); cmpAutoBuildingFoundation.OnHealthChanged(); }, }); const cmpBuildableAuto = ConstructComponent(foundationEnt2, "AutoBuildable", { "Rate": "1.0" }); -cmpAutoBuildingFoundation.InitialiseConstruction(player, finalTemplate); +cmpAutoBuildingFoundation.InitialiseConstruction(finalTemplate); // We start at 3 cause there is no delay on the first run. cmpTimer.OnUpdate({ "turnLength": turnLength }); for (let i = 0; i < 10; ++i) { if (i == 8) { cmpBuildableAuto.CancelTimer(); TS_ASSERT_EQUALS(cmpAutoBuildingFoundation.GetNumBuilders(), 0); } let currentPercentage = cmpAutoBuildingFoundation.GetBuildPercentage(); cmpTimer.OnUpdate({ "turnLength": turnLength * 5 }); let newPercentage = cmpAutoBuildingFoundation.GetBuildPercentage(); if (i >= 8) TS_ASSERT_EQUALS(currentPercentage, newPercentage); else // Rate * Max Health / Cost. TS_ASSERT_EQUALS(currentPercentage + 2, newPercentage); } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25189) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 25190) @@ -1,1816 +1,1815 @@ // Setting this to true will display some warnings when commands // are likely to fail, which may be useful for debugging AIs var g_DebugCommands = false; function ProcessCommand(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; let data = { "cmpPlayer": cmpPlayer, "controlAllUnits": cmpPlayer.CanControlAllUnits() }; if (cmd.entities) data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits); // TODO: queuing order and forcing formations doesn't really work. // To play nice, we'll still no-formation queued order if units are in formation // but the opposite perhaps ought to be implemented. if (!cmd.queued || cmd.formation == NULL_FORMATION) data.formation = cmd.formation || undefined; // Allow focusing the camera on recent commands let commandData = { "type": "playercommand", "players": [player], "cmd": cmd }; // Save the position, since the GUI event is received after the unit died if (cmd.type == "delete-entities") { let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position); commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D(); } let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification(commandData); // Note: checks of UnitAI targets are not robust enough here, as ownership // can change after the order is issued, they should be checked by UnitAI // when the specific behavior (e.g. attack, garrison) is performed. // (Also it's not ideal if a command silently fails, it's nicer if UnitAI // moves the entities closer to the target before giving up.) // Now handle various commands if (g_Commands[cmd.type]) { var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.CallEvent("PlayerCommand", { "player": player, "cmd": cmd }); g_Commands[cmd.type](player, cmd, data); } else error("Invalid command: unknown command type: "+uneval(cmd)); } var g_Commands = { "aichat": function(player, cmd, data) { var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); var notification = { "players": [player] }; for (var key in cmd) notification[key] = cmd[key]; cmpGuiInterface.PushNotification(notification); }, "cheat": function(player, cmd, data) { Cheat(cmd); }, "collect-treasure": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasure(cmd.target, cmd.autocontinue, cmd.queued); }); }, "collect-treasure-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.autocontinue, cmd.queued); }); }, "diplomacy": function(player, cmd, data) { let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (data.cmpPlayer.GetLockTeams() || cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive()) return; switch(cmd.to) { case "ally": data.cmpPlayer.SetAlly(cmd.player); break; case "neutral": data.cmpPlayer.SetNeutral(cmd.player); break; case "enemy": data.cmpPlayer.SetEnemy(cmd.player); break; default: warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to); } var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "diplomacy", "players": [player], "targetPlayer": cmd.player, "status": cmd.to }); }, "tribute": function(player, cmd, data) { data.cmpPlayer.TributeResource(cmd.player, cmd.amounts); }, "control-all": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - control all units)") }); data.cmpPlayer.SetControlAllUnits(cmd.flag); }, "reveal-map": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - reveal map)") }); // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.SetLosRevealAll(-1, cmd.enable); }, "walk": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront); }); }, "walk-custom": function(player, cmd, data) { for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront); }); }, "walk-to-range": function(player, cmd, data) { // Only used by the AI for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront); } }, "attack-walk": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack-walk-custom": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; for (let ent in data.entities) GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, allowCapture, cmd.queued, cmd.pushFront); }); }, "attack": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; if (g_DebugCommands && !allowCapture && !(IsOwnedByEnemyOfPlayer(player, cmd.target) || IsOwnedByNeutralOfPlayer(player, cmd.target))) warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Attack(cmd.target, allowCapture, cmd.queued, cmd.pushFront); }); }, "patrol": function(player, cmd, data) { let allowCapture = cmd.allowCapture || cmd.allowCapture == null; GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, allowCapture, cmd.queued) ); }, "heal": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target))) warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront); }); }, "repair": function(player, cmd, data) { // This covers both repairing damaged buildings, and constructing unfinished foundations if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront); }); }, "gather": function(player, cmd, data) { if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront); }); }, "gather-near-position": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront); }); }, "returnresource": function(player, cmd, data) { if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target)) warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront); }); }, "back-to-work": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if(!cmpUnitAI || !cmpUnitAI.BackToWork()) notifyBackToWorkFailure(player); } }, "remove-guard": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.RemoveGuard(); } }, "train": function(player, cmd, data) { if (!Number.isInteger(cmd.count) || cmd.count <= 0) { warn("Invalid command: can't train " + uneval(cmd.count) + " units"); return; } // Check entity limits var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var unitCategory = null; if (template.TrainingRestrictions) unitCategory = template.TrainingRestrictions.Category; // Verify that the building(s) can be controlled by the player if (data.entities.length <= 0) { if (g_DebugCommands) warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd)); return; } for (let ent of data.entities) { if (unitCategory) { var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits); if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit)) { if (g_DebugCommands) warn(unitCategory + " train limit is reached: " + uneval(cmd)); continue; } } var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: training requires unresearched technology: " + uneval(cmd)); continue; } var queue = Engine.QueryInterface(ent, IID_ProductionQueue); // Check if the building can train the unit // TODO: the AI API does not take promotion technologies into account for the list // of trainable units (taken directly from the unit template). Here is a temporary fix. if (queue && data.cmpPlayer.IsAI()) { var list = queue.GetEntitiesList(); if (list.indexOf(cmd.template) === -1 && cmd.promoted) { for (var promoted of cmd.promoted) { if (list.indexOf(promoted) === -1) continue; cmd.template = promoted; break; } } } if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1) if ("metadata" in cmd) queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata); else queue.AddItem(cmd.template, "unit", +cmd.count); } }, "research": function(player, cmd, data) { var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template)) { if (g_DebugCommands) warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd)); return; } var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (queue) queue.AddItem(cmd.template, "technology"); }, "stop-production": function(player, cmd, data) { let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue); if (cmpProductionQueue) cmpProductionQueue.RemoveItem(cmd.id); }, "construct": function(player, cmd, data) { TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "construct-wall": function(player, cmd, data) { TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd); }, "delete-entities": function(player, cmd, data) { for (let ent of data.entities) { if (!data.controlAllUnits) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && cmpIdentity.IsUndeletable()) continue; let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable && cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2) continue; let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather()) continue; } let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) { let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health); if (cmpMiragedHealth) cmpMiragedHealth.Kill(); else Engine.DestroyEntity(cmpMirage.parent); Engine.DestroyEntity(ent); continue; } let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth) cmpHealth.Kill(); else Engine.DestroyEntity(ent); } }, "set-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) { if (!cmd.queued) cmpRallyPoint.Unset(); cmpRallyPoint.AddPosition(cmd.x, cmd.z); cmpRallyPoint.AddData(clone(cmd.data)); } } }, "unset-rallypoint": function(player, cmd, data) { for (let ent of data.entities) { var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) cmpRallyPoint.Reset(); } }, "resign": function(player, cmd, data) { data.cmpPlayer.SetState("defeated", markForTranslation("%(player)s has resigned.")); }, "occupy-turret": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => { cmpUnitAI.OccupyTurret(cmd.target, cmd.queued); }); }, "garrison": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront); }); }, "guard": function(player, cmd, data) { if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd)); return; } GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront); }); }, "stop": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.Stop(cmd.queued); }); }, "leave-turret": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } if (notUnloaded) notifyUnloadFailure(player); }, "unload-turrets": function(player, cmd, data) { let notUnloaded = 0; for (let ent of data.entities) { let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder); for (let turret of cmpTurretHolder.GetEntities()) { let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable); if (!cmpTurretable || !cmpTurretable.LeaveTurret()) ++notUnloaded; } } if (notUnloaded) notifyUnloadFailure(player); }, "unload": function(player, cmd, data) { if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits)) { if (g_DebugCommands) warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd)); return; } var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); var notUngarrisoned = 0; // The owner can ungarrison every garrisoned unit if (IsOwnedByPlayer(player, cmd.garrisonHolder)) data.entities = cmd.entities; for (let ent of data.entities) if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent)) ++notUngarrisoned; if (notUngarrisoned != 0) notifyUnloadFailure(player, cmd.garrisonHolder); }, "unload-template": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Only the owner of the garrisonHolder may unload entities from any owners if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits && player != +cmd.owner) continue; if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all)) notifyUnloadFailure(player, garrisonHolder); } } }, "unload-all-by-owner": function(player, cmd, data) { var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player)) notifyUnloadFailure(player, garrisonHolder); } }, "unload-all": function(player, cmd, data) { var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits); for (let garrisonHolder of entities) { var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll()) notifyUnloadFailure(player, garrisonHolder); } }, "alert-raise": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.RaiseAlert(); } }, "alert-end": function(player, cmd, data) { for (let ent of data.entities) { var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) cmpAlertRaiser.EndOfAlert(); } }, "formation": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => { cmpUnitAI.MoveIntoFormation(cmd); }); }, "promote": function(player, cmd, data) { if (!data.cmpPlayer.GetCheatsEnabled()) return; var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": markForTranslation("(Cheat - promoted units)"), "translateMessage": true }); for (let ent of cmd.entities) { var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp()); } }, "stance": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && !cmpUnitAI.IsTurret()) cmpUnitAI.SwitchToStance(cmd.name); } }, "lock-gate": function(player, cmd, data) { for (let ent of data.entities) { var cmpGate = Engine.QueryInterface(ent, IID_Gate); if (!cmpGate) continue; if (cmd.lock) cmpGate.LockGate(); else cmpGate.UnlockGate(); } }, "setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued); }); }, "cancel-setup-trade-route": function(player, cmd, data) { GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => { cmpUnitAI.CancelSetupTradeRoute(cmd.target); }); }, "set-trading-goods": function(player, cmd, data) { data.cmpPlayer.SetTradingGoods(cmd.tradingGoods); }, "barter": function(player, cmd, data) { var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount); }, "set-shading-color": function(player, cmd, data) { // Prevent multiplayer abuse if (!data.cmpPlayer.IsAI()) return; // Debug command to make an entity brightly colored for (let ent of cmd.entities) { var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0 } }, "pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.Pack(cmd.queued, cmd.pushFront); else cmpUnitAI.Unpack(cmd.queued, cmd.pushFront); } }, "cancel-pack": function(player, cmd, data) { for (let ent of data.entities) { var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; if (cmd.pack) cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront); else cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront); } }, "upgrade": function(player, cmd, data) { for (let ent of data.entities) { var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template)) continue; if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template)) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.") }); continue; } // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template)) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd)); continue; } let cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager); let requiredTechnology = cmpUpgrade.GetRequiredTechnology(cmd.template); if (requiredTechnology && (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(requiredTechnology))) { if (g_DebugCommands) warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd)); continue; } cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer); } }, "cancel-upgrade": function(player, cmd, data) { for (let ent of data.entities) { let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) cmpUpgrade.CancelUpgrade(player); } }, "attack-request": function(player, cmd, data) { // Send a chat message to human players var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "aichat", "players": [player], "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."), "translateParameters": ["_player_"], "parameters": { "_player_": cmd.player } }); // And send an attackRequest event to the AIs let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("AttackRequest", cmd); }, "spy-request": function(player, cmd, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => { let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing); return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player); })); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "spy-response", "players": [player], "target": cmd.player, "entity": ent }); if (ent) Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source); else { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy"); IncurBribeCost(template, player, cmd.player, true); // update statistics for failed bribes let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpBribesStatisticsTracker) cmpBribesStatisticsTracker.IncreaseFailedBribesCounter(); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("There are no bribable units"), "translateMessage": true }); } }, "diplomacy-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("DiplomacyRequest", cmd); }, "tribute-request": function(player, cmd, data) { let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); if (cmpAIInterface) cmpAIInterface.PushEvent("TributeRequest", cmd); }, "dialog-answer": function(player, cmd, data) { // Currently nothing. Triggers can read it anyway, and send this // message to any component you like. }, "set-dropsite-sharing": function(player, cmd, data) { for (let ent of data.entities) { let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.IsSharable()) cmpResourceDropsite.SetSharing(cmd.shared); } }, }; /** * Sends a GUI notification about unit(s) that failed to ungarrison. */ function notifyUnloadFailure(player) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Unable to unload unit(s)."), "translateMessage": true }); } /** * Sends a GUI notification about worker(s) that failed to go back to work. */ function notifyBackToWorkFailure(player) { var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("Some unit(s) can't go back to work"), "translateMessage": true }); } /** * Sends a GUI notification about entities that can't be controlled. * @param {number} player - The player-ID of the player that needs to receive this message. */ function notifyOrderFailure(entity, player) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); if (!cmpIdentity) return; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "text", "players": [player], "message": sprintf(markForTranslation("%(unit)s can't be controlled."), { "unit": cmpIdentity.GetGenericName() }), "translateMessage": true }); } /** * Get some information about the formations used by entities. */ function ExtractFormations(ents) { let entities = []; // Entities with UnitAI. let members = {}; // { formationentity: [ent, ent, ...], ... } let templates = {}; // { formationentity: template } for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI) continue; entities.push(ent); let fid = cmpUnitAI.GetFormationController(); if (fid == INVALID_ENTITY) continue; if (!members[fid]) { members[fid] = []; templates[fid] = cmpUnitAI.GetFormationTemplate(); } members[fid].push(ent); } return { "entities": entities, "members": members, "templates": templates }; } /** * Tries to find the best angle to put a dock at a given position * Taken from GuiInterface.js */ function GetDockAngle(template, x, z) { var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return undefined; // Get footprint size var halfSize = 0; if (template.Footprint.Square) halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; else if (template.Footprint.Circle) halfSize = template.Footprint.Circle["@radius"]; /* Find direction of most open water, algorithm: * 1. Pick points in a circle around dock * 2. If point is in water, add to array * 3. Scan array looking for consecutive points * 4. Find longest sequence of consecutive points * 5. If sequence equals all points, no direction can be determined, * expand search outward and try (1) again * 6. Calculate angle using average of sequence */ const numPoints = 16; for (var dist = 0; dist < 4; ++dist) { var waterPoints = []; for (var i = 0; i < numPoints; ++i) { var angle = (i/numPoints)*2*Math.PI; var d = halfSize*(dist+1); var nx = x - d*Math.sin(angle); var nz = z + d*Math.cos(angle); if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) waterPoints.push(i); } var consec = []; var length = waterPoints.length; if (!length) continue; for (var i = 0; i < length; ++i) { var count = 0; for (let j = 0; j < length - 1; ++j) { if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } var start = 0; var count = 0; for (var c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI; } return undefined; } /** * Attempts to construct a building using the specified parameters. * Returns true on success, false on failure. */ function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd) { // Message structure: // { // "type": "construct", // "entities": [...], // entities that will be ordered to construct the building (if applicable) // "template": "...", // template name of the entity being constructed // "x": ..., // "z": ..., // "angle": ..., // "metadata": "...", // AI metadata of the building // "actorSeed": ..., // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable) // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. If specified, must be a valid control group ID (> 0). // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction // // testing to determine placement validity. May be INVALID_ENTITY. // } /* * Construction process: * . Take resources away immediately. * . Create a foundation entity with 1hp, 0% build progress. * . Increase hp and build progress up to 100% when people work on it. * . If it's destroyed, an appropriate fraction of the resource cost is refunded. * . If it's completed, it gets replaced with the real building. */ // Check whether we can control these units var entities = FilterEntityList(cmd.entities, player, controlAllUnits); if (!entities.length) return false; var foundationTemplate = "foundation|" + cmd.template; // Tentatively create the foundation (we might find later that it's a invalid build command) var ent = Engine.AddEntity(foundationTemplate); if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) error("Error creating foundation entity for '" + cmd.template + "'"); return false; } // If it's a dock, get the right angle. var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template); var angle = cmd.angle; if (template.BuildRestrictions.PlacementType === "shore") { let angleDock = GetDockAngle(template, cmd.x, cmd.z); if (angleDock !== undefined) angle = angleDock; } // Move the foundation to the right place var cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(angle); // Set the obstruction control group if needed if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2) { var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); // primary control group must always be valid if (cmd.obstructionControlGroup) { if (cmd.obstructionControlGroup <= 0) warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0"); cmpObstruction.SetControlGroup(cmd.obstructionControlGroup); } if (cmd.obstructionControlGroup2) cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2); } // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (cmpBuildRestrictions) { var ret = cmpBuildRestrictions.CheckPlacement(); if (!ret.success) { if (g_DebugCommands) warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); ret.players = [player]; cmpGuiInterface.PushNotification(ret); // Remove the foundation because the construction was aborted // move it out of world because it's not destroyed immediately. cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } } else error("cmpBuildRestrictions not defined"); // Check entity limits var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) { if (g_DebugCommands) warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); return false; } var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template)) { if (g_DebugCommands) warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd)); var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "text", "players": [player], "message": markForTranslation("The building's technology requirements are not met."), "translateMessage": true }); // Remove the foundation because the construction was aborted cmpPosition.MoveOutOfWorld(); Engine.DestroyEntity(ent); } - // We need the cost after tech and aura modifications - // To calculate this with an entity requires ownership, so use the template instead + // We need the cost after tech and aura modifications. let cmpCost = Engine.QueryInterface(ent, IID_Cost); - let costs = cmpCost.GetResourceCosts(player); + let costs = cmpCost.GetResourceCosts(); if (!cmpPlayer.TrySubtractResources(costs)) { if (g_DebugCommands) warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); Engine.DestroyEntity(ent); cmpPosition.MoveOutOfWorld(); return false; } var cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); // Initialise the foundation var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); - cmpFoundation.InitialiseConstruction(player, cmd.template); + cmpFoundation.InitialiseConstruction(cmd.template); // send Metadata info if any if (cmd.metadata) Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } ); // Tell the units to start building this new entity if (cmd.autorepair) { ProcessCommand(player, { "type": "repair", "entities": entities, "target": ent, "autocontinue": cmd.autocontinue, "queued": cmd.queued, "pushFront": cmd.pushFront, "formation": cmd.formation || undefined }); } return ent; } function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd) { // 'cmd' message structure: // { // "type": "construct-wall", // "entities": [...], // entities that will be ordered to construct the wall (if applicable) // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...) // { // "template": "...", // one of the templates from the wallset // "x": ..., // "z": ..., // "angle": ..., // }, // ... // ], // "wallSet": { // "templates": { // "tower": // tower template name // "long": // long wall segment template name // ... // etc. // }, // "maxTowerOverlap": ..., // "minTowerOverlap": ..., // }, // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall // "autorepair": true, // whether to automatically start constructing/repairing the new foundation // "autocontinue": true, // whether to automatically gather/build/etc after finishing this // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable) // } if (cmd.pieces.length <= 0) return; if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side"); return; } if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower) { error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side"); return; } // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing // towers in the case of snapping). The towers themselves all keep their default unique control groups. // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of // the first tower encountered towards the ending side of the wall (if any). // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes: // // FIRST PASS: // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID // as the primary control group, thus allowing it to be built overlapping the previous piece. // - If we encounter a new tower along the way (which will gain its own control group), do the following: // o First build it using temporarily the same control group of the previous (non-tower) piece // o Set the previous piece's secondary control group to the tower's entity ID // o Restore the primary control group of the constructed tower back its original (unique) value. // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able // to be placed while overlapping the previous piece. // // SECOND PASS: // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this // time registering the right neighbouring tower in each non-tower piece. // first pass; L -> R var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that // the first wall piece can be built while overlapping it. if (cmd.startSnappedEntity) { var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction); if (!cmpSnappedStartObstruction) { error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup(); //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup); } var i = 0; var queued = cmd.queued; var pieces = clone(cmd.pieces); for (; i < pieces.length; ++i) { var piece = pieces[i]; // All wall pieces after the first must be queued. if (i > 0 && !queued) queued = true; // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do // start position snapping (implying that the first entity we build must be a tower) if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY) { if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity)) { error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")"); break; } } var constructPieceCmd = { "type": "construct", "entities": cmd.entities, "template": piece.template, "x": piece.x, "z": piece.z, "angle": piece.angle, "autorepair": cmd.autorepair, "autocontinue": cmd.autocontinue, "queued": queued, // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed // using the control group of the last tower (see comments above). "obstructionControlGroup": lastTowerControlGroup, }; // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's // control group directly at construction time (instead of setting it in the second pass) to allow it to be built // while overlapping the snapped entity. if (i == pieces.length - 1 && cmd.endSnappedEntity) { var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (cmpEndSnappedObstruction) constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup(); } var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd); if (pieceEntityId) { // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later piece.ent = pieceEntityId; // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex if (piece.template == cmd.wallSet.templates.tower) { var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction); var newTowerControlGroup = pieceEntityId; if (i > 0) { //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup); var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction); // TODO: ensure that cmpPreviousObstruction exists // TODO: ensure that the previous obstruction does not yet have a secondary control group set cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup); } // TODO: ensure that cmpTowerObstruction exists cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group lastTowerIndex = i; lastTowerControlGroup = newTowerControlGroup; } } else // failed to build wall piece, abort break; } var lastBuiltPieceIndex = i - 1; var wallComplete = (lastBuiltPieceIndex == pieces.length - 1); // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower). // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any) // as their secondary control groups. lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces // only start off with the ending side's snapped tower's control group if we were able to build the entire wall if (cmd.endSnappedEntity && wallComplete) { var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction); if (!cmpSnappedEndObstruction) { error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component"); return; } lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup(); } for (var j = lastBuiltPieceIndex; j >= 0; --j) { var piece = pieces[j]; if (!piece.ent) { error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'"); continue; } var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction); if (!cmpPieceObstruction) { error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component"); continue; } if (piece.template == cmd.wallSet.templates.tower) { // encountered a tower entity, update the last tower control group lastTowerControlGroup = cmpPieceObstruction.GetControlGroup(); } else { // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'. // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'. var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2(); if (existingSecondaryControlGroup == INVALID_ENTITY) { if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY) { cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup); } } else if (existingSecondaryControlGroup != lastTowerControlGroup) { error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")"); break; } } } } /** * Remove the given list of entities from their current formations. */ function RemoveFromFormation(ents) { let formation = ExtractFormations(ents); for (let fid in formation.members) { let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation) cmpFormation.RemoveMembers(formation.members[fid]); } } /** * Returns a list of UnitAI components, each belonging either to a * selected unit or to a formation entity for groups of the selected units. */ function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate) { // If an individual was selected, remove it from any formation // and command it individually. if (ents.length == 1) { let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI); if (!cmpUnitAI) return []; RemoveFromFormation(ents); return [ cmpUnitAI ]; } let formationUnitAIs = []; // Find what formations the selected entities are currently in, // and default to that unless the formation is forced or it's the null formation // (we want that to reset whatever formations units are in). if (formationTemplate != NULL_FORMATION) { let formation = ExtractFormations(ents); let formationIds = Object.keys(formation.members); if (formationIds.length == 1) { // Selected units either belong to this formation or have no formation. let fid = formationIds[0]; let cmpFormation = Engine.QueryInterface(+fid, IID_Formation); if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length && cmpFormation.GetMemberCount() == formation.entities.length) { cmpFormation.DeleteTwinFormations(); // The whole formation was selected, so reuse its controller for this command. if (!forceTemplate || formationTemplate == formation.templates[fid]) { formationTemplate = formation.templates[fid]; formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)]; } else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate)) formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)]; } else if (cmpFormation && !forceTemplate) { // Just reuse the template. formationTemplate = formation.templates[fid]; } } else if (formationIds.length) { // Check if all entities share a common formation, if so reuse this template. let template = formation.templates[formationIds[0]]; for (let i = 1; i < formationIds.length; ++i) if (formation.templates[formationIds[i]] != template) { template = null; break; } if (template && !forceTemplate) formationTemplate = template; } } // Separate out the units that don't support the chosen formation. let formedUnits = []; let nonformedUnitAIs = []; for (let ent of ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld()) continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); // TODO: We only check if the formation is usable by some units // if we move them to it. We should check if we can use formations // for the other cases. let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION; if (nullFormation || !cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate || NULL_FORMATION)) { if (nullFormation && cmpUnitAI.GetFormationController()) cmpUnitAI.LeaveFormation(cmd.queued || false); nonformedUnitAIs.push(cmpUnitAI); } else formedUnits.push(ent); } if (nonformedUnitAIs.length == ents.length) { // No units support the formation. return nonformedUnitAIs; } if (!formationUnitAIs.length) { // We need to give the selected units a new formation controller. // TODO replace the fixed 60 with something sensible, based on vision range f.e. let formationSeparation = 60; let clusters = ClusterEntities(formedUnits, formationSeparation); let formationEnts = []; for (let cluster of clusters) { RemoveFromFormation(cluster); if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate)) { for (let ent of cluster) nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI)); continue; } // Create the new controller. let formationEnt = Engine.AddEntity(formationTemplate); let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation); formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI)); cmpFormation.SetFormationSeparation(formationSeparation); cmpFormation.SetMembers(cluster); for (let ent of formationEnts) cmpFormation.RegisterTwinFormation(ent); formationEnts.push(formationEnt); let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership); cmpOwnership.SetOwner(player); } } return nonformedUnitAIs.concat(formationUnitAIs); } /** * Group a list of entities in clusters via single-links */ function ClusterEntities(ents, separationDistance) { let clusters = []; if (!ents.length) return clusters; let distSq = separationDistance * separationDistance; let positions = []; // triangular matrix with the (squared) distances between the different clusters // the other half is not initialised let matrix = []; for (let i = 0; i < ents.length; ++i) { matrix[i] = []; clusters.push([ents[i]]); let cmpPosition = Engine.QueryInterface(ents[i], IID_Position); positions.push(cmpPosition.GetPosition2D()); for (let j = 0; j < i; ++j) matrix[i][j] = positions[i].distanceToSquared(positions[j]); } while (clusters.length > 1) { // search two clusters that are closer than the required distance let closeClusters = undefined; for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i) for (let j = i - 1; j >= 0 && !closeClusters; --j) if (matrix[i][j] < distSq) closeClusters = [i,j]; // if no more close clusters found, just return all found clusters so far if (!closeClusters) return clusters; // make a new cluster with the entities from the two found clusters let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]); // calculate the minimum distance between the new cluster and all other remaining // clusters by taking the minimum of the two distances. let distances = []; for (let i = 0; i < clusters.length; ++i) { let a = closeClusters[1]; let b = closeClusters[0]; if (i == a || i == b) continue; let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a]; let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b]; distances.push(Math.min(dist1, dist2)); } // remove the rows and columns in the matrix for the merged clusters, // and the clusters themselves from the cluster list clusters.splice(closeClusters[0],1); clusters.splice(closeClusters[1],1); matrix.splice(closeClusters[0],1); matrix.splice(closeClusters[1],1); for (let i = 0; i < matrix.length; ++i) { if (matrix[i].length > closeClusters[0]) matrix[i].splice(closeClusters[0],1); if (matrix[i].length > closeClusters[1]) matrix[i].splice(closeClusters[1],1); } // add a new row of distances to the matrix and the new cluster clusters.push(newCluster); matrix.push(distances); } return clusters; } function GetFormationRequirements(formationTemplate) { var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate); if (!template.Formation) return false; return { "minCount": +template.Formation.RequiredMemberCount }; } function CanMoveEntsIntoFormation(ents, formationTemplate) { // TODO: should check the player's civ is allowed to use this formation // See simulation/components/Player.js GetFormations() for a list of all allowed formations var requirements = GetFormationRequirements(formationTemplate); if (!requirements) return false; var count = 0; for (let ent of ents) { var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.CanUseFormation(formationTemplate)) continue; ++count; } return count >= requirements.minCount; } /** * Check if player can control this entity * returns: true if the entity is owned by the player and controllable * or control all units is activated, else false */ function CanControlUnit(entity, player, controlAll) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); let canBeControlled = IsOwnedByPlayer(player, entity) && (!cmpIdentity || cmpIdentity.IsControllable()) || controlAll; if (!canBeControlled) notifyOrderFailure(entity, player); return canBeControlled; } /** * @param {number} entity - The entityID to verify. * @param {number} player - The playerID to check against. * @return {boolean}. */ function IsOwnedByPlayerOrMutualAlly(entity, player) { return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity); } /** * Check if player can control this entity * @return {boolean} - True if the entity is valid and controlled by the player * or the entity is owned by an mutualAlly and can be controlled * or control all units is activated, else false. */ function CanPlayerOrAllyControlUnit(entity, player, controlAll) { return CanControlUnit(player, entity, controlAll) || IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity); } /** * @return {boolean} - Whether the owner of this entity can control the entity. */ function CanOwnerControlEntity(entity) { let cmpOwner = QueryOwnerInterface(entity); return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID()); } /** * Filter entities which the player can control. */ function FilterEntityList(entities, player, controlAll) { return entities.filter(ent => CanControlUnit(ent, player, controlAll)); } /** * Filter entities which the player can control or are mutualAlly */ function FilterEntityListWithAllies(entities, player, controlAll) { return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll)); } /** * Incur the player with the cost of a bribe, optionally multiply the cost with * the additionalMultiplier */ function IncurBribeCost(template, player, playerBribed, failedBribe) { let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed); if (!cmpPlayerBribed) return false; let costs = {}; // Additional cost for this owner let multiplier = cmpPlayerBribed.GetSpyCostMultiplier(); if (failedBribe) multiplier *= template.VisionSharing.FailureCostRatio; for (let res in template.Cost.Resources) costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template)); let cmpPlayer = QueryPlayerIDInterface(player); return cmpPlayer && cmpPlayer.TrySubtractResources(costs); } Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements); Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation); Engine.RegisterGlobal("GetDockAngle", GetDockAngle); Engine.RegisterGlobal("ProcessCommand", ProcessCommand); Engine.RegisterGlobal("g_Commands", g_Commands); Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);