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