Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20078) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20079) @@ -1,486 +1,486 @@ /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if the classes given in the identity template * match a list of classes * @param classes List of the classes to check against * @param match Either a string in the form * "Class1 Class2+Class3" * where spaces are handled as OR and '+'-signs as AND, * and ! is handled as NOT, thus Class1+!Class2 = Class1 AND NOT Class2 * Or a list in the form * [["Class1"], ["Class2", "Class3"]] * where the outer list is combined as OR, and the inner lists are AND-ed * Or a hybrid format containing a list of strings, where the list is * combined as OR, and the strings are split by space and '+' and AND-ed * * @return undefined if there are no classes or no match object * true if the the logical combination in the match object matches the classes * false otherwise */ function MatchesClassList(classes, match) { if (!match || !classes) return undefined; // transform the string to an array if (typeof match == "string") match = match.split(/\s+/); for (var sublist of match) { // if the elements are still strings, split them by space or by '+' if (typeof sublist == "string") sublist = sublist.split(/[+\s]+/); if (sublist.every(c => (c[0] == "!" && classes.indexOf(c.substr(1)) == -1) || (c[0] != "!" && classes.indexOf(c) != -1))) return true; } return false; } /** * Gets the value originating at the value_path as-is, with no modifiers applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers) current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} resources - An instance of the Resources prototype. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, modifiers={}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Armour) ret.armour = { "hack": getEntityValue("Armour/Hack"), "pierce": getEntityValue("Armour/Pierce"), "crush": getEntityValue("Armour/Crush") }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; if (type == "Capture") ret.attack.Capture = { "value": getAttackStat("Value") }; else { ret.attack[type] = { "hack": getAttackStat("Hack"), "pierce": getAttackStat("Pierce"), "crush": getAttackStat("Crush"), "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus") }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); } ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Splash) ret.attack[type].splash = { "hack": getAttackStat("Splash/Hack"), "pierce": getAttackStat("Splash/Pierce"), "crush": getAttackStat("Splash/Crush"), // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape }; } } if (template.DeathDamage) { ret.deathDamage = { "hack": getEntityValue("DeathDamage/Hack"), "pierce": getEntityValue("DeathDamage/Pierce"), "crush": getEntityValue("DeathDamage/Crush"), "friendlyFire": template.DeathDamage.FriendlyFire != "false" }; } if (template.Auras) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) - ret.buildRestrictions.distance.min = +template.BuildRestrictions.Distance.MinDistance; + ret.buildRestrictions.distance.min = getEntityValue("BuildRestrctions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) - ret.buildRestrictions.distance.max = +template.BuildRestrictions.Distance.MaxDistance; + ret.buildRestrictions.distance.max = getEntityValue("BuildRestrctions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "hp": getEntityValue("Heal/HP"), "range": getEntityValue("Heal/Range"), "rate": getEntityValue("Heal/Rate") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; if (template.UnitMotion.Run) ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort, }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap, }; if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length }; return ret; } /** * Get basic information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects }; } /** * Get information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } Index: ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 20078) +++ ps/trunk/binaries/data/mods/public/simulation/components/BuildRestrictions.js (revision 20079) @@ -1,347 +1,350 @@ function BuildRestrictions() {} BuildRestrictions.prototype.Schema = "Specifies building placement restrictions as they relate to terrain, territories, and distance." + "" + "" + "land" + "own" + "Special" + "" + "CivilCentre" + "40" + "" + "" + "" + "" + "" + "land" + "shore" + "land-shore"+ "" + "" + "" + "" + "" + "" + "own" + "ally" + "neutral" + "enemy" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; BuildRestrictions.prototype.Init = function() { this.territories = this.template.Territory.split(/\s+/); }; /** * Checks whether building placement is valid * 1. Visibility is not hidden (may be fogged or visible) * 2. Check foundation * a. Doesn't obstruct foundation-blocking entities * b. On valid terrain, based on passability class * 3. Territory type is allowed (see note below) * 4. Dock is on shoreline and facing into water * 5. Distance constraints satisfied * * Returns result object: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the GUI message * "translateMessage": always true * "translateParameters": list of parameters to translate * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": plural translation argument (optional) * } * * Note: The entity which is used to check this should be a preview entity * (template name should be "preview|"+templateName), as otherwise territory * checks for buildings with territory influence will not work as expected. */ BuildRestrictions.prototype.CheckPlacement = function() { var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); var name = cmpIdentity ? cmpIdentity.GetGenericName() : "Building"; var result = { "success": false, "message": markForTranslation("%(name)s cannot be built due to unknown error"), "parameters": { "name": name, }, "translateMessage": true, "translateParameters": ["name"], }; // TODO: AI has no visibility info var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer.IsAI()) { // Check whether it's in a visible or fogged region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpRangeManager || !cmpOwnership) return result; // Fail var explored = (cmpRangeManager.GetLosVisibility(this.entity, cmpOwnership.GetOwner()) != "hidden"); if (!explored) { result.message = markForTranslation("%(name)s cannot be built in unexplored area"); return result; // Fail } } // Check obstructions and terrain passability var passClassName = ""; switch (this.template.PlacementType) { case "shore": passClassName = "building-shore"; break; case "land-shore": // 'default-terrain-only' is everywhere a normal unit can go, ignoring // obstructions (i.e. on passable land, and not too deep in the water) passClassName = "default-terrain-only"; break; case "land": default: passClassName = "building-land"; } var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction); if (!cmpObstruction) return result; // Fail if (this.template.Category == "Wall") { // for walls, only test the center point var ret = cmpObstruction.CheckFoundation(passClassName, true); } else { var ret = cmpObstruction.CheckFoundation(passClassName, false); } if (ret != "success") { switch (ret) { case "fail_error": case "fail_no_obstruction": error("CheckPlacement: Error returned from CheckFoundation"); break; case "fail_obstructs_foundation": result.message = markForTranslation("%(name)s cannot be built on another building or resource"); break; case "fail_terrain_class": // TODO: be more specific and/or list valid terrain? result.message = markForTranslation("%(name)s cannot be built on invalid terrain"); } return result; // Fail } // Check territory restrictions var cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!(cmpTerritoryManager && cmpPlayer && cmpPosition && cmpPosition.IsInWorld())) return result; // Fail var pos = cmpPosition.GetPosition2D(); var tileOwner = cmpTerritoryManager.GetOwner(pos.x, pos.y); var isConnected = !cmpTerritoryManager.IsTerritoryBlinking(pos.x, pos.y); var isOwn = tileOwner == cmpPlayer.GetPlayerID(); var isMutualAlly = cmpPlayer.IsExclusiveMutualAlly(tileOwner); var isNeutral = tileOwner == 0; var invalidTerritory = ""; if (isOwn) { if (!this.HasTerritory("own")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "own"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected own"); } else if (isMutualAlly) { if (!this.HasTerritory("ally")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "allied"); else if (!isConnected && !this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "unconnected allied"); } else if (isNeutral) { if (!this.HasTerritory("neutral")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "neutral"); } else { // consider everything else enemy territory if (!this.HasTerritory("enemy")) // Translation: territoryType being displayed in a translated sentence in the form: "House cannot be built in %(territoryType)s territory.". invalidTerritory = markForTranslationWithContext("Territory type", "enemy"); } if (invalidTerritory) { result.message = markForTranslation("%(name)s cannot be built in %(territoryType)s territory. Valid territories: %(validTerritories)s"); result.translateParameters.push("territoryType"); result.translateParameters.push("validTerritories"); result.parameters.territoryType = {"context": "Territory type", "message": invalidTerritory}; // gui code will join this array to a string result.parameters.validTerritories = {"context": "Territory type list", "list": this.GetTerritories()}; return result; // Fail } // Check special requirements if (this.template.PlacementType == "shore") { // TODO: Probably should check unit passability classes here, to determine if: // 1. ships can be spawned "nearby" // 2. builders can pass the terrain where the dock is placed (don't worry about paths) // so it's correct even if the criteria changes for these units var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); if (!cmpFootprint) return result; // Fail // Get building's footprint var shape = cmpFootprint.GetShape(); var halfSize = 0; if (shape.type == "square") halfSize = shape.depth/2; else if (shape.type == "circle") halfSize = shape.radius; var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); if (!cmpTerrain || !cmpWaterManager) return result; // Fail var ang = cmpPosition.GetRotation().y; var sz = halfSize * Math.sin(ang); var cz = halfSize * Math.cos(ang); if ((cmpWaterManager.GetWaterLevel(pos.x + sz, pos.y + cz) - cmpTerrain.GetGroundLevel(pos.x + sz, pos.y + cz)) < 1.0 // front || (cmpWaterManager.GetWaterLevel(pos.x - sz, pos.y - cz) - cmpTerrain.GetGroundLevel(pos.x - sz, pos.y - cz)) > 2.0) // back { result.message = markForTranslation("%(name)s must be built on a valid shoreline"); return result; // Fail } } + + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + + let templateName = cmpTemplateManager.GetCurrentTemplateName(this.entity); + let template = cmpTemplateManager.GetTemplate(templateName.substr(templateName.lastIndexOf("|") + 1)); // Check distance restriction if (this.template.Distance) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); var cat = this.template.Distance.FromClass; var filter = function(id) { var cmpIdentity = Engine.QueryInterface(id, IID_Identity); return cmpIdentity.GetClassesList().indexOf(cat) > -1; }; - if (this.template.Distance.MinDistance) + if (this.template.Distance.MinDistance !== undefined) { - var dist = +this.template.Distance.MinDistance; - var nearEnts = cmpRangeManager.ExecuteQuery(this.entity, 0, dist, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions).filter(filter); - if (nearEnts.length) + let minDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MinDistance", +this.template.Distance.MinDistance, cmpPlayer.GetPlayerID(), template); + if (cmpRangeManager.ExecuteQuery(this.entity, 0, minDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions).some(filter)) { - var result = markForPluralTranslation( + let result = markForPluralTranslation( "%(name)s too close to a %(category)s, must be at least %(distance)s meter away", "%(name)s too close to a %(category)s, must be at least %(distance)s meters away", - +this.template.Distance.MinDistance); + minDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, - "distance": this.template.Distance.MinDistance + "distance": minDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } - if (this.template.Distance.MaxDistance) + if (this.template.Distance.MaxDistance !== undefined) { - var dist = +this.template.Distance.MaxDistance; - var nearEnts = cmpRangeManager.ExecuteQuery(this.entity, 0, dist, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions).filter(filter); - if (!nearEnts.length) + let maxDistance = ApplyValueModificationsToTemplate("BuildRestrictions/Distance/MaxDistance", +this.template.Distance.MaxDistance, cmpPlayer.GetPlayerID(), template); + if (!cmpRangeManager.ExecuteQuery(this.entity, 0, maxDistance, [cmpPlayer.GetPlayerID()], IID_BuildRestrictions).some(filter)) { - var result = markForPluralTranslation( + let result = markForPluralTranslation( "%(name)s too far from a %(category)s, must be within %(distance)s meter", "%(name)s too far from a %(category)s, must be within %(distance)s meters", - +this.template.Distance.MaxDistance); + maxDistance); result.success = false; result.translateMessage = true; result.parameters = { "name": name, "category": cat, - "distance": this.template.Distance.MaxDistance + "distance": maxDistance }; result.translateParameters = ["name", "category"]; return result; // Fail } } } // Success result.success = true; result.message = ""; return result; }; BuildRestrictions.prototype.GetCategory = function() { return this.template.Category; }; BuildRestrictions.prototype.GetTerritories = function() { return ApplyValueModificationsToEntity("BuildRestrictions/Territory", this.territories, this.entity); }; BuildRestrictions.prototype.HasTerritory = function(territory) { return (this.GetTerritories().indexOf(territory) != -1); }; // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "own"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "ally"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "neutral"); // Translation: Territory types being displayed as part of a list like "Valid territories: own, ally". markForTranslationWithContext("Territory type list", "enemy"); Engine.RegisterComponentType(IID_BuildRestrictions, "BuildRestrictions", BuildRestrictions);