Index: ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/globalscripts/Technologies.js (revision 23991) @@ -1,374 +1,374 @@ /** * This file contains shared logic for applying tech modifications in GUI, AI, * and simulation scripts. As such it must be fully deterministic and not store * any global state, but each context should do its own caching as needed. * Also it cannot directly access the simulation and requires data passed to it. */ /** * Returns modified property value modified by the applicable tech * modifications. * * @param modifications array of modificiations * @param classes Array containing the class list of the template. * @param originalValue Number storing the original value. Can also be * non-numeric, but then only "replace" and "tokens" techs can be supported. */ function GetTechModifiedProperty(modifications, classes, originalValue) { if (!modifications.length) return originalValue; // From indicative profiling, splitting in two sub-functions or checking directly // is about as efficient, but splitting makes it easier to report errors. if (typeof originalValue === "string") return GetTechModifiedProperty_string(modifications, classes, originalValue); return GetTechModifiedProperty_numeric(modifications, classes, originalValue); } function GetTechModifiedProperty_numeric(modifications, classes, originalValue) { let multiply = 1; let add = 0; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; if (modification.multiply) multiply *= modification.multiply; else if (modification.add) add += modification.add; else warn("GetTechModifiedProperty: numeric modification format not recognised : " + uneval(modification)); } return originalValue * multiply + add; } function GetTechModifiedProperty_string(modifications, classes, originalValue) { let value = originalValue; for (let modification of modifications) { if (!DoesModificationApply(modification, classes)) continue; if (modification.replace !== undefined) return modification.replace; // Multiple token replacement works, though ordering is not technically guaranteed. // In practice, the order will be that of 'research', which ought to be fine, // and operations like adding tokens are order-independent anyways, // but modders beware if replacement or deletions are implemented. if (modification.tokens !== undefined) value = HandleTokens(value, modification.tokens); else warn("GetTechModifiedProperty: string modification format not recognised : " + uneval(modification)); } return value; } /** * Returns whether the given modification applies to the entity containing the given class list */ function DoesModificationApply(modification, classes) { return MatchesClassList(classes, modification.affects); } /** * Returns a modified list of tokens. * Supports "A>B" to replace A by B, "-A" to remove A, and the rest will add tokens. */ function HandleTokens(originalValue, modification) { let tokens = originalValue === "" ? [] : originalValue.split(/\s+/); let newTokens = modification === "" ? [] : modification.split(/\s+/); for (let token of newTokens) { if (token.indexOf(">") !== -1) { let [oldToken, newToken] = token.split(">"); let index = tokens.indexOf(oldToken); if (index !== -1) tokens[index] = newToken; } else if (token[0] == "-") { let index = tokens.indexOf(token.substr(1)); if (index !== -1) tokens.splice(index, 1); } else tokens.push(token); } return tokens.join(" "); } /** * Derives the technology requirements from a given technology template. * Takes into account the `supersedes` attribute. * - * @param {object} template - The template object. Loading of the template must have already occured. + * @param {Object} template - The template object. Loading of the template must have already occured. * * @return Derived technology requirements. See `InterpretTechRequirements` for object's syntax. */ function DeriveTechnologyRequirements(template, civ) { let requirements = []; if (template.requirements) { let op = Object.keys(template.requirements)[0]; let val = template.requirements[op]; requirements = InterpretTechRequirements(civ, op, val); } if (template.supersedes && requirements) { if (!requirements.length) requirements.push({}); for (let req of requirements) { if (!req.techs) req.techs = []; req.techs.push(template.supersedes); } } return requirements; } /** * Interprets the prerequisite requirements of a technology. * * Takes the initial { key: value } from the short-form requirements object in entity templates, * and parses it into an object that can be more easily checked by simulation and gui. * * Works recursively if needed. * * The returned object is in the form: * ``` * { "techs": ["tech1", "tech2"] }, * { "techs": ["tech3"] } * ``` * or * ``` * { "entities": [[{ * "class": "human", * "number": 2, * "check": "count" * } * or * ``` * false; * ``` * (Or, to translate: * 1. need either both `tech1` and `tech2`, or `tech3` * 2. need 2 entities with the `human` class * 3. cannot research this tech at all) * * @param {string} civ - The civ code * @param {string} operator - The base operation. Can be "civ", "notciv", "tech", "entity", "all" or "any". * @param {mixed} value - The value associated with the above operation. * * @return Object containing the requirements for the given civ, or false if the civ cannot research the tech. */ function InterpretTechRequirements(civ, operator, value) { let requirements = []; switch (operator) { case "civ": return !civ || civ == value ? [] : false; case "notciv": return civ == value ? false : []; case "entity": { let number = value.number || value.numberOfTypes || 0; if (number > 0) requirements.push({ "entities": [{ "class": value.class, "number": number, "check": value.number ? "count" : "variants" }] }); break; } case "tech": requirements.push({ "techs": [value] }); break; case "all": { let civPermitted = undefined; // tri-state (undefined, false, or true) for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) civPermitted = true; else if (civPermitted !== true) civPermitted = false; break; case "notciv": if (!result) return false; break; case "any": if (!result) return false; // else, fall through case "all": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) civPermitted = false; continue; } // else, fall through case "tech": case "entity": { if (result.length) { if (!requirements.length) requirements.push({}); let newRequirements = []; for (let currReq of requirements) for (let res of result) { let newReq = {}; for (let subtype in currReq) newReq[subtype] = currReq[subtype]; for (let subtype in res) { if (!newReq[subtype]) newReq[subtype] = []; newReq[subtype] = newReq[subtype].concat(res[subtype]); } newRequirements.push(newReq); } requirements = newRequirements; } break; } } } if (civPermitted === false) // if and only if false return false; break; } case "any": { let civPermitted = false; for (let subvalue of value) { let newOper = Object.keys(subvalue)[0]; let newValue = subvalue[newOper]; let result = InterpretTechRequirements(civ, newOper, newValue); switch (newOper) { case "civ": if (result) return []; break; case "notciv": if (!result) return false; civPermitted = true; break; case "any": if (!result) { let nullcivreqs = InterpretTechRequirements(null, newOper, newValue); if (!nullcivreqs || !nullcivreqs.length) continue; return false; } // else, fall through case "all": if (!result) continue; civPermitted = true; // else, fall through case "tech": case "entity": for (let res of result) requirements.push(res); break; } } if (!civPermitted && !requirements.length) return false; break; } default: warn("Unknown requirement operator: "+operator); } return requirements; } /** * Determine order of phases. * - * @param {object} phases - The current available store of phases. + * @param {Object} phases - The current available store of phases. * @return {array} List of phases */ function UnravelPhases(phases) { let phaseMap = {}; for (let phaseName in phases) { let phaseData = phases[phaseName]; if (!phaseData.reqs.length || !phaseData.reqs[0].techs || !phaseData.replaces) continue; let myPhase = phaseData.replaces[0]; let reqPhase = phaseData.reqs[0].techs[0]; if (phases[reqPhase] && phases[reqPhase].replaces) reqPhase = phases[reqPhase].replaces[0]; phaseMap[myPhase] = reqPhase; if (!phaseMap[reqPhase]) phaseMap[reqPhase] = undefined; } let phaseList = Object.keys(phaseMap); phaseList.sort((a, b) => phaseList.indexOf(a) - phaseList.indexOf(phaseMap[b])); return phaseList; } Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 23991) @@ -1,552 +1,552 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * @return {string[]} - All the classes for this identity template. */ function GetIdentityClasses(template) { let classString = ""; if (template.Classes && template.Classes._string) classString += " " + template.Classes._string; if (template.VisibleClasses && template.VisibleClasses._string) classString += " " + template.VisibleClasses._string; if (template.Rank) classString += " " + template.Rank; return classString.length > 1 ? classString.substring(1).split(" ") : []; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { return template.VisibleClasses && template.VisibleClasses._string ? template.VisibleClasses._string.split(" ") : []; } /** * Check if a given list of classes matches another list of classes. * Useful f.e. for checking identity 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 (let 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 {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 {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, + * @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 && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), 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 {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} modifiers - Modifications from auto-researched techs, unit upgrades + * @param {Object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. + * @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, 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 = {}; for (let damageType in template.Armour) if (damageType != "Foundation") ret.armour[damageType] = getEntityValue("Armour/" + damageType); } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } if (temp.ApplyStatus) effects.ApplyStatus = temp.ApplyStatus; return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "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].Projectile) ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { 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 = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/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 = { "health": getEntityValue("Heal/Health"), "range": getEntityValue("Heal/Range"), "interval": getEntityValue("Heal/Interval") }; 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); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; ret.speed.run = getEntityValue("UnitMotion/WalkSpeed"); if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } 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, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "_fortress", "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.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(/\s+/); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. - * @param {object} template - A valid template as obtained by loading the tech JSON file. + * @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, "replaces": template.replaces }; } /** * Get information about a technology template. - * @param {object} template - A valid template as obtained by loading the tech JSON file. + * @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; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/common/gamedescription.js (revision 23991) @@ -1,446 +1,446 @@ /** * Highlights the victory condition in the game-description. */ var g_DescriptionHighlight = "orange"; /** * The rating assigned to lobby players who didn't complete a ranked 1v1 yet. */ var g_DefaultLobbyRating = 1200; /** * XEP-0172 doesn't restrict nicknames, but our lobby policy does. * So use this human readable delimiter to separate buddy names in the config file. */ var g_BuddyListDelimiter = ","; /** * Returns the nickname without the lobby rating. */ function splitRatingFromNick(playerName) { let result = /^(\S+)\ \((\d+)\)$/g.exec(playerName); return { "nick": result ? result[1] : playerName, "rating": result ? +result[2] : "" }; } /** * Array of playernames that the current user has marked as buddies. */ var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); /** * Denotes which players are a lobby buddy of the current user. */ var g_BuddySymbol = '•'; /** * Returns a formatted string describing the player assignments. * Needs g_CivData to translate! * - * @param {object} playerDataArray - As known from gamesetup and simstate. + * @param {Object} playerDataArray - As known from gamesetup and simstate. * @param {(string[]|false)} playerStates - One of "won", "defeated", "active" for each player. * @returns {string} */ function formatPlayerInfo(playerDataArray, playerStates) { let playerDescriptions = {}; let playerIdx = 0; for (let playerData of playerDataArray) { if (playerData == null || playerData.Civ && playerData.Civ == "gaia") continue; ++playerIdx; let teamIdx = playerData.Team; let isAI = playerData.AI && playerData.AI != ""; let playerState = playerStates && playerStates[playerIdx] || playerData.State; let isActive = !playerState || playerState == "active"; let playerDescription; if (isAI) { if (playerData.Civ) { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(AIdescription)s, %(state)s)"); } else { if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdescription)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(AIdescription)s, %(state)s)"); } } else { if (playerData.Offline) { // Can only occur in the lobby for now, so no strings with civ needed if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (OFFLINE, %(state)s)"); } else { if (playerData.Civ) if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s)"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(civ)s, %(state)s)"); else if (isActive) // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s"); else // Translation: Describe a player in a selected game, f.e. in the replay- or savegame menu playerDescription = translate("%(playerName)s (%(state)s)"); } } // Sort player descriptions by team if (!playerDescriptions[teamIdx]) playerDescriptions[teamIdx] = []; let playerNick = splitRatingFromNick(playerData.Name).nick; playerDescriptions[teamIdx].push(sprintf(playerDescription, { "playerName": coloredText( (g_Buddies.indexOf(playerNick) != -1 ? g_BuddySymbol + " " : "") + escapeText(playerData.Name), (typeof getPlayerColor == 'function' ? (isAI ? "white" : getPlayerColor(playerNick)) : rgbToGuiColor(playerData.Color || g_Settings.PlayerDefaults[playerIdx].Color))), "civ": !playerData.Civ ? translate("Unknown Civilization") : g_CivData && g_CivData[playerData.Civ] && g_CivData[playerData.Civ].Name ? translate(g_CivData[playerData.Civ].Name) : playerData.Civ, "state": playerState == "defeated" ? translateWithContext("playerstate", "defeated") : translateWithContext("playerstate", "won"), "AIdescription": translateAISettings(playerData) })); } let teams = Object.keys(playerDescriptions); if (teams.indexOf("observer") > -1) teams.splice(teams.indexOf("observer"), 1); let teamDescription = []; // If there are no teams, merge all playersDescriptions if (teams.length == 1) teamDescription.push(playerDescriptions[teams[0]].join("\n")); // If there are teams, merge "Team N:" + playerDescriptions else teamDescription = teams.map(team => { let teamCaption = team == -1 ? translate("No Team") : sprintf(translate("Team %(team)s"), { "team": +team + 1 }); // Translation: Describe players of one team in a selected game, f.e. in the replay- or savegame menu or lobby return sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + teamCaption + "[/font]", "playerDescriptions": playerDescriptions[team].join("\n") }); }); if (playerDescriptions.observer) teamDescription.push(sprintf(translate("%(team)s:\n%(playerDescriptions)s"), { "team": '[font="sans-bold-14"]' + translatePlural("Observer", "Observers", playerDescriptions.observer.length) + "[/font]", "playerDescriptions": playerDescriptions.observer.join("\n") })); return teamDescription.join("\n\n"); } /** * Sets an additional map label, map preview image and describes the chosen gamesettings more closely. * * Requires g_GameAttributes and g_VictoryConditions. */ function getGameDescription(mapCache) { let titles = []; if (!g_GameAttributes.settings.VictoryConditions.length) titles.push({ "label": translateWithContext("victory condition", "Endless Game"), "value": translate("No winner will be determined, even if everyone is defeated.") }); for (let victoryCondition of g_VictoryConditions) { if (g_GameAttributes.settings.VictoryConditions.indexOf(victoryCondition.Name) == -1) continue; let title = translateVictoryCondition(victoryCondition.Name); if (victoryCondition.Name == "wonder") { let wonderDuration = Math.round(g_GameAttributes.settings.WonderDuration); title = sprintf( translatePluralWithContext( "victory condition", "Wonder (%(min)s minute)", "Wonder (%(min)s minutes)", wonderDuration ), { "min": wonderDuration }); } let isCaptureTheRelic = victoryCondition.Name == "capture_the_relic"; if (isCaptureTheRelic) { let relicDuration = Math.round(g_GameAttributes.settings.RelicDuration); title = sprintf( translatePluralWithContext( "victory condition", "Capture the Relic (%(min)s minute)", "Capture the Relic (%(min)s minutes)", relicDuration ), { "min": relicDuration }); } titles.push({ "label": title, "value": victoryCondition.Description }); if (isCaptureTheRelic) titles.push({ "label": translate("Relic Count"), "value": Math.round(g_GameAttributes.settings.RelicCount) }); if (victoryCondition.Name == "regicide") if (g_GameAttributes.settings.RegicideGarrison) titles.push({ "label": translate("Hero Garrison"), "value": translate("Heroes can be garrisoned.") }); else titles.push({ "label": translate("Exposed Heroes"), "value": translate("Heroes cannot be garrisoned and they are vulnerable to raids.") }); } if (g_GameAttributes.settings.RatingEnabled && g_GameAttributes.settings.PlayerData.length == 2) titles.push({ "label": translate("Rated game"), "value": translate("When the winner of this match is determined, the lobby score will be adapted.") }); if (g_GameAttributes.settings.LockTeams) titles.push({ "label": translate("Locked Teams"), "value": translate("Players can't change the initial teams.") }); else titles.push({ "label": translate("Diplomacy"), "value": translate("Players can make alliances and declare war on allies.") }); if (g_GameAttributes.settings.LastManStanding) titles.push({ "label": translate("Last Man Standing"), "value": translate("Only one player can win the game. If the remaining players are allies, the game continues until only one remains.") }); else titles.push({ "label": translate("Allied Victory"), "value": translate("If one player wins, his or her allies win too. If one group of allies remains, they win.") }); let ceasefire = Math.round(g_GameAttributes.settings.Ceasefire); titles.push({ "label": translate("Ceasefire"), "value": ceasefire == 0 ? translate("disabled") : sprintf(translatePlural( "For the first minute, other players will stay neutral.", "For the first %(min)s minutes, other players will stay neutral.", ceasefire), { "min": ceasefire }) }); if (g_GameAttributes.map == "random") titles.push({ "label": translateWithContext("Map Selection", "Random Map"), "value": translate("Randomly select a map from the list.") }); else { titles.push({ "label": translate("Map Name"), "value": mapCache.translateMapName( mapCache.getTranslatableMapName(g_GameAttributes.mapType, g_GameAttributes.map, g_GameAttributes)) }); titles.push({ "label": translate("Map Description"), "value": mapCache.getTranslatedMapDescription(g_GameAttributes.mapType, g_GameAttributes.map) }); } titles.push({ "label": translate("Map Type"), "value": g_MapTypes.Title[g_MapTypes.Name.indexOf(g_GameAttributes.mapType)] }); if (g_GameAttributes.mapType == "random") { let mapSize = g_MapSizes.Name[g_MapSizes.Tiles.indexOf(g_GameAttributes.settings.Size)]; if (mapSize) titles.push({ "label": translate("Map Size"), "value": mapSize }); } if (g_GameAttributes.settings.Biome) { let biome = g_Settings.Biomes.find(b => b.Id == g_GameAttributes.settings.Biome); titles.push({ "label": biome ? biome.Title : translateWithContext("biome", "Random Biome"), "value": biome ? biome.Description : translate("Randomly select a biome from the list.") }); } if (g_GameAttributes.settings.TriggerDifficulty !== undefined) { let triggerDifficulty = g_Settings.TriggerDifficulties.find(difficulty => difficulty.Difficulty == g_GameAttributes.settings.TriggerDifficulty); titles.push({ "label": triggerDifficulty.Title, "value": triggerDifficulty.Tooltip }); } if (g_GameAttributes.settings.Nomad !== undefined) titles.push({ "label": g_GameAttributes.settings.Nomad ? translate("Nomad Mode") : translate("Civic Centers"), "value": g_GameAttributes.settings.Nomad ? translate("Players start with only few units and have to find a suitable place to build their city.") : translate("Players start with a Civic Center.") }); if (g_GameAttributes.settings.StartingResources !== undefined) titles.push({ "label": translate("Starting Resources"), "value": g_GameAttributes.settings.PlayerData && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.Resources !== undefined) ? translateWithContext("starting resources", "Per Player") : sprintf(translate("%(startingResourcesTitle)s (%(amount)s)"), { "startingResourcesTitle": g_StartingResources.Title[ g_StartingResources.Resources.indexOf( g_GameAttributes.settings.StartingResources)], "amount": g_GameAttributes.settings.StartingResources }) }); if (g_GameAttributes.settings.PopulationCap !== undefined) titles.push({ "label": translate("Population Limit"), "value": g_GameAttributes.settings.PlayerData && g_GameAttributes.settings.PlayerData.some(pData => pData && pData.PopulationLimit !== undefined) ? translateWithContext("population limit", "Per Player") : g_PopulationCapacities.Title[ g_PopulationCapacities.Population.indexOf( g_GameAttributes.settings.PopulationCap)] }); if (g_GameAttributes.settings.WorldPopulationCap !== undefined) titles.push({ "label": translate("World Population Cap"), "value": g_WorldPopulationCapacities.Title[ g_WorldPopulationCapacities.Population.indexOf( g_GameAttributes.settings.WorldPopulationCap)] }); titles.push({ "label": translate("Treasures"), "value": g_GameAttributes.settings.DisableTreasures ? translateWithContext("treasures", "Disabled") : translateWithContext("treasures", "As defined by the map.") }); titles.push({ "label": translate("Revealed Map"), "value": g_GameAttributes.settings.RevealMap }); titles.push({ "label": translate("Explored Map"), "value": g_GameAttributes.settings.ExploreMap }); titles.push({ "label": translate("Cheats"), "value": g_GameAttributes.settings.CheatsEnabled }); return titles.map(title => sprintf(translate("%(label)s %(details)s"), { "label": coloredText(title.label, g_DescriptionHighlight), "details": title.value === true ? translateWithContext("gamesetup option", "enabled") : title.value || translateWithContext("gamesetup option", "disabled") })).join("\n"); } /** * Sets the win/defeat icon to indicate current player's state. */ function setOutcomeIcon(state, image) { if (state == "won") { image.sprite = "stretched:session/icons/victory.png"; image.tooltip = translate("Victorious"); } else if (state == "defeated") { image.sprite = "stretched:session/icons/defeat.png"; image.tooltip = translate("Defeated"); } } function translateAISettings(playerData) { if (!playerData.AI) return ""; return sprintf(translate("%(AIdifficulty)s %(AIbehavior)s %(AIname)s"), { "AIname": translateAIName(playerData.AI), "AIdifficulty": translateAIDifficulty(playerData.AIDiff), "AIbehavior": translateAIBehavior(playerData.AIBehavior), }); } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/ReferencePage.js (revision 23991) @@ -1,67 +1,67 @@ /** * This class contains code common to the Structure Tree, Template Viewer, and any other "Reference Page" that may be added in the future. */ class ReferencePage { constructor() { this.civData = loadCivData(true, false); this.TemplateLoader = new TemplateLoader(); this.TemplateLister = new TemplateLister(this.TemplateLoader); this.TemplateParser = new TemplateParser(this.TemplateLoader); this.activeCiv = this.TemplateLoader.DefaultCiv; this.currentTemplateLists = {}; } setActiveCiv(civCode) { if (civCode == this.TemplateLoader.DefaultCiv) return; this.activeCiv = civCode; this.currentTemplateLists = this.TemplateLister.compileTemplateLists(this.activeCiv, this.civData); this.TemplateParser.deriveModifications(this.activeCiv); this.TemplateParser.derivePhaseList(this.currentTemplateLists.techs.keys(), this.activeCiv); } /** * Concatanates the return values of the array of passed functions. * - * @param {object} template + * @param {Object} template * @param {array} textFunctions * @param {string} joiner * @return {string} The built text. */ static buildText(template, textFunctions=[], joiner="\n") { return textFunctions.map(func => func(template)).filter(tip => tip).join(joiner); } } ReferencePage.prototype.IconPath = "session/portraits/"; /** * List of functions that get the statistics of any template or entity, * formatted in such a way as to appear in a tooltip. * * The functions listed are defined in gui/common/tooltips.js */ ReferencePage.prototype.StatsFunctions = [ getHealthTooltip, getHealerTooltip, getAttackTooltip, getSplashDamageTooltip, getArmorTooltip, getGarrisonTooltip, getProjectilesTooltip, getSpeedTooltip, getGatherTooltip, getResourceSupplyTooltip, getPopulationBonusTooltip, getResourceTrickleTooltip, getLootTooltip ]; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLister.js (revision 23991) @@ -1,137 +1,137 @@ /** * This class compiles and stores lists of which templates can be built/trained/researched by other templates. */ class TemplateLister { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; this.templateLists = new Map(); } /** * Compile lists of templates buildable/trainable/researchable of a given civ. * - * @param {object} civCode - * @param {object} civData - Data defining every civ in the game. + * @param {Object} civCode + * @param {Object} civData - Data defining every civ in the game. */ compileTemplateLists(civCode, civData) { if (this.hasTemplateLists(civCode)) return this.templateLists.get(civCode); let templatesToParse = civData[civCode].StartEntities.map(entity => entity.Template); let templateLists = { "units": new Map(), "structures": new Map(), "techs": new Map(), "wallsetPieces": new Map() }; do { const templatesThisIteration = templatesToParse; templatesToParse = []; for (let templateBeingParsed of templatesThisIteration) { let list = this.deriveTemplateListsFromTemplate(templateBeingParsed, civCode); for (let type in list) for (let templateName of list[type]) if (!templateLists[type].has(templateName)) { templateLists[type].set(templateName, [templateBeingParsed]); if (type != "techs") templatesToParse.push(templateName); } else if (templateLists[type].get(templateName).indexOf(templateBeingParsed) == -1) templateLists[type].get(templateName).push(templateBeingParsed); } } while (templatesToParse.length); // Expand/filter tech pairs for (let [techCode, researcherList] of templateLists.techs) { if (!this.TemplateLoader.isPairTech(techCode)) continue; for (let subTech of this.TemplateLoader.loadTechnologyPairTemplate(techCode, civCode).techs) if (!templateLists.techs.has(subTech)) templateLists.techs.set(subTech, researcherList); else for (let researcher of researcherList) if (templateLists.techs.get(subTech).indexOf(researcher) == -1) templateLists.techs.get(subTech).push(researcher); templateLists.techs.delete(techCode); } // Remove wallset pieces, as they've served their purpose. delete templateLists.wallsetPieces; this.templateLists.set(civCode, templateLists); return this.templateLists.get(civCode); } /** * Returns a civ's template list. * * Note: this civ must have gone through the compilation process above! * * @param {string} civCode - * @return {object} containing lists of template names, grouped by type. + * @return {Object} containing lists of template names, grouped by type. */ getTemplateLists(civCode) { if (this.hasTemplateLists(civCode)) return this.templateLists.get(civCode); error("Template lists of \"" + civCode + "\" requested, but this civ has not been loaded."); return {}; } /** * Returns whether the civ of the given civCode has been loaded into cache. * * @param {string} civCode * @return {boolean} */ hasTemplateLists(civCode) { return this.templateLists.has(civCode); } /** * Compiles lists of buildable, trainable, or researchable entities from * a named template. */ deriveTemplateListsFromTemplate(templateName, civCode) { if (!templateName || !Engine.TemplateExists(templateName)) return {}; // If this is a non-promotion variant (ie. {civ}_support_female_citizen_house) // then it is functionally equivalent to another unit being processed, so skip it. if (this.TemplateLoader.getBaseTemplateName(templateName, civCode) != templateName) return {}; let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); let templateLists = this.TemplateLoader.deriveProductionQueue(template, civCode); templateLists.structures = this.TemplateLoader.deriveBuildQueue(template, civCode); if (template.WallSet) { templateLists.wallsetPieces = []; for (let segment in template.WallSet.Templates) { segment = template.WallSet.Templates[segment].replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(segment)) templateLists.wallsetPieces.push(segment); } } return templateLists; } } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateLoader.js (revision 23991) @@ -1,262 +1,262 @@ /** * This class handles the loading of files. */ class TemplateLoader { constructor() { /** * Raw Data Caches. */ this.auraData = {}; this.technologyData = {}; this.templateData = {}; /** * Partly-composed data. */ this.autoResearchTechList = this.findAllAutoResearchedTechs(); } /** * Loads raw aura template. * * Loads from local cache if available, else from file system. * * @param {string} templateName - * @return {object} Object containing raw template data. + * @return {Object} Object containing raw template data. */ loadAuraTemplate(templateName) { if (!(templateName in this.auraData)) { let data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json"); translateObjectKeys(data, this.AuraTranslateKeys); this.auraData[templateName] = data; } return this.auraData[templateName]; } /** * Loads raw entity template. * * Loads from local cache if data present, else from file system. * * @param {string} templateName * @param {string} civCode - * @return {object} Object containing raw template data. + * @return {Object} Object containing raw template data. */ loadEntityTemplate(templateName, civCode) { if (!(templateName in this.templateData)) { // We need to clone the template because we want to perform some translations. let data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, this.EntityTranslateKeys); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) this.loadAuraTemplate(auraID); if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode) warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " + "This does not match the currently selected civ \"" + civCode + "\"."); this.templateData[templateName] = data; } return this.templateData[templateName]; } /** * Loads raw technology template. * * Loads from local cache if available, else from file system. * * @param {string} templateName - * @return {object} Object containing raw template data. + * @return {Object} Object containing raw template data. */ loadTechnologyTemplate(templateName) { if (!(templateName in this.technologyData)) { let data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json"); translateObjectKeys(data, this.TechnologyTranslateKeys); this.technologyData[templateName] = data; } return this.technologyData[templateName]; } /** * @param {string} templateName * @param {string} civCode - * @return {object} Contains a list and the requirements of the techs in the pair + * @return {Object} Contains a list and the requirements of the techs in the pair */ loadTechnologyPairTemplate(templateName, civCode) { let template = this.loadTechnologyTemplate(templateName); return { "techs": [template.top, template.bottom], "reqs": DeriveTechnologyRequirements(template, civCode) }; } deriveProductionQueue(template, civCode) { let production = { "techs": [], "units": [] }; if (!template.ProductionQueue) return production; if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) for (let templateName of template.ProductionQueue.Entities._string.split(" ")) { templateName = templateName.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(templateName)) production.units.push(this.getBaseTemplateName(templateName, civCode)); } let appendTechnology = (technologyName) => { let technology = this.loadTechnologyTemplate(technologyName, civCode); if (DeriveTechnologyRequirements(technology, civCode)) production.techs.push(technologyName); }; if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) for (let technologyName of template.ProductionQueue.Technologies._string.split(" ")) { if (technologyName.indexOf("{civ}") != -1) { let civTechName = technologyName.replace("{civ}", civCode); technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic"); } if (this.isPairTech(technologyName)) { let technologyPair = this.loadTechnologyPairTemplate(technologyName, civCode); if (technologyPair.reqs) for (technologyName of technologyPair.techs) appendTechnology(technologyName); } else appendTechnology(technologyName); } return production; } deriveBuildQueue(template, civCode) { let buildQueue = []; if (!template.Builder || !template.Builder.Entities._string) return buildQueue; for (let build of template.Builder.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, civCode); if (Engine.TemplateExists(build)) buildQueue.push(build); } return buildQueue; } deriveModifications(civCode) { let techData = []; for (let techName of this.autoResearchTechList) techData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode)); return DeriveModificationsFromTechnologies(techData); } /** * Crudely iterates through every tech JSON file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ findAllAutoResearchedTechs() { let techList = []; for (let templateName of listFiles(this.TechnologyPath, ".json", true)) { let data = this.loadTechnologyTemplate(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } /** * Returns the name of a template's base form (without `_house`, `_trireme`, or similar), * or the template's own name if the base is of a different promotion rank. */ getBaseTemplateName(templateName, civCode) { if (!templateName || !Engine.TemplateExists(templateName)) return undefined; templateName = removeFiltersFromTemplateName(templateName); let template = this.loadEntityTemplate(templateName, civCode); if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName)) return templateName; let parentTemplate = this.loadEntityTemplate(template["@parent"], civCode); if (parentTemplate.Identity && parentTemplate.Identity.Rank && parentTemplate.Identity.Rank != template.Identity.Rank) return templateName; if (!parentTemplate.Cost) return templateName; if (parentTemplate.Upgrade) for (let upgrade in parentTemplate.Upgrade) if (parentTemplate.Upgrade[upgrade].Entity) return templateName; for (let res in parentTemplate.Cost.Resources) if (+parentTemplate.Cost.Resources[res]) return this.getBaseTemplateName(template["@parent"], civCode); return templateName; } isPairTech(technologyCode) { return !!this.loadTechnologyTemplate(technologyCode).top; } isPhaseTech(technologyCode) { return basename(technologyCode).startsWith("phase"); } } /** * Paths to certain files. * * It might be nice if we could get these from somewhere, instead of having them hardcoded here. */ TemplateLoader.prototype.AuraPath = "simulation/data/auras/"; TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/"; TemplateLoader.prototype.DefaultCiv = "gaia"; /** * Keys of template values that are to be translated on load. */ TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"]; TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"]; TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"]; Index: ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/TemplateParser.js (revision 23991) @@ -1,323 +1,323 @@ /** * This class parses and stores parsed template data. */ class TemplateParser { constructor(TemplateLoader) { this.TemplateLoader = TemplateLoader; /** * Parsed Data Stores */ this.entities = {}; this.techs = {}; this.phases = {}; this.modifiers = {}; this.phaseList = []; } /** * Load and parse a structure, unit, resource, etc from its entity template file. * * @param {string} templateName * @param {string} civCode * @return {(object|null)} Sanitized object about the requested template or null if entity template doesn't exist. */ getEntity(templateName, civCode) { if (!(civCode in this.entities)) this.entities[civCode] = {}; else if (templateName in this.entities[civCode]) return this.entities[civCode][templateName]; if (!Engine.TemplateExists(templateName)) return null; let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode); let parsed = GetTemplateDataHelper(template, null, this.TemplateLoader.auraData, this.modifiers[civCode] || {}); parsed.name.internal = templateName; parsed.history = template.Identity.History; parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode); if (template.Builder) parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode); // Set the minimum phase that this entity is available. // For gaia objects, this is meaningless. if (!parsed.requiredTechnology) parsed.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(parsed.requiredTechnology)) parsed.phase = this.getActualPhase(parsed.requiredTechnology); else parsed.phase = this.getPhaseOfTechnology(parsed.requiredTechnology, civCode); if (template.Identity.Rank) parsed.promotion = { "current_rank": template.Identity.Rank, "entity": template.Promotion && template.Promotion.Entity }; if (template.ResourceSupply) parsed.supply = { "type": template.ResourceSupply.Type.split("."), "amount": template.ResourceSupply.Amount, }; if (parsed.upgrades) parsed.upgrades = this.getActualUpgradeData(parsed.upgrades, civCode); if (parsed.wallSet) { parsed.wallset = {}; if (!parsed.upgrades) parsed.upgrades = []; // Note: An assumption is made here that wall segments all have the same armor and auras let struct = this.getEntity(parsed.wallSet.templates.long, civCode); parsed.armour = struct.armour; parsed.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = this.getEntity(parsed.wallSet.templates.tower, civCode); parsed.techCostMultiplier = struct.techCostMultiplier; let health; for (let wSegm in parsed.wallSet.templates) { if (wSegm == "fort" || wSegm == "curves") continue; let wPart = this.getEntity(parsed.wallSet.templates[wSegm], civCode); parsed.wallset[wSegm] = wPart; for (let research of wPart.production.techs) parsed.production.techs.push(research); if (wPart.upgrades) Array.prototype.push.apply(parsed.upgrades, wPart.upgrades); if (["gate", "tower"].indexOf(wSegm) != -1) continue; if (!health) { health = { "min": wPart.health, "max": wPart.health }; continue; } health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (parsed.wallSet.templates.curves) for (let curve of parsed.wallSet.templates.curves) { let wPart = this.getEntity(curve, civCode); health.min = Math.min(health.min, wPart.health); health.max = Math.max(health.max, wPart.health); } if (health.min == health.max) parsed.health = health.min; else parsed.health = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": health.min, "health_max": health.max }); } this.entities[civCode][templateName] = parsed; return parsed; } /** * Load and parse technology from json template. * * @param {string} technologyName * @param {string} civCode - * @return {object} Sanitized data about the requested technology. + * @return {Object} Sanitized data about the requested technology. */ getTechnology(technologyName, civCode) { if (!TechnologyTemplateExists(technologyName)) return null; if (this.TemplateLoader.isPhaseTech(technologyName) && technologyName in this.phases) return this.phases[technologyName]; if (!(civCode in this.techs)) this.techs[civCode] = {}; else if (technologyName in this.techs[civCode]) return this.techs[civCode][technologyName]; let template = this.TemplateLoader.loadTechnologyTemplate(technologyName); let tech = GetTechnologyDataHelper(template, civCode, g_ResourceData); tech.name.internal = technologyName; if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = this.mergeRequirements(tech.reqs, this.TemplateLoader.loadTechnologyPairTemplate(template.pair).reqs); } if (this.TemplateLoader.isPhaseTech(technologyName)) { tech.actualPhase = technologyName; if (tech.replaces !== undefined) tech.actualPhase = tech.replaces[0]; this.phases[technologyName] = tech; } else this.techs[civCode][technologyName] = tech; return tech; } /** * @param {string} phaseCode * @param {string} civCode - * @return {object} Sanitized object containing phase data + * @return {Object} Sanitized object containing phase data */ getPhase(phaseCode, civCode) { return this.getTechnology(phaseCode, civCode); } /** * Provided with an array containing basic information about possible * upgrades, such as that generated by globalscript's GetTemplateDataHelper, * this function loads the actual template data of the upgrades, overwrites * certain values within, then passes an array containing the template data * back to caller. */ getActualUpgradeData(upgradesInfo, civCode) { let newUpgrades = []; for (let upgrade of upgradesInfo) { upgrade.entity = upgrade.entity.replace(/\{(civ|native)\}/g, civCode); let data = GetTemplateDataHelper(this.TemplateLoader.loadEntityTemplate(upgrade.entity, civCode), null, this.TemplateLoader.auraData); data.name.internal = upgrade.entity; data.cost = upgrade.cost; data.icon = upgrade.icon || data.icon; data.tooltip = upgrade.tooltip || data.tooltip; data.requiredTechnology = upgrade.requiredTechnology || data.requiredTechnology; if (!data.requiredTechnology) data.phase = this.phaseList[0]; else if (this.TemplateLoader.isPhaseTech(data.requiredTechnology)) data.phase = this.getActualPhase(data.requiredTechnology); else data.phase = this.getPhaseOfTechnology(data.requiredTechnology, civCode); newUpgrades.push(data); } return newUpgrades; } /** * Determines and returns the phase in which a given technology can be * first researched. Works recursively through the given tech's * pre-requisite and superseded techs if necessary. * * @param {string} techName - The Technology's name * @param {string} civCode * @return The name of the phase the technology belongs to, or false if * the current civ can't research this tech */ getPhaseOfTechnology(techName, civCode) { let phaseIdx = -1; if (basename(techName).startsWith("phase")) { if (!this.phases[techName].reqs) return false; phaseIdx = this.phaseList.indexOf(this.getActualPhase(techName)); if (phaseIdx > 0) return this.phaseList[phaseIdx - 1]; } let techReqs = this.getTechnology(techName, civCode).reqs; if (!techReqs) return false; for (let option of techReqs) if (option.techs) for (let tech of option.techs) { if (basename(tech).startsWith("phase")) return tech; if (basename(tech).startsWith("pair")) continue; phaseIdx = Math.max(phaseIdx, this.phaseList.indexOf(this.getPhaseOfTechnology(tech, civCode))); } return this.phaseList[phaseIdx] || false; } /** * Returns the actual phase a certain phase tech represents or stands in for. * * For example, passing `phase_city_athen` would result in `phase_city`. * * @param {string} phaseName * @return {string} */ getActualPhase(phaseName) { if (this.phases[phaseName]) return this.phases[phaseName].actualPhase; warn("Unrecognized phase (" + phaseName + ")"); return this.phaseList[0]; } getModifiers(civCode) { return this.modifiers[civCode]; } deriveModifications(civCode) { this.modifiers[civCode] = this.TemplateLoader.deriveModifications(civCode); } derivePhaseList(technologyList, civCode) { // Load all of a civ's specific phase technologies for (let techcode of technologyList) if (this.TemplateLoader.isPhaseTech(techcode)) this.getTechnology(techcode, civCode); this.phaseList = UnravelPhases(this.phases); // Make sure all required generic phases are loaded and parsed for (let phasecode of this.phaseList) this.getTechnology(phasecode, civCode); } mergeRequirements(reqsA, reqsB) { if (!reqsA || !reqsB) return false; let finalReqs = clone(reqsA); for (let option of reqsB) for (let type in option) for (let opt in finalReqs) { if (!finalReqs[opt][type]) finalReqs[opt][type] = []; Array.prototype.push.apply(finalReqs[opt][type], option[type]); } return finalReqs; } } Index: ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/structree/structree.js (revision 23991) @@ -1,14 +1,14 @@ /** - * Initialize the page + * Initialize the page. * - * @param {object} data - Parameters passed from the code that calls this page into existence. + * @param {Object} data - Parameters passed from the code that calls this page into existence. */ function init(data = {}) { g_Page = new StructreePage(data); if (data.civ) g_Page.civSelection.selectCiv(data.civ); else g_Page.civSelection.selectFirstCiv(); } Index: ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/reference/viewer/viewer.js (revision 23991) @@ -1,19 +1,19 @@ /** * Override style so we can get a bigger specific name. */ g_TooltipTextFormats.nameSpecificBig.font = "sans-bold-20"; g_TooltipTextFormats.nameSpecificSmall.font = "sans-bold-16"; g_TooltipTextFormats.nameGeneric.font = "sans-bold-16"; /** * Page initialisation. May also eventually pre-draw/arrange objects. * - * @param {object} data - Contains the civCode and the name of the template to display. + * @param {Object} data - Contains the civCode and the name of the template to display. * @param {string} data.templateName * @param {string} [data.civ] */ function init(data) { g_Page = new ViewerPage(); g_Page.selectTemplate(data); } Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 23991) @@ -1,813 +1,813 @@ const g_IsReplay = Engine.IsVisualReplay(); const g_CivData = loadCivData(false, true); const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities); const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities); const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources); const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions; var g_Ambient; var g_Chat; var g_Cheats; var g_DeveloperOverlay; var g_DiplomacyColors; var g_DiplomacyDialog; var g_GameSpeedControl; var g_Menu; var g_MiniMapPanel; var g_NetworkStatusOverlay; var g_ObjectivesDialog; var g_OutOfSyncNetwork; var g_OutOfSyncReplay; var g_PanelEntityManager; var g_PauseControl; var g_PauseOverlay; var g_PlayerViewControl; var g_QuitConfirmationDefeat; var g_QuitConfirmationReplay; var g_RangeOverlayManager; var g_ResearchProgress; var g_TimeNotificationOverlay; var g_TopPanel; var g_TradeDialog; /** * Map, player and match settings set in gamesetup. */ const g_GameAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes")); /** * True if this is a multiplayer game. */ const g_IsNetworked = Engine.HasNetClient(); /** * Is this user in control of game settings (i.e. is a network server, or offline player). */ var g_IsController = !g_IsNetworked || Engine.HasNetServer(); /** * Whether we have finished the synchronization and * can start showing simulation related message boxes. */ var g_IsNetworkedActive = false; /** * True if the connection to the server has been lost. */ var g_Disconnected = false; /** * True if the current user has observer capabilities. */ var g_IsObserver = false; /** * True if the current user has rejoined (or joined the game after it started). */ var g_HasRejoined = false; /** * The playerID selected in the change perspective tool. */ var g_ViewedPlayer = Engine.GetPlayerID(); /** * True if the camera should focus on attacks and player commands * and select the affected units. */ var g_FollowPlayer = false; /** * Cache the basic player data (name, civ, color). */ var g_Players = []; /** * Last time when onTick was called(). * Used for animating the main menu. */ var g_LastTickTime = Date.now(); /** * Recalculate which units have their status bars shown with this frequency in milliseconds. */ var g_StatusBarUpdate = 200; /** * For restoring selection, order and filters when returning to the replay menu */ var g_ReplaySelectionData; /** * Remembers which clients are assigned to which player slots. * The keys are GUIDs or "local" in single-player. */ var g_PlayerAssignments; /** * Whether the entire UI should be hidden (useful for promotional screenshots). * Can be toggled with a hotkey. */ var g_ShowGUI = true; /** * Whether status bars should be shown for all of the player's units. */ var g_ShowAllStatusBars = false; /** * Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update). */ var g_SimState; var g_EntityStates = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_ResourceData = new Resources(); /** * These handlers are called each time a new turn was simulated. * Use this as sparely as possible. */ var g_SimulationUpdateHandlers = new Set(); /** * These handlers are called after the player states have been initialized. */ var g_PlayersInitHandlers = new Set(); /** * These handlers are called when a player has been defeated or won the game. */ var g_PlayerFinishedHandlers = new Set(); /** * These events are fired whenever the player added or removed entities from the selection. */ var g_EntitySelectionChangeHandlers = new Set(); /** * These events are fired when the user has performed a hotkey assignment change. * Currently only fired on init, but to be fired from any hotkey editor dialog. */ var g_HotkeyChangeHandlers = new Set(); /** * List of additional entities to highlight. */ var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; /** * Order in which the panel entities are shown. */ var g_PanelEntityOrder = ["Hero", "Relic"]; /** * Unit classes to be checked for the idle-worker-hotkey. */ var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"]; /** * Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey. */ var g_MilitaryTypes = ["Melee", "Ranged"]; function GetSimState() { if (!g_SimState) g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState")); return g_SimState; } function GetMultipleEntityStates(ents) { if (!ents.length) return null; let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents); for (let item of entityStates) g_EntityStates[item.entId] = item.state && deepfreeze(item.state); return entityStates; } function GetEntityState(entId) { if (!g_EntityStates[entId]) { let entityState = Engine.GuiInterfaceCall("GetEntityState", entId); g_EntityStates[entId] = entityState && deepfreeze(entityState); } return g_EntityStates[entId]; } /** * Returns template data calling GetTemplateData defined in GuiInterface.js * and deepfreezing returned object. * @param {string} templateName - Data of this template will be returned. * @param {number|undefined} player - Modifications of this player will be applied to the template. * If undefined, id of player calling this method will be used. */ function GetTemplateData(templateName, player) { if (!(templateName in g_TemplateData)) { let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player }); translateObjectKeys(template, ["specific", "generic", "tooltip"]); g_TemplateData[templateName] = deepfreeze(template); } return g_TemplateData[templateName]; } function GetTechnologyData(technologyName, civ) { if (!g_TechnologyData[civ]) g_TechnologyData[civ] = {}; if (!(technologyName in g_TechnologyData[civ])) { let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData); translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]); g_TechnologyData[civ][technologyName] = deepfreeze(template); } return g_TechnologyData[civ][technologyName]; } function init(initData, hotloadData) { if (!g_Settings) { Engine.EndGame(); Engine.SwitchGuiPage("page_pregame.xml"); return; } // Fallback used by atlas g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } }; // Fallback used by atlas and autostart games if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name) g_PlayerAssignments.local.name = singleplayerName(); if (initData) { g_ReplaySelectionData = initData.replaySelectionData; g_HasRejoined = initData.isRejoining; if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); } let mapCache = new MapCache(); g_Cheats = new Cheats(); g_DiplomacyColors = new DiplomacyColors(); g_PlayerViewControl = new PlayerViewControl(); g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl)); g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects); g_PauseControl = new PauseControl(); g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay); g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates); g_Ambient = new Ambient(); g_Chat = new Chat(g_PlayerViewControl, g_Cheats); g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection); g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors); g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl); g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat); g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes); g_NetworkStatusOverlay = new NetworkStatusOverlay(); g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache); g_OutOfSyncNetwork = new OutOfSyncNetwork(); g_OutOfSyncReplay = new OutOfSyncReplay(); g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder); g_PauseOverlay = new PauseOverlay(g_PauseControl); g_QuitConfirmationDefeat = new QuitConfirmationDefeat(); g_QuitConfirmationReplay = new QuitConfirmationReplay(); g_RangeOverlayManager = new RangeOverlayManager(g_Selection); g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection); g_TradeDialog = new TradeDialog(g_PlayerViewControl); g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl); g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl); initBatchTrain(); initSelectionPanels(); LoadModificationTemplates(); updatePlayerData(); initializeMusic(); // before changing the perspective Engine.SetBoundingBoxDebugOverlay(false); for (let handler of g_PlayersInitHandlers) handler(); for (let handler of g_HotkeyChangeHandlers) handler(); if (hotloadData) { g_Selection.selected = hotloadData.selection; g_PlayerAssignments = hotloadData.playerAssignments; g_Players = hotloadData.player; } // TODO: use event instead onSimulationUpdate(); setTimeout(displayGamestateNotifications, 1000); } function registerPlayersInitHandler(handler) { g_PlayersInitHandlers.add(handler); } function registerPlayersFinishedHandler(handler) { g_PlayerFinishedHandlers.add(handler); } function registerSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.add(handler); } function unregisterSimulationUpdateHandler(handler) { g_SimulationUpdateHandlers.delete(handler); } function registerEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.add(handler); } function unregisterEntitySelectionChangeHandler(handler) { g_EntitySelectionChangeHandlers.delete(handler); } function registerHotkeyChangeHandler(handler) { g_HotkeyChangeHandlers.add(handler); } function updatePlayerData() { let simState = GetSimState(); if (!simState) return; let playerData = []; for (let i = 0; i < simState.players.length; ++i) { let playerState = simState.players[i]; playerData.push({ "name": playerState.name, "civ": playerState.civ, "color": { "r": playerState.color.r * 255, "g": playerState.color.g * 255, "b": playerState.color.b * 255, "a": playerState.color.a * 255 }, "team": playerState.team, "teamsLocked": playerState.teamsLocked, "cheatsEnabled": playerState.cheatsEnabled, "state": playerState.state, "isAlly": playerState.isAlly, "isMutualAlly": playerState.isMutualAlly, "isNeutral": playerState.isNeutral, "isEnemy": playerState.isEnemy, "guid": undefined, // network guid for players controlled by hosts "offline": g_Players[i] && !!g_Players[i].offline }); } for (let guid in g_PlayerAssignments) { let playerID = g_PlayerAssignments[guid].player; if (!playerData[playerID]) continue; playerData[playerID].guid = guid; playerData[playerID].name = g_PlayerAssignments[guid].name; } g_Players = playerData; } /** * Returns the entity itself except when garrisoned where it returns its garrisonHolder */ function getEntityOrHolder(ent) { let entState = GetEntityState(ent); if (entState && !entState.position && entState.unitAI && entState.unitAI.orders.length && entState.unitAI.orders[0].type == "Garrison") return getEntityOrHolder(entState.unitAI.orders[0].data.target); return ent; } function initializeMusic() { initMusic(); if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music) global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music); global.music.setState(global.music.states.PEACE); } function resetTemplates() { // Update GUI and clear player-dependent cache g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); // TODO: do this more selectively onSimulationUpdate(); } /** * Returns true if the player with that ID is in observermode. */ function isPlayerObserver(playerID) { let playerStates = GetSimState().players; return !playerStates[playerID] || playerStates[playerID].state != "active"; } /** * Returns true if the current user can issue commands for that player. */ function controlsPlayer(playerID) { let playerStates = GetSimState().players; return !!playerStates[Engine.GetPlayerID()] && playerStates[Engine.GetPlayerID()].controlsAll || Engine.GetPlayerID() == playerID && !!playerStates[playerID] && playerStates[playerID].state != "defeated"; } /** * Called when one or more players have won or were defeated. * * @param {array} - IDs of the players who have won or were defeated. - * @param {object} - a plural string stating the victory reason. + * @param {Object} - a plural string stating the victory reason. * @param {boolean} - whether these players have won or lost. */ function playersFinished(players, victoryString, won) { addChatMessage({ "type": "playerstate", "message": victoryString, "players": players }); updatePlayerData(); // TODO: The other calls in this function should move too for (let handler of g_PlayerFinishedHandlers) handler(players, won); if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning()) return; global.music.setState( won ? global.music.states.VICTORY : global.music.states.DEFEAT ); } function resumeGame() { g_PauseControl.implicitResume(); } function closeOpenDialogs() { g_Menu.close(); g_Chat.closePage(); g_DiplomacyDialog.close(); g_ObjectivesDialog.close(); g_TradeDialog.close(); } function endGame() { // Before ending the game let replayDirectory = Engine.GetCurrentReplayDirectory(); let simData = Engine.GuiInterfaceCall("GetReplayMetadata"); let playerID = Engine.GetPlayerID(); Engine.EndGame(); // After the replay file was closed in EndGame // Done here to keep EndGame small if (!g_IsReplay) Engine.AddReplayToCache(replayDirectory); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "sim": simData, "gui": { "dialog": false, "assignedPlayer": playerID, "disconnected": g_Disconnected, "isReplay": g_IsReplay, "replayDirectory": !g_HasRejoined && replayDirectory, "replaySelectionData": g_ReplaySelectionData } }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { "selection": g_Selection.selected, "playerAssignments": g_PlayerAssignments, "player": g_Players, }; } function getSavedGameData() { return { "groups": g_Groups.groups }; } function restoreSavedGameData(data) { // Restore camera if any if (data.camera) Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ, data.camera.RotX, data.camera.RotY, data.camera.Zoom); // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (let groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } /** * Called every frame. */ function onTick() { if (!g_Settings) return; let now = Date.now(); let tickLength = now - g_LastTickTime; g_LastTickTime = now; handleNetMessages(); updateCursorAndTooltip(); if (g_Selection.dirty) { g_Selection.dirty = false; // When selection changed, get the entityStates of new entities GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId])); for (let handler of g_EntitySelectionChangeHandlers) handler(); updateGUIObjects(); // Display rally points for selected structures. if (Engine.GetPlayerID() != -1) Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength) recalculateStatusBarDisplay(); updateTimers(); Engine.GuiInterfaceCall("ClearRenamedEntities"); } function onSimulationUpdate() { // Templates change depending on technologies and auras, so they have to be reloaded after such a change. // g_TechnologyData data never changes, so it shouldn't be deleted. g_EntityStates = {}; if (Engine.GuiInterfaceCall("IsTemplateModified")) { g_TemplateData = {}; Engine.GuiInterfaceCall("ResetTemplateModified"); } g_SimState = undefined; // Some changes may require re-rendering the selection. if (Engine.GuiInterfaceCall("IsSelectionDirty")) { g_Selection.onChange(); Engine.GuiInterfaceCall("ResetSelectionDirty"); } if (!GetSimState()) return; GetMultipleEntityStates(g_Selection.toList()); for (let handler of g_SimulationUpdateHandlers) handler(); // TODO: Move to handlers updateCinemaPath(); handleNotifications(); updateGUIObjects(); } function toggleGUI() { g_ShowGUI = !g_ShowGUI; updateCinemaPath(); } function updateCinemaPath() { let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected; Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath; Engine.Renderer_SetSilhouettesEnabled(!isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true"); } // TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead function updateGUIObjects() { g_Selection.update(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateGroups(); updateSelectionDetails(); updateBuildingPlacementPreview(); if (!g_IsObserver) { // Update music state on basis of battle state. let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer); if (battleState) global.music.setState(global.music.states[battleState]); } } function updateGroups() { g_Groups.update(); // Determine the sum of the costs of a given template let getCostSum = (ent) => { let cost = GetTemplateData(GetEntityState(ent).template).cost; return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0; }; for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children) { Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i; let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]"); button.hidden = g_Groups.groups[i].getTotalCount() == 0; button.onPress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i); button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i); button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i); // Choose the icon of the most common template (or the most costly if it's not unique) if (g_Groups.groups[i].getTotalCount() > 0) { let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => { if (pre.ents.length == cur.ents.length) return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur; return pre.ents.length > cur.ents.length ? pre : cur; }).ents[0]).template).icon; Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite = icon ? ("stretched:session/portraits/" + icon) : "groupsIcon"; } setPanelObjectPosition(button, i, 1); } } /** * Toggles the display of status bars for all of the player's entities. * * @param {Boolean} remove - Whether to hide all previously shown status bars. */ function recalculateStatusBarDisplay(remove = false) { let entities; if (g_ShowAllStatusBars && !remove) entities = g_ViewedPlayer == -1 ? Engine.PickNonGaiaEntitiesOnScreen() : Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer); else { let selected = g_Selection.toList(); for (let ent in g_Selection.highlighted) selected.push(g_Selection.highlighted[ent]); // Remove selected entities from the 'all entities' array, // to avoid disabling their status bars. entities = Engine.GuiInterfaceCall( g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", { "viewedPlayer": g_ViewedPlayer }).filter(idx => selected.indexOf(idx) == -1); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars && !remove, "showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true", "showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true" }); } function removeStatusBarDisplay() { if (g_ShowAllStatusBars) recalculateStatusBarDisplay(true); } /** * Inverts the given configuration boolean and returns the current state. * For example "silhouettes". */ function toggleConfigBool(configName) { let enabled = Engine.ConfigDB_GetValue("user", configName) != "true"; Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg"); return enabled; } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { let entsAdd = []; // list of entities units to be highlighted let entsRemove = []; let highlighted = g_Selection.toList(); for (let ent in g_Selection.highlighted) highlighted.push(g_Selection.highlighted[ent]); if (g_ShowGuarding) // flag the guarding entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.guard || !state.guard.entities.length) continue; for (let ent of state.guard.entities) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } if (g_ShowGuarded) // flag the guarded entities to add in this additional highlight for (let sel in g_Selection.selected) { let state = GetEntityState(g_Selection.selected[sel]); if (!state.unitAI || !state.unitAI.isGuarding) continue; let ent = state.unitAI.isGuarding; if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1) entsAdd.push(ent); } // flag the entities to remove (from the previously added) from this additional highlight for (let ent of g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd, g_HighlightedAlpha, true); _setHighlight(entsRemove, 0, false); g_AdditionalHighlight = entsAdd; } Index: ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 23991) @@ -1,414 +1,414 @@ /** * Heightmap manipulation functionality * * A heightmapt is an array of width arrays of height floats * Width and height is normally mapSize+1 (Number of vertices is one bigger than number of tiles in each direction) * The default heightmap is g_Map.height (See the Map object) * * @warning - Ambiguous naming and potential confusion: * To use this library use TILE_CENTERED_HEIGHT_MAP = false (default) * Otherwise TILE_CENTERED_HEIGHT_MAP has nothing to do with any tile centered map in this library * @todo - TILE_CENTERED_HEIGHT_MAP should be removed and g_Map.height should never be tile centered */ /** * Get the height range of a heightmap * @param {array} [heightmap=g_Map.height] - The reliefmap the minimum and maximum height should be determined for - * @return {object} Height range with 2 floats in properties "min" and "max" + * @return {Object} Height range with 2 floats in properties "min" and "max" */ function getMinAndMaxHeight(heightmap = g_Map.height) { let height = { "min": Infinity, "max": -Infinity }; for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) { height.min = Math.min(height.min, heightmap[x][y]); height.max = Math.max(height.max, heightmap[x][y]); } return height; } /** * Rescales a heightmap so its minimum and maximum height is as the arguments told preserving it's global shape * @param {Number} [minHeight=MIN_HEIGHT] - Minimum height that should be used for the resulting heightmap * @param {Number} [maxHeight=MAX_HEIGHT] - Maximum height that should be used for the resulting heightmap * @param {array} [heightmap=g_Map.height] - A reliefmap * @todo Add preserveCostline to leave a certain height untoucht and scale below and above that seperately */ function rescaleHeightmap(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, heightmap = g_Map.height) { let oldHeightRange = getMinAndMaxHeight(heightmap); for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight); return heightmap; } /** * Translates the heightmap by the given vector, i.e. moves the heights in that direction. * * @param {Vector2D} offset - A vector indicating direction and distance. * @param {Number} [defaultHeight] - The elevation to be set for vertices that don't have a corresponding location on the source heightmap. * @param {Array} [heightmap=g_Map.height] - A reliefmap */ function translateHeightmap(offset, defaultHeight = undefined, heightmap = g_Map.height) { if (defaultHeight === undefined) defaultHeight = getMinAndMaxHeight(heightmap).min; offset.round(); let sourceHeightmap = clone(heightmap); for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) heightmap[x][y] = sourceHeightmap[x + offset.x] !== undefined && sourceHeightmap[x + offset.x][y + offset.y] !== undefined ? sourceHeightmap[x + offset.x][y + offset.y] : defaultHeight; return heightmap; } /** * Get start location with the largest minimum distance between players - * @param {object} [heightRange] - The height range start locations are allowed + * @param {Object} [heightRange] - The height range start locations are allowed * @param {integer} [maxTries=1000] - How often random player distributions are rolled to be compared * @param {Number} [minDistToBorder=20] - How far start locations have to be away from the map border * @param {integer} [numberOfPlayers=g_MapSettings.PlayerData.length] - How many start locations should be placed * @param {array} [heightmap=g_Map.height] - The reliefmap for the start locations to be placed on * @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular * @return {Vector2D[]} */ function getStartLocationsByHeightmap(heightRange, maxTries = 1000, minDistToBorder = 20, numberOfPlayers = g_MapSettings.PlayerData.length - 1, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap) { let validStartLoc = []; let mapCenter = g_Map.getCenter(); let mapSize = g_Map.getSize(); let heightConstraint = new HeightConstraint(heightRange.min, heightRange.max); for (let x = minDistToBorder; x < mapSize - minDistToBorder; ++x) for (let y = minDistToBorder; y < mapSize - minDistToBorder; ++y) { let position = new Vector2D(x, y); if (heightConstraint.allows(position) && (!isCircular || position.distanceTo(mapCenter)) < mapSize / 2 - minDistToBorder) validStartLoc.push(position); } let maxMinDist = 0; let finalStartLoc; for (let tries = 0; tries < maxTries; ++tries) { let startLoc = []; let minDist = Infinity; for (let p = 0; p < numberOfPlayers; ++p) startLoc.push(pickRandom(validStartLoc)); for (let p1 = 0; p1 < numberOfPlayers - 1; ++p1) for (let p2 = p1 + 1; p2 < numberOfPlayers; ++p2) { let dist = startLoc[p1].distanceTo(startLoc[p2]); if (dist < minDist) minDist = dist; } if (minDist > maxMinDist) { maxMinDist = minDist; finalStartLoc = startLoc; } } return finalStartLoc; } /** * Sets the heightmap to a relatively realistic shape * The function doubles the size of the initial heightmap (if given, else a random 2x2 one) until it's big enough, then the extend is cut off * @note min/maxHeight will not necessarily be present in the heightmap * @note On circular maps the edges (given by initialHeightmap) may not be in the playable map area * @note The impact of the initial heightmap depends on its size and target map size * @param {Number} [minHeight=MIN_HEIGHT] - Lower limit of the random height to be rolled * @param {Number} [maxHeight=MAX_HEIGHT] - Upper limit of the random height to be rolled * @param {array} [initialHeightmap] - Optional, Small (e.g. 3x3) heightmap describing the global shape of the map e.g. an island [[MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MAX_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT]] * @param {Number} [smoothness=0.5] - Float between 0 (rough, more local structures) to 1 (smoother, only larger scale structures) * @param {array} [heightmap=g_Map.height] - The reliefmap that will be set by this function */ function setBaseTerrainDiamondSquare(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, initialHeightmap = undefined, smoothness = 0.5, heightmap = g_Map.height) { g_Map.log("Generating map using the diamond-square algorithm"); initialHeightmap = (initialHeightmap || [[randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)], [randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)]]); let heightRange = maxHeight - minHeight; if (heightRange <= 0) warn("setBaseTerrainDiamondSquare: heightRange <= 0"); let offset = heightRange / 2; // Double initialHeightmap width until target width is reached (diamond square method) let newHeightmap = []; while (initialHeightmap.length < heightmap.length) { newHeightmap = []; let oldWidth = initialHeightmap.length; // Square for (let x = 0; x < 2 * oldWidth - 1; ++x) { newHeightmap.push([]); for (let y = 0; y < 2 * oldWidth - 1; ++y) { if (x % 2 == 0 && y % 2 == 0) // Old tile newHeightmap[x].push(initialHeightmap[x/2][y/2]); else if (x % 2 == 1 && y % 2 == 1) // New tile with diagonal old tile neighbors { newHeightmap[x].push((initialHeightmap[(x-1)/2][(y-1)/2] + initialHeightmap[(x+1)/2][(y-1)/2] + initialHeightmap[(x-1)/2][(y+1)/2] + initialHeightmap[(x+1)/2][(y+1)/2]) / 4); newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else // New tile with straight old tile neighbors newHeightmap[x].push(undefined); // Define later } } // Diamond for (let x = 0; x < 2 * oldWidth - 1; ++x) { for (let y = 0; y < 2 * oldWidth - 1; ++y) { if (newHeightmap[x][y] !== undefined) continue; if (x > 0 && x + 1 < newHeightmap.length - 1 && y > 0 && y + 1 < newHeightmap.length - 1) // Not a border tile { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 4; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x < newHeightmap.length - 1 && y > 0 && y < newHeightmap.length - 1) // Left border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && y > 0 && y < newHeightmap.length - 1) // Right border { newHeightmap[x][y] = (newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && x < newHeightmap.length - 1 && y < newHeightmap.length - 1) // Bottom border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && x < newHeightmap.length - 1 && y > 0) // Top border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } } } initialHeightmap = clone(newHeightmap); offset /= Math.pow(2, smoothness); } // Cut initialHeightmap to fit target width let shift = [Math.floor((newHeightmap.length - heightmap.length) / 2), Math.floor((newHeightmap[0].length - heightmap[0].length) / 2)]; for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[0].length; ++y) heightmap[x][y] = newHeightmap[x + shift[0]][y + shift[1]]; return heightmap; } /** * Meant to place e.g. resource spots within a height range * @param {array} [heightRange] - The height range in which to place the entities (An associative array with keys "min" and "max" each containing a float) * @param {array} [avoidPoints=[]] - An array of objects of the form { "x": int, "y": int, "dist": int }, points that will be avoided in the given dist e.g. start locations - * @param {object} [avoidClass=undefined] - TileClass to be avoided + * @param {Object} [avoidClass=undefined] - TileClass to be avoided * @param {integer} [minDistance=30] - How many tile widths the entities to place have to be away from each other, start locations and the map border * @param {array} [heightmap=g_Map.height] - The reliefmap the entities should be distributed on * @param {integer} [maxTries=2 * g_Map.size] - How often random player distributions are rolled to be compared (256 to 1024) * @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular */ function getPointsByHeight(heightRange, avoidPoints = [], avoidClass = undefined, minDistance = 20, maxTries = 2 * g_Map.size, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap) { let points = []; let placements = clone(avoidPoints); let validVertices = []; let r = 0.5 * (heightmap.length - 1); // Map center x/y as well as radius let avoidMap; if (avoidClass) avoidMap = avoidClass.inclusionCount; for (let x = minDistance; x < heightmap.length - minDistance; ++x) for (let y = minDistance; y < heightmap[x].length - minDistance; ++y) { if (avoidClass && (avoidMap[Math.max(x - 1, 0)][y] > 0 || avoidMap[x][Math.max(y - 1, 0)] > 0 || avoidMap[Math.min(x + 1, avoidMap.length - 1)][y] > 0 || avoidMap[x][Math.min(y + 1, avoidMap[0].length - 1)] > 0)) continue; if (heightmap[x][y] > heightRange.min && heightmap[x][y] < heightRange.max && // Has correct height (!isCircular || r - Math.euclidDistance2D(x, y, r, r) >= minDistance)) // Enough distance to the map border validVertices.push({ "x": x, "y": y , "dist": minDistance}); } for (let tries = 0; tries < maxTries; ++tries) { let point = pickRandom(validVertices); if (placements.every(p => Math.euclidDistance2D(p.x, p.y, point.x, point.y) > Math.max(minDistance, p.dist))) { points.push(point); placements.push(point); } } return points; } /** * Returns an approximation of the heights of the tiles between the vertices, a tile centered heightmap * A tile centered heightmap is one smaller in width and height than an ordinary heightmap * It is meant to e.g. texture a map by height (x/y coordinates correspond to those of the terrain texture map) * Don't use this to override g_Map height (Potentially breaks the map)! * @param {array} [heightmap=g_Map.height] - A reliefmap the tile centered version should be build from */ function getTileCenteredHeightmap(heightmap = g_Map.height) { let max_x = heightmap.length - 1; let max_y = heightmap[0].length - 1; let tchm = []; for (let x = 0; x < max_x; ++x) { tchm[x] = new Float32Array(max_y); for (let y = 0; y < max_y; ++y) tchm[x][y] = 0.25 * (heightmap[x][y] + heightmap[x + 1][y] + heightmap[x][y + 1] + heightmap[x + 1][y + 1]); } return tchm; } /** * Returns a slope map (same form as the a heightmap with one less width and height) * Not normalized. Only returns the steepness (float), not the direction of incline. * The x and y coordinates of a tile in the terrain texture map correspond to those of the slope map * @param {array} [inclineMap=getInclineMap(g_Map.height)] - A map with the absolute inclination for each tile */ function getSlopeMap(inclineMap = getInclineMap(g_Map.height)) { let max_x = inclineMap.length; let slopeMap = []; for (let x = 0; x < max_x; ++x) { let max_y = inclineMap[x].length; slopeMap[x] = new Float32Array(max_y); for (let y = 0; y < max_y; ++y) slopeMap[x][y] = Math.euclidDistance2D(0, 0, inclineMap[x][y].x, inclineMap[x][y].y); } return slopeMap; } /** * Returns an inclination map corresponding to the tiles between the heightmaps vertices: * array of heightmap width-1 arrays of height-1 vectors (associative arrays) of the form: * { "x": x_slope, "y": y_slope ] so a 2D Vector pointing to the hightest incline (with the length the incline in the vectors direction) * The x and y coordinates of a tile in the terrain texture map correspond to those of the inclination map * @param {array} [heightmap=g_Map.height] - The reliefmap the inclination map is to be generated from */ function getInclineMap(heightmap) { heightmap = (heightmap || g_Map.height); let max_x = heightmap.length - 1; let max_y = heightmap[0].length - 1; let inclineMap = []; for (let x = 0; x < max_x; ++x) { inclineMap[x] = []; for (let y = 0; y < max_y; ++y) { let dx = heightmap[x + 1][y] - heightmap[x][y]; let dy = heightmap[x][y + 1] - heightmap[x][y]; let next_dx = heightmap[x + 1][y + 1] - heightmap[x][y + 1]; let next_dy = heightmap[x + 1][y + 1] - heightmap[x + 1][y]; inclineMap[x][y] = { "x" : 0.5 * (dx + next_dx), "y" : 0.5 * (dy + next_dy) }; } } return inclineMap; } function getGrad(wrapped = true, scalarField = g_Map.height) { let vectorField = []; let max_x = scalarField.length; let max_y = scalarField[0].length; if (!wrapped) { max_x -= 1; max_y -= 1; } for (let x = 0; x < max_x; ++x) { vectorField.push([]); for (let y = 0; y < max_y; ++y) { vectorField[x].push({ "x" : scalarField[(x + 1) % max_x][y] - scalarField[x][y], "y" : scalarField[x][(y + 1) % max_y] - scalarField[x][y] }); } } return vectorField; } function splashErodeMap(strength = 1, heightmap = g_Map.height) { let max_x = heightmap.length; let max_y = heightmap[0].length; let dHeight = getGrad(heightmap); for (let x = 0; x < max_x; ++x) { let next_x = (x + 1) % max_x; let prev_x = (x + max_x - 1) % max_x; for (let y = 0; y < max_y; ++y) { let next_y = (y + 1) % max_y; let prev_y = (y + max_y - 1) % max_y; let slopes = [- dHeight[x][y].x, - dHeight[x][y].y, dHeight[prev_x][y].x, dHeight[x][prev_y].y]; let sumSlopes = 0; for (let i = 0; i < slopes.length; ++i) if (slopes[i] > 0) sumSlopes += slopes[i]; let drain = []; for (let i = 0; i < slopes.length; ++i) { drain.push(0); if (slopes[i] > 0) drain[i] += Math.min(strength * slopes[i] / sumSlopes, slopes[i]); } let sumDrain = 0; for (let i = 0; i < drain.length; ++i) sumDrain += drain[i]; // Apply changes to maps heightmap[x][y] -= sumDrain; heightmap[next_x][y] += drain[0]; heightmap[x][next_y] += drain[1]; heightmap[prev_x][y] += drain[2]; heightmap[x][prev_y] += drain[3]; } } return heightmap; } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 23991) @@ -1,927 +1,927 @@ /** * @file Contains functionality to place walls on random maps. */ /** * Set some globals for this module. */ var g_WallStyles = loadWallsetsFromCivData(); var g_FortressTypes = createDefaultFortressTypes(); /** * Fetches wallsets from {civ}.json files, and then uses them to load * basic wall elements. */ function loadWallsetsFromCivData() { let wallsets = {}; for (let civ in g_CivData) { let civInfo = g_CivData[civ]; if (!civInfo.WallSets) continue; for (let path of civInfo.WallSets) { // File naming conventions: // - other/wallset_{style} // - structures/{civ}_wallset_{style} let style = basename(path).split("_"); style = style[0] == "wallset" ? style[1] : style[0] + "_" + style[2]; if (!wallsets[style]) wallsets[style] = loadWallset(Engine.GetTemplate(path), civ); } } return wallsets; } function loadWallset(wallsetPath, civ) { let newWallset = { "curves": [] }; let wallsetData = GetTemplateDataHelper(wallsetPath).wallSet; for (let element in wallsetData.templates) if (element == "curves") for (let filename of wallsetData.templates.curves) newWallset.curves.push(readyWallElement(filename, civ)); else newWallset[element] = readyWallElement(wallsetData.templates[element], civ); newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length; return newWallset; } /** * Fortress class definition * * We use "fortress" to describe a closed wall built of multiple wall * elements attached together surrounding a central point. We store the * abstract of the wall (gate, tower, wall, ...) and only apply the style * when we get to build it. * * @param {string} type - Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements. * @param {array} [wall] - Array of wall element strings. May be defined at a later point. * Example: ["medium", "cornerIn", "gate", "cornerIn", "medium", "cornerIn", "gate", "cornerIn"] - * @param {object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element. + * @param {Object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element. * @param {number} [centerToFirstElement.x] * @param {number} [centerToFirstElement.y] */ function Fortress(type, wall=[], centerToFirstElement=undefined) { this.type = type; this.wall = wall; this.centerToFirstElement = centerToFirstElement; } function createDefaultFortressTypes() { let defaultFortresses = {}; /** * Define some basic default fortress types. */ let addFortress = (type, walls) => defaultFortresses[type] = { "wall": walls.concat(walls, walls, walls) }; addFortress("tiny", ["gate", "tower", "short", "cornerIn", "short", "tower"]); addFortress("small", ["gate", "tower", "medium", "cornerIn", "medium", "tower"]); addFortress("medium", ["gate", "tower", "long", "cornerIn", "long", "tower"]); addFortress("normal", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("large", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); addFortress("veryLarge", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "long", "cornerIn", "long", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("giant", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); /** * Define some fortresses based on those above, but designed for use * with the "palisades" wallset. */ for (let fortressType in defaultFortresses) { const fillTowersBetween = ["short", "medium", "long", "start", "end", "cornerIn", "cornerOut"]; const newKey = fortressType + "Palisades"; const oldWall = defaultFortresses[fortressType].wall; defaultFortresses[newKey] = { "wall": [] }; for (let j = 0; j < oldWall.length; ++j) { defaultFortresses[newKey].wall.push(oldWall[j]); if (j + 1 < oldWall.length && fillTowersBetween.indexOf(oldWall[j]) != -1 && fillTowersBetween.indexOf(oldWall[j + 1]) != -1) { defaultFortresses[newKey].wall.push("tower"); } } } return defaultFortresses; } /** * Define some helper functions */ /** * Get a wall element of a style. * * Valid elements: * long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort * * Dynamic elements: * `gap_{x}` returns a non-blocking gap of length `x` meters. * `turn_{x}` returns a zero-length turn of angle `x` radians. * * Any other arbitrary string passed will be attempted to be used as: `structures/{civ}_{arbitrary_string}`. * * @param {string} element - What sort of element to fetch. * @param {string} [style] - The style from which this element should come from. - * @returns {object} The wall element requested. Or a tower element. + * @returns {Object} The wall element requested. Or a tower element. */ function getWallElement(element, style) { style = validateStyle(style); if (g_WallStyles[style][element]) return g_WallStyles[style][element]; // Attempt to derive any unknown elements. // Defaults to a wall tower piece const quarterBend = Math.PI / 2; let wallset = g_WallStyles[style]; let civ = style.split("_")[0]; let ret = wallset.tower ? clone(wallset.tower) : { "angle": 0, "bend": 0, "length": 0, "indent": 0 }; switch (element) { case "cornerIn": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) ret = curve; if (ret.bend != quarterBend) { ret.angle += Math.PI / 4; ret.indent = ret.length / 4; ret.length = 0; ret.bend = Math.PI / 2; } break; case "cornerOut": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) { ret = clone(curve); ret.angle += Math.PI / 2; ret.indent -= ret.indent * 2; } if (ret.bend != quarterBend) { ret.angle -= Math.PI / 4; ret.indent = -ret.length / 4; ret.length = 0; } ret.bend = -Math.PI / 2; break; case "entry": ret.templateName = undefined; ret.length = wallset.gate.length; break; case "entryTower": ret.templateName = g_CivData[civ] ? "structures/" + civ + "_defense_tower" : "structures/palisades_watchtower"; ret.indent = ret.length * -3; ret.length = wallset.gate.length; break; case "entryFort": ret = clone(wallset.fort); ret.angle -= Math.PI; ret.length *= 1.5; ret.indent = ret.length; break; case "start": if (wallset.end) { ret = clone(wallset.end); ret.angle += Math.PI; } break; case "end": if (wallset.end) ret = wallset.end; break; default: if (element.startsWith("gap_")) { ret.templateName = undefined; ret.angle = 0; ret.length = +element.slice("gap_".length); } else if (element.startsWith("turn_")) { ret.templateName = undefined; ret.bend = +element.slice("turn_".length) * Math.PI; ret.length = 0; } else { if (!g_CivData[civ]) civ = Object.keys(g_CivData)[0]; let templateName = "structures/" + civ + "_" + element; if (Engine.TemplateExists(templateName)) { ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5); ret.templateName = templateName; ret.length = 0; } else warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element.")); } } // Cache to save having to calculate this element again. g_WallStyles[style][element] = deepfreeze(ret); return ret; } /** * Prepare a wall element for inclusion in a style. * * @param {string} path - The template path to read values from */ function readyWallElement(path, civCode) { path = path.replace(/\{civ\}/g, civCode); let template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null); let length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width; return deepfreeze({ "templateName": path, "angle": template.wallPiece ? template.wallPiece.angle : Math.PI, "length": length / TERRAIN_TILE_SIZE, "indent": template.wallPiece ? template.wallPiece.indent / TERRAIN_TILE_SIZE : 0, "bend": template.wallPiece ? template.wallPiece.bend : 0 }); } /** * Returns a list of objects containing all information to place all the wall elements entities with placeObject (but the player ID) * Placing the first wall element at startX/startY placed with an angle given by orientation * An alignment can be used to get the "center" of a "wall" (more likely used for fortresses) with getCenterToFirstElement * * @param {Vector2D} position * @param {array} [wall] * @param {string} [style] * @param {number} [orientation] * @returns {array} */ function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0) { style = validateStyle(style); let alignment = []; let wallPosition = position.clone(); for (let i = 0; i < wall.length; ++i) { let element = getWallElement(wall[i], style); if (!element && i == 0) { warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element)); continue; } // Add wall elements entity placement arguments to the alignment alignment.push({ "position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)), "templateName": element.templateName, "angle": orientation + element.angle }); // Preset vars for the next wall element if (i + 1 < wall.length) { orientation += element.bend; let nextElement = getWallElement(wall[i + 1], style); if (!nextElement) { warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement)); continue; } let distance = (element.length + nextElement.length) / 2 - g_WallStyles[style].overlap; // Corrections for elements with indent AND bending let indent = element.indent; let bend = element.bend; if (bend != 0 && indent != 0) { // Indent correction to adjust distance distance += indent * Math.sin(bend); // Indent correction to normalize indentation wallPosition.add(new Vector2D(indent).rotate(-orientation)); } // Set the next coordinates of the next element in the wall without indentation adjustment wallPosition.add(new Vector2D(distance, 0).rotate(-orientation).perpendicular()); } } return alignment; } /** * Center calculation works like getting the center of mass assuming all wall elements have the same "weight" * * Used to get centerToFirstElement of fortresses by default * * @param {number} alignment - * @returns {object} Vector from the center of the set of aligned wallpieces to the first wall element. + * @returns {Object} Vector from the center of the set of aligned wallpieces to the first wall element. */ function getCenterToFirstElement(alignment) { return alignment.reduce((result, align) => result.sub(Vector2D.div(align.position, alignment.length)), new Vector2D(0, 0)); } /** * Does not support bending wall elements like corners. * * @param {string} style * @param {array} wall * @returns {number} The sum length (in terrain cells, not meters) of the provided wall. */ function getWallLength(style, wall) { style = validateStyle(style); let length = 0; let overlap = g_WallStyles[style].overlap; for (let element of wall) length += getWallElement(element, style).length - overlap; return length; } /** * Makes sure the style exists and, if not, provides a fallback. * * @param {string} style * @param {number} [playerId] * @returns {string} Valid style. */ function validateStyle(style, playerId = 0) { if (!style || !g_WallStyles[style]) { if (playerId == 0) return Object.keys(g_WallStyles)[0]; style = getCivCode(playerId) + "_stone"; return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style; } return style; } /** * Define the different wall placer functions */ /** * Places an abitrary wall beginning at the location comprised of the array of elements provided. * * @param {Vector2D} position * @param {array} [wall] - Array of wall element types. Example: ["start", "long", "tower", "long", "end"] * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle at which the first wall element is placed. * 0 means "outside" or "front" of the wall is right (positive X) like placeObject * It will then be build towards top/positive Y (if no bending wall elements like corners are used) * Raising orientation means the wall is rotated counter-clockwise like placeObject */ function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined) { style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); for (let align of getWallAlignment(position, wall, style, orientation)) if (align.templateName && g_Map.inMapBounds(align.position) && constraint.allows(align.position.clone().floor())) entities.push(g_Map.placeEntityPassable(align.templateName, playerId, align.position, align.angle)); return entities; } /** * Places an abitrarily designed "fortress" (closed loop of wall elements) * centered around a given point. * * The fortress wall should always start with the main entrance (like * "entry" or "gate") to get the orientation correct. * * @param {Vector2D} centerPosition - * @param {object} [fortress] - If not provided, defaults to the predefined "medium" fortress type. + * @param {Object} [fortress] - If not provided, defaults to the predefined "medium" fortress type. * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle the first wall element (should be a gate or entrance) is placed. Default is 0 */ function placeCustomFortress(centerPosition, fortress, style, playerId = 0, orientation = 0, constraints = undefined) { fortress = fortress || g_FortressTypes.medium; style = validateStyle(style, playerId); // Calculate center if fortress.centerToFirstElement is undefined (default) let centerToFirstElement = fortress.centerToFirstElement; if (centerToFirstElement === undefined) centerToFirstElement = getCenterToFirstElement(getWallAlignment(new Vector2D(0, 0), fortress.wall, style)); // Placing the fortress wall let position = Vector2D.sum([ centerPosition, new Vector2D(centerToFirstElement.x, 0).rotate(-orientation), new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation) ]); return placeWall(position, fortress.wall, style, playerId, orientation, constraints); } /** * Places a predefined fortress centered around the provided point. * * @see Fortress * * @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes. */ function placeFortress(centerPosition, type = "medium", style, playerId = 0, orientation = 0, constraints = undefined) { return placeCustomFortress(centerPosition, g_FortressTypes[type], style, playerId, orientation, constraints); } /** * Places a straight wall from a given point to another, using the provided * wall parts repeatedly. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} startPosition - Approximate start point of the wall. * @param {Vector2D} targetPosition - Approximate end point of the wall. * @param {array} [wallPart=["tower", "long"]] * @param {number} [playerId] * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. */ function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); // Check arguments for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeLinearWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = startPosition.distanceTo(targetPosition); let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Setup angle let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y); let placeAngle = wallAngle - Math.PI / 2; // Place wall entities let entities = []; let position = startPosition.clone(); let overlap = g_WallStyles[style].overlap; let constraint = new StaticConstraint(constraints); for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) { let wallEle = getWallElement(wallPart[elementIndex], style); let wallLength = (wallEle.length - overlap) / 2; let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle); // Length correction position.add(dist); // Indent correction let place = Vector2D.add(position, new Vector2D(0, wallEle.indent).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); position.add(dist); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let wallLength = (wallEle.length - overlap) / 2; position.add(new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(position) && constraint.allows(position.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, position, placeAngle + wallEle.angle)); } return entities; } /** * Places a (semi-)circular wall of repeated wall elements around a central * point at a given radius. * * The wall does not have to be closed, and can be left open in the form * of an arc if maxAngle < 2 * Pi. In this case, the orientation determines * where this open part faces, with 0 meaning "right" like an unrotated * building's drop-point. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} center - Center of the circle or arc. * @param (number} radius - Approximate radius of the circle. (Given the maxBendOff argument) * @param {array} [wallPart] * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Angle at which the first wall element is placed. * @param {number} [maxAngle] - How far the wall should circumscribe the center. Default is Pi * 2 (for a full circle). * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. For full circles, the default is false. For arcs, true. * @param {number} [maxBendOff] Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle) */ function placeCircularWall(center, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = 2 * Math.PI, endWithFirst, maxBendOff = 0, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); if (endWithFirst === undefined) endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better? // Check arguments if (maxBendOff > Math.PI / 2 || maxBendOff < 0) warn("placeCircularWall : maxBendOff should satisfy 0 < maxBendOff < PI/2 (~1.5rad) but it is: " + maxBendOff); for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeCircularWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = maxAngle * radius; let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Place wall entities let entities = []; let constraint = new StaticConstraint(constraints); let actualAngle = orientation; let position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); let overlap = g_WallStyles[style].overlap; for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let wallEle of wallPart) { wallEle = getWallElement(wallEle, style); // Width correction let addAngle = scaleFactor * (wallEle.length - overlap) / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; // Indent correction place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle)); // Placement if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); // Prepare for the next wall element actualAngle += addAngle; position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let addAngle = scaleFactor * wallEle.length / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; if (g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); } return entities; } /** * Places a polygonal wall of repeated wall elements around a central * point at a given radius. * * Note: Any "bending" wall pieces passed will be ignored. * * @param {Vector2D} centerPosition * @param {number} radius * @param {array} [wallPart] * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wall piece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {boolean} [skipFirstWall] - If the first linear wall part will be left opened as entrance. */ function placePolygonalWall(centerPosition, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true, constraints = undefined) { wallPart = wallPart || ["long", "tower"]; style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); let angleAdd = Math.PI * 2 / numCorners; let angleStart = orientation - angleAdd / 2; let corners = new Array(numCorners).fill(0).map((zero, i) => Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleStart - i * angleAdd))); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) entities.push( g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner)); if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let cornerAngle = angleToCorner + angleAdd / 2; let targetCorner = (i + 1) % numCorners; let cornerPosition = new Vector2D(cornerLength, 0).rotate(-cornerAngle).perpendicular(); entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], cornerPosition), Vector2D.add(corners[targetCorner], cornerPosition), wallPart, style, playerId, undefined, constraints)); } } return entities; } /** * Places an irregular polygonal wall consisting of parts semi-randomly * chosen from a provided assortment, built around a central point at a * given radius. * * Note: Any "bending" wall pieces passed will be ... I'm not sure. TODO: test what happens! * * Note: The wallPartsAssortment is last because it's the hardest to set. * * @param {Vector2D} centerPosition * @param {number} radius * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wallpiece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {number} [irregularity] - How irregular the polygon will be. 0 = regular, 1 = VERY irregular. * @param {boolean} [skipFirstWall] - If true, the first linear wall part will be left open as an entrance. * @param {array} [wallPartsAssortment] - An array of wall part arrays to choose from for each linear wall connecting the corners. */ function placeIrregularPolygonalWall(centerPosition, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment = undefined, constraints = undefined) { style = validateStyle(style, playerId); numCorners = numCorners || randIntInclusive(5, 7); // Generating a generic wall part assortment with each wall part including 1 gate lengthened by walls and towers // NOTE: It might be a good idea to write an own function for that... let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]]; let centeredWallPart = ["gate"]; let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]]; defaultWallPartsAssortment.push(centeredWallPart); for (let assortment of extendingWallPartAssortment) { let wallPart = centeredWallPart; for (let j = 0; j < radius; ++j) { if (j % 2 == 0) wallPart = wallPart.concat(assortment); else { assortment.reverse(); wallPart = assortment.concat(wallPart); assortment.reverse(); } defaultWallPartsAssortment.push(wallPart); } } // Setup optional arguments to the default wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment; // Setup angles let angleToCover = Math.PI * 2; let angleAddList = []; for (let i = 0; i < numCorners; ++i) { // Randomize covered angles. Variety scales down with raising angle though... angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity))); angleToCover -= angleAddList[angleAddList.length - 1]; } // Setup corners let corners = []; let angleActual = orientation - angleAddList[0] / 2; for (let i = 0; i < numCorners; ++i) { corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual))); if (i < numCorners - 1) angleActual += angleAddList[i + 1]; } // Setup best wall parts for the different walls (a bit confusing naming...) let wallPartLengths = []; let maxWallPartLength = 0; for (let wallPart of wallPartsAssortment) { let length = getWallLength(style, wallPart); wallPartLengths.push(length); if (length > maxWallPartLength) maxWallPartLength = length; } let wallPartList = []; // This is the list of the wall parts to use for the walls between the corners, not to confuse with wallPartsAssortment! for (let i = 0; i < numCorners; ++i) { let bestWallPart = []; // This is a simple wall part not a wallPartsAssortment! let bestWallLength = Infinity; let targetCorner = (i + 1) % numCorners; // NOTE: This is not quite the length the wall will be in the end. Has to be tweaked... let wallLength = corners[i].distanceTo(corners[targetCorner]); let numWallParts = Math.ceil(wallLength / maxWallPartLength); for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex) { let linearWallLength = numWallParts * wallPartLengths[partIndex]; if (linearWallLength < bestWallLength && linearWallLength > wallLength) { bestWallPart = wallPartsAssortment[partIndex]; bestWallLength = linearWallLength; } } wallPartList.push(bestWallPart); } // Place Corners and walls let entities = []; let constraint = new StaticConstraint(constraints); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) entities.push( g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner)); if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let targetCorner = (i + 1) % numCorners; let startAngle = angleToCorner + angleAddList[i] / 2; let targetAngle = angleToCorner + angleAddList[targetCorner] / 2; entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], new Vector2D(cornerLength, 0).perpendicular().rotate(-startAngle)), Vector2D.add(corners[targetCorner], new Vector2D(cornerLength, 0).rotate(-targetAngle - Math.PI / 2)), wallPartList[i], style, playerId, false, constraints)); } } return entities; } /** * Places a generic fortress with towers at the edges connected with long * walls and gates, positioned around a central point at a given radius. * * The difference between this and the other two Fortress placement functions * is that those place a predefined fortress, regardless of terrain type. * This function attempts to intelligently place a wall circuit around * the central point taking into account terrain and other obstacles. * * This is the default Iberian civ bonus starting wall. * * @param {Vector2D} center - The approximate center coordinates of the fortress * @param {number} [radius] - The approximate radius of the wall to be placed. * @param {number} [playerId] * @param {string} [style] * @param {number} [irregularity] - 0 = circle, 1 = very spiky * @param {number} [gateOccurence] - Integer number, every n-th walls will be a gate instead. * @param {number} [maxTries] - How often the function tries to find a better fitting shape. */ function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined) { style = validateStyle(style, playerId); // Setup some vars let startAngle = randomAngle(); let actualOff = new Vector2D(radius, 0).rotate(-startAngle); let actualAngle = startAngle; let pointDistance = getWallLength(style, ["long", "tower"]); // Searching for a well fitting point derivation let tries = 0; let bestPointDerivation; let minOverlap = 1000; let overlap; while (tries < maxTries && minOverlap > g_WallStyles[style].overlap) { let pointDerivation = []; let distanceToTarget = 1000; while (true) { let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance); let tmp = new Vector2D(radius + indent, 0).rotate(-actualAngle - pointDistance / radius); let tmpAngle = getAngle(actualOff.x, actualOff.y, tmp.x, tmp.y); actualOff.add(new Vector2D(pointDistance, 0).rotate(-tmpAngle)); actualAngle = getAngle(0, 0, actualOff.x, actualOff.y); pointDerivation.push(actualOff.clone()); distanceToTarget = pointDerivation[0].distanceTo(actualOff); let numPoints = pointDerivation.length; if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better... { overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]); if (overlap < minOverlap) { minOverlap = overlap; bestPointDerivation = pointDerivation; } break; } } ++tries; } log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries"); // Place wall let entities = []; let constraint = new StaticConstraint(constraints); for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex) { let start = Vector2D.add(center, bestPointDerivation[pointIndex]); let target = Vector2D.add(center, bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length]); let angle = getAngle(start.x, start.y, target.x, target.y); let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long"; element = getWallElement(element, style); if (element.templateName) { let pos = Vector2D.add(start, new Vector2D(start.distanceTo(target) / 2, 0).rotate(-angle)); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push(g_Map.placeEntityPassable(element.templateName, playerId, pos, angle - Math.PI / 2 + element.angle)); } // Place tower start = Vector2D.add(center, bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length]); angle = getAngle(start.x, start.y, target.x, target.y); let tower = getWallElement("tower", style); let pos = Vector2D.add(center, bestPointDerivation[pointIndex]); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push( g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle)); } return entities; } Index: ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/simulation/components/StatusEffectsReceiver.js (revision 23991) @@ -1,162 +1,162 @@ function StatusEffectsReceiver() {} StatusEffectsReceiver.prototype.DefaultInterval = 1000; /** * Initialises the status effects. */ StatusEffectsReceiver.prototype.Init = function() { this.activeStatusEffects = {}; }; /** * Which status effects are active on this entity. * * @return {Object} - An object containing the status effects which currently affect the entity. */ StatusEffectsReceiver.prototype.GetActiveStatuses = function() { return this.activeStatusEffects; }; /** * Called by Attacking effects. Adds status effects for each entry in the effectData. * * @param {Object} effectData - An object containing the status effects to give to the entity. * @param {number} attacker - The entity ID of the attacker. * @param {number} attackerOwner - The player ID of the attacker. * @param {number} bonusMultiplier - A value to multiply the damage with (not implemented yet for SE). * * @return {Object} - The names of the status effects which were processed. */ StatusEffectsReceiver.prototype.ApplyStatus = function(effectData, attacker, attackerOwner, bonusMultiplier) { let attackerData = { "entity": attacker, "owner": attackerOwner }; for (let effect in effectData) this.AddStatus(effect, effectData[effect], attackerData); // TODO: implement loot / resistance. return { "inflictedStatuses": Object.keys(effectData) }; }; /** * Adds a status effect to the entity. * * @param {string} statusName - The name of the status effect. - * @param {object} data - The various effects and timings. - * @param {object} attackerData - The attacker and attackerOwner. + * @param {Object} data - The various effects and timings. + * @param {Object} attackerData - The attacker and attackerOwner. */ StatusEffectsReceiver.prototype.AddStatus = function(statusName, data, attackerData) { if (this.activeStatusEffects[statusName]) { if (data.Stackability == "Ignore") return; if (data.Stackability == "Extend") { this.activeStatusEffects[statusName].Duration += data.Duration; return; } if (data.Stackability == "Replace") this.RemoveStatus(statusName); else if (data.Stackability == "Stack") { let i = 0; let temp; do temp = statusName + "_" + i++; while (!!this.activeStatusEffects[temp]); statusName = temp; } } this.activeStatusEffects[statusName] = {}; let status = this.activeStatusEffects[statusName]; Object.assign(status, data); if (status.Modifiers) { let modifications = DeriveModificationsFromXMLTemplate(status.Modifiers); let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers(statusName, modifications, this.entity); } // With neither an interval nor a duration, there is no point in starting a timer. if (!status.Duration && !status.Interval) return; // We need this to prevent Status Effects from giving XP // to the entity that applied them. status.StatusEffect = true; // We want an interval to update the GUI to show how much time of the status effect // is left even if the status effect itself has no interval. if (!status.Interval) status._interval = this.DefaultInterval; status._timeElapsed = 0; status._firstTime = true; status.source = attackerData; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); status._timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +(status.Interval || status._interval), statusName); }; /** * Removes a status effect from the entity. * * @param {string} statusName - The status effect to be removed. */ StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) { let statusEffect = this.activeStatusEffects[statusName]; if (!statusEffect) return; if (statusEffect.Modifiers) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.RemoveAllModifiers(statusName, this.entity); } if (statusEffect._timer) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(statusEffect._timer); } delete this.activeStatusEffects[statusName]; }; /** * Called by the timers. Executes a status effect. * * @param {string} statusName - The name of the status effect to be executed. * @param {number} lateness - The delay between the calling of the function and the actual execution (turn time?). */ StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness) { let status = this.activeStatusEffects[statusName]; if (!status) return; if (status.Damage || status.Capture) Attacking.HandleAttackEffects(statusName, status, this.entity, status.source.entity, status.source.owner); if (!status.Duration) return; if (status._firstTime) { status._firstTime = false; status._timeElapsed += lateness; } else status._timeElapsed += +(status.Interval || status._interval) + lateness; if (status._timeElapsed >= +status.Duration) this.RemoveStatus(statusName); }; Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver); Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 23990) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 23991) @@ -1,376 +1,376 @@ function TechnologyManager() {} TechnologyManager.prototype.Schema = ""; TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} // Some technologies are automatically researched when their conditions are met. They have no cost and are // researched instantly. This allows civ bonuses and more complicated technologies. this.unresearchedAutoResearchTechs = new Set(); let allTechs = TechnologyTemplates.GetAll(); for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(key); }; TechnologyManager.prototype.OnUpdate = function() { this.UpdateAutoResearch(); }; // This function checks if the requirements of any autoresearch techs are met and if they are it researches them TechnologyManager.prototype.UpdateAutoResearch = function() { for (let key of this.unresearchedAutoResearchTechs) { let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(key); this.ResearchTechnology(key); return; // We will have recursively handled any knock-on effects so can just return } } }; // Checks an entity template to see if its technology requirements have been met TechnologyManager.prototype.CanProduce = function (templateName) { var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTempManager.GetTemplate(templateName); if (template.Identity && template.Identity.RequiredTechnology) return this.IsTechnologyResearched(template.Identity.RequiredTechnology); // If there is no required technology then this entity can be produced return true; }; TechnologyManager.prototype.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); return false; } if (template.top && this.IsInProgress(template.top) || template.bottom && this.IsInProgress(template.bottom)) return false; if (template.pair && !this.CanResearch(template.pair)) return false; if (this.IsInProgress(tech)) return false; if (this.IsTechnologyResearched(tech)) return false; return this.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met - * @param {object} reqs - Technology requirements as derived from the technology template by globalscripts + * @param {Object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg) { // This automatically updates classCounts and typeCountsByClass var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID(); if (msg.to == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; var classes = cmpIdentity.GetClassesList(); // don't use foundations for the class counts but check if techs apply (e.g. health increase) if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { for (let cls of classes) { this.classCounts[cls] = this.classCounts[cls] || 0; this.classCounts[cls] += 1; this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {}; this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0; this.typeCountsByClass[cls][template] += 1; } } } if (msg.from == playerID) { var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity); // don't use foundations for the class counts if (!Engine.QueryInterface(msg.entity, IID_Foundation)) { var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (cmpIdentity) { var classes = cmpIdentity.GetClassesList(); for (let cls of classes) { this.classCounts[cls] -= 1; if (this.classCounts[cls] <= 0) delete this.classCounts[cls]; this.typeCountsByClass[cls][template] -= 1; if (this.typeCountsByClass[cls][template] <= 0) delete this.typeCountsByClass[cls][template]; } } } } }; /** * Marks a technology as researched. * Note that this does not verify that the requirements are met. * * @param {String} tech - The technology to mark as researched. */ TechnologyManager.prototype.ResearchTechnology = function(tech) { this.StoppedResearch(tech, false); let modifiedComponents = {}; this.researchedTechs.add(tech); // Store the modifications in an easy to access structure. let template = TechnologyTemplates.Get(tech); if (template.modifications) { let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager); cmpModifiersManager.AddModifiers("tech/" + tech, DeriveModificationsFromTech(template), this.entity); } if (template.replaces && template.replaces.length > 0) { for (let i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // Change the EntityLimit if any. let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined) { let playerID = cmpPlayer.GetPlayerID(); let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(i); } } } this.UpdateAutoResearch(); let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined) return; let playerID = cmpPlayer.GetPlayerID(); // Change the EntityLimit if any. let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits); if (cmpPlayerEntityLimits) cmpPlayerEntityLimits.UpdateLimitsFromTech(tech); // Always send research finished message. Engine.PostMessage(this.entity, MT_ResearchFinished, { "player": playerID, "tech": tech }); if (tech.startsWith("phase") && !template.autoResearch) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [playerID], "phaseName": tech, "phaseState": "completed" }); } }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech); }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ TechnologyManager.prototype.GetResearchedTechs = function() { return this.researchedTechs; }; TechnologyManager.prototype.GetClassCounts = function() { return this.classCounts; }; TechnologyManager.prototype.GetTypeCountsByClass = function() { return this.typeCountsByClass; }; Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);