Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 20625) @@ -1,517 +1,529 @@ /** * 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; } /** * Gets an array of all classes for this identity template */ function GetIdentityClasses(template) { var classList = []; if (template.Classes && template.Classes._string) classList = classList.concat(template.Classes._string.split(/\s+/)); if (template.VisibleClasses && template.VisibleClasses._string) classList = classList.concat(template.VisibleClasses._string.split(/\s+/)); if (template.Rank) classList = classList.concat(template.Rank); return classList; } /** * Gets an array with all classes for this identity template * that should be shown in the GUI */ function GetVisibleIdentityClasses(template) { if (template.VisibleClasses && template.VisibleClasses._string) return template.VisibleClasses._string.split(/\s+/); return []; } /** * Check if 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 {string} value_path - Route to value within the xml template structure. * @return {number} */ function GetBaseTemplateDataValue(template, value_path) { let current_value = template; for (let property of value_path.split("/")) current_value = current_value[property] || 0; return +current_value; } /** * Gets the value originating at the value_path with the modifiers dictated by the mod_key applied. * * @param {object} template - A valid template as returned from a template loader. * @param {string} value_path - Route to value within the xml template structure. * @param {string} mod_key - Tech modification key, if different from value_path. * @param {number} player - Optional player id. * @param {object} modifiers - Value modifiers from auto-researched techs, unit upgrades, * etc. Optional as only used if no player id provided. * @return {number} Modifier altered value. */ function GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers={}) { let current_value = GetBaseTemplateDataValue(template, value_path); mod_key = mod_key || value_path; if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); else if (modifiers) current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} resources - An instance of the Resources prototype. * @param {object} damageTypes - An instance of the DamageTypes prototype. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, resources, damageTypes, 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 of damageTypes.GetTypes()) ret.armour[damageType] = getEntityValue("Armour/" + damageType); } if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; if (type == "Capture") ret.attack.Capture = { "value": getAttackStat("Value") }; else { ret.attack[type] = { "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus") }; for (let damageType of damageTypes.GetTypes()) ret.attack[type][damageType] = getAttackStat(damageType); ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); } ret.attack[type].repeatTime = getAttackStat("RepeatTime"); if (template.Attack[type].Splash) { ret.attack[type].splash = { // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape }; for (let damageType of damageTypes.GetTypes()) ret.attack[type].splash[damageType] = getAttackStat("Splash/" + damageType); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false" }; for (let damageType of damageTypes.GetTypes()) ret.deathDamage[damageType] = getEntityValue("DeathDamage/" + damageType); } - if (template.Auras) + 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("BuildRestrctions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrctions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "hp": getEntityValue("Heal/HP"), "range": getEntityValue("Heal/Range"), "rate": getEntityValue("Heal/Rate") }; if (template.ResourceGatherer) { ret.resourceGatherRates = {}; let baseSpeed = getEntityValue("ResourceGatherer/BaseSpeed"); for (let type in template.ResourceGatherer.Rates) ret.resourceGatherRates[type] = getEntityValue("ResourceGatherer/Rates/"+ type) * baseSpeed; } if (template.ResourceTrickle) { ret.resourceTrickle = { "interval": +template.ResourceTrickle.Interval, "rates": {} }; for (let type in template.ResourceTrickle.Rates) ret.resourceTrickle.rates[type] = getEntityValue("ResourceTrickle/Rates/" + type); } if (template.Loot) { ret.loot = {}; for (let type in template.Loot) ret.loot[type] = getEntityValue("Loot/"+ type); } if (template.Obstruction) { ret.obstruction = { "active": ("" + template.Obstruction.Active == "true"), "blockMovement": ("" + template.Obstruction.BlockMovement == "true"), "blockPathfinding": ("" + template.Obstruction.BlockPathfinding == "true"), "blockFoundation": ("" + template.Obstruction.BlockFoundation == "true"), "blockConstruction": ("" + template.Obstruction.BlockConstruction == "true"), "disableBlockMovement": ("" + template.Obstruction.DisableBlockMovement == "true"), "disableBlockPathfinding": ("" + template.Obstruction.DisableBlockPathfinding == "true"), "shape": {} }; if (template.Obstruction.Static) { ret.obstruction.shape.type = "static"; ret.obstruction.shape.width = +template.Obstruction.Static["@width"]; ret.obstruction.shape.depth = +template.Obstruction.Static["@depth"]; } else if (template.Obstruction.Unit) { ret.obstruction.shape.type = "unit"; ret.obstruction.shape.radius = +template.Obstruction.Unit["@radius"]; } else ret.obstruction.shape.type = "cluster"; } if (template.Pack) ret.pack = { "state": template.Pack.State, "time": getEntityValue("Pack/Time"), }; if (template.Health) ret.health = Math.round(getEntityValue("Health/Max")); if (template.Identity) { ret.selectionGroupName = template.Identity.SelectionGroupName; ret.name = { "specific": (template.Identity.SpecificName || template.Identity.GenericName), "generic": template.Identity.GenericName }; ret.icon = template.Identity.Icon; ret.tooltip = template.Identity.Tooltip; ret.requiredTechnology = template.Identity.RequiredTechnology; ret.visibleIdentityClasses = GetVisibleIdentityClasses(template.Identity); } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; if (template.UnitMotion.Run) ret.speed.run = getEntityValue("UnitMotion/Run/Speed"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) + { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, + "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, + "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, - "minTowerOverlap": +template.WallSet.MinTowerOverlap, + "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(" "); + } if (template.WallPiece) - ret.wallPiece = { "length": +template.WallPiece.Length }; + 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 {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects }; } /** * Get information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } Index: ps/trunk/binaries/data/mods/public/gui/reference/common/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/reference/common/load.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/gui/reference/common/load.js (revision 20625) @@ -1,346 +1,358 @@ /** * Paths to certain files. */ const g_TechnologyPath = "simulation/data/technologies/"; const g_AuraPath = "simulation/data/auras/"; /** * Raw Data Caches. */ var g_AuraData = {}; var g_TemplateData = {}; var g_TechnologyData = {}; var g_CivData = loadCivData(true, false); /** * Parsed Data Stores. */ var g_ParsedData = {}; var g_ResourceData = new Resources(); var g_DamageTypes = new DamageTypes(); // This must be defined after the g_TechnologyData cache object is declared. var g_AutoResearchTechList = findAllAutoResearchedTechs(); /** * Loads raw entity template. * * Loads from local cache if data present, else from file system. * * @param {string} templateName * @return {object} Object containing raw template data. */ function loadTemplate(templateName) { if (!(templateName in g_TemplateData)) { // We need to clone the template because we want to perform some translations. let data = clone(Engine.GetTemplate(templateName)); translateObjectKeys(data, ["GenericName", "SpecificName", "Tooltip"]); if (data.Auras) for (let auraID of data.Auras._string.split(/\s+/)) loadAuraData(auraID); g_TemplateData[templateName] = data; } return g_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. */ function loadTechData(templateName) { if (!(templateName in g_TechnologyData)) { let data = Engine.ReadJSONFile(g_TechnologyPath + templateName + ".json"); translateObjectKeys(data, ["genericName", "tooltip"]); g_TechnologyData[templateName] = data; } return g_TechnologyData[templateName]; } function techDataExists(templateName) { return Engine.FileExists("simulation/data/technologies/" + templateName + ".json"); } /** * Loads raw aura template. * * Loads from local cache if available, else from file system. * * @param {string} templateName * @return {object} Object containing raw template data. */ function loadAuraData(templateName) { if (!(templateName in g_AuraData)) { let data = Engine.ReadJSONFile(g_AuraPath + templateName + ".json"); translateObjectKeys(data, ["auraName", "auraDescription"]); g_AuraData[templateName] = data; } return g_AuraData[templateName]; } /** * Load and parse unit from entity template. * * @param {string} templateName * @return Sanitized object about the requested unit or null if entity template doesn't exist. */ function loadUnit(templateName) { if (!Engine.TemplateExists(templateName)) return null; let template = loadTemplate(templateName); let unit = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_DamageTypes, g_CurrentModifiers); if (template.ProductionQueue) { unit.production = {}; if (template.ProductionQueue.Entities) { unit.production.units = []; for (let build of template.ProductionQueue.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) unit.production.units.push(build); } } if (template.ProductionQueue.Technologies) { unit.production.techs = []; for (let research of template.ProductionQueue.Technologies._string.split(" ")) { if (research.indexOf("{civ}") != -1) { let civResearch = research.replace("{civ}", g_SelectedCiv); research = techDataExists(civResearch) ? civResearch : research.replace("{civ}", "generic"); } if (isPairTech(research)) for (let tech of loadTechnologyPair(research).techs) unit.production.techs.push(tech); else unit.production.techs.push(research); } } } if (template.Builder && template.Builder.Entities._string) { unit.builder = []; for (let build of template.Builder.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) unit.builder.push(build); } } if (unit.upgrades) unit.upgrades = getActualUpgradeData(unit.upgrades); return unit; } /** * Load and parse structure from entity template. * * @param {string} templateName * @return {object} Sanitized data about the requested structure or null if entity template doesn't exist. */ function loadStructure(templateName) { if (!Engine.TemplateExists(templateName)) return null; let template = loadTemplate(templateName); let structure = GetTemplateDataHelper(template, null, g_AuraData, g_ResourceData, g_DamageTypes, g_CurrentModifiers); structure.production = { "technology": [], "units": [] }; if (template.ProductionQueue) { if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string) for (let build of template.ProductionQueue.Entities._string.split(" ")) { build = build.replace(/\{(civ|native)\}/g, g_SelectedCiv); if (Engine.TemplateExists(build)) structure.production.units.push(build); } if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string) for (let research of template.ProductionQueue.Technologies._string.split(" ")) { if (research.indexOf("{civ}") != -1) { let civResearch = research.replace("{civ}", g_SelectedCiv); research = techDataExists(civResearch) ? civResearch : research.replace("{civ}", "generic"); } if (isPairTech(research)) for (let tech of loadTechnologyPair(research).techs) structure.production.technology.push(tech); else structure.production.technology.push(research); } } if (structure.upgrades) structure.upgrades = getActualUpgradeData(structure.upgrades); if (structure.wallSet) { structure.wallset = {}; if (!structure.upgrades) structure.upgrades = []; // Note: An assumption is made here that wall segments all have the same armor and auras let struct = loadStructure(structure.wallSet.templates.long); structure.armour = struct.armour; structure.auras = struct.auras; // For technology cost multiplier, we need to use the tower struct = loadStructure(structure.wallSet.templates.tower); structure.techCostMultiplier = struct.techCostMultiplier; let health; for (let wSegm in structure.wallSet.templates) { + if (wSegm == "fort" || wSegm == "curves") + continue; + let wPart = loadStructure(structure.wallSet.templates[wSegm]); structure.wallset[wSegm] = wPart; for (let research of wPart.production.technology) structure.production.technology.push(research); if (wPart.upgrades) structure.upgrades = structure.upgrades.concat(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 (structure.wallSet.templates.curves) + for (let curve of structure.wallSet.templates.curves) + { + let wPart = loadStructure(curve); + health.min = Math.min(health.min, wPart.health); + health.max = Math.max(health.max, wPart.health); + } + if (health.min == health.max) structure.health = health.min; else structure.health = sprintf(translate("%(health_min)s to %(health_max)s"), { "health_min": health.min, "health_max": health.max }); } return structure; } /** * Load and parse technology from json template. * * @param {string} templateName * @return {object} Sanitized data about the requested technology. */ function loadTechnology(techName) { let template = loadTechData(techName); let tech = GetTechnologyDataHelper(template, g_SelectedCiv, g_ResourceData); if (template.pair !== undefined) { tech.pair = template.pair; tech.reqs = mergeRequirements(tech.reqs, loadTechnologyPair(template.pair).reqs); } return tech; } /** * Crudely iterates through every tech JSON file and identifies those * that are auto-researched. * * @return {array} List of techs that are researched automatically */ function findAllAutoResearchedTechs() { let techList = []; for (let filename of Engine.ListDirectoryFiles(g_TechnologyPath, "*.json", true)) { // -5 to strip off the file extension let templateName = filename.slice(g_TechnologyPath.length, -5); let data = loadTechData(templateName); if (data && data.autoResearch) techList.push(templateName); } return techList; } /** * @param {string} phaseCode * @return {object} Sanitized object containing phase data */ function loadPhase(phaseCode) { let template = loadTechData(phaseCode); let phase = loadTechnology(phaseCode); phase.actualPhase = phaseCode; if (template.replaces !== undefined) phase.actualPhase = template.replaces[0]; return phase; } /** * @param {string} pairCode * @return {object} Contains a list and the requirements of the techs in the pair */ function loadTechnologyPair(pairCode) { var pairInfo = loadTechData(pairCode); return { "techs": [ pairInfo.top, pairInfo.bottom ], "reqs": DeriveTechnologyRequirements(pairInfo, g_SelectedCiv) }; } /** * @param {string} modCode * @return {object} Sanitized object containing modifier tech data */ function loadModifierTech(modCode) { if (!Engine.FileExists("simulation/data/technologies/"+modCode+".json")) return {}; return loadTechData(modCode); } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20625) @@ -1,525 +1,527 @@ const PI = Math.PI; const TWO_PI = 2 * Math.PI; const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const HEIGHT_UNITS_PER_METRE = 92; const MAP_BORDER_WIDTH = 3; +const g_DamageTypes = new DamageTypes(); + /** * Constants needed for heightmap_manipulation.js */ const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters const MIN_HEIGHT = - SEA_LEVEL; /** * Length of one tile of the terrain grid in metres. * Useful to transform footprint sizes of templates to the coordinate system used by getMapSize. */ const TERRAIN_TILE_SIZE = Engine.GetTerrainTileSize(); const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL; // Default angle for buildings const BUILDING_ORIENTATION = - PI / 4; const g_CivData = deepfreeze(loadCivFiles(false)); function fractionToTiles(f) { return g_Map.size * f; } function tilesToFraction(t) { return t / g_Map.size; } function fractionToSize(f) { return getMapArea() * f; } function sizeToFraction(s) { return s / getMapArea(); } function scaleByMapSize(min, max, minMapSize = 128, maxMapSize = 512) { return min + (max - min) * (g_Map.size - minMapSize) / (maxMapSize - minMapSize); } function cos(x) { return Math.cos(x); } function sin(x) { return Math.sin(x); } function abs(x) { return Math.abs(x); } function round(x) { return Math.round(x); } function lerp(a, b, t) { return a + (b-a) * t; } function sqrt(x) { return Math.sqrt(x); } function ceil(x) { return Math.ceil(x); } function floor(x) { return Math.floor(x); } function max(a, b) { return a > b ? a : b; } function min(a, b) { return a < b ? a : b; } /** * Retries the given function with those arguments as often as specified. */ function retryPlacing(placeFunc, retryFactor, amount, getResult, behaveDeprecated = false) { let maxFail = amount * retryFactor; let results = []; let good = 0; let bad = 0; while (good < amount && bad <= maxFail) { let result = placeFunc(); if (result !== undefined || behaveDeprecated) { ++good; if (getResult) results.push(result); } else ++bad; } return getResult ? results : good; } /** * Sets the x and z property of the given object (typically a Placer or Group) to a random point on the map. * @param passableOnly - Should be true for entity placement and false for terrain or elevation operations. */ function randomizeCoordinates(obj, passableOnly) { let border = passableOnly ? MAP_BORDER_WIDTH : 0; if (g_MapSettings.CircularMap) { // Polar coordinates // Uniformly distributed on the disk let halfMapSize = g_Map.size / 2 - border; let r = halfMapSize * Math.sqrt(randFloat(0, 1)); let theta = randFloat(0, 2 * Math.PI); obj.x = Math.floor(r * Math.cos(theta)) + halfMapSize; obj.z = Math.floor(r * Math.sin(theta)) + halfMapSize; } else { // Rectangular coordinates obj.x = randIntExclusive(border, g_Map.size - border); obj.z = randIntExclusive(border, g_Map.size - border); } } /** * Sets the x and z property of the given JS object (typically a Placer or Group) to a random point of the area. */ function randomizeCoordinatesFromAreas(obj, areas) { let pt = pickRandom(pickRandom(areas).points); obj.x = pt.x; obj.z = pt.z; } // TODO this is a hack to simulate the old behaviour of those functions // until all old maps are changed to use the correct version of these functions function createObjectGroupsDeprecated(group, player, constraint, amount, retryFactor = 10) { return createObjectGroups(group, player, constraint, amount, retryFactor, true); } function createObjectGroupsByAreasDeprecated(group, player, constraint, amount, retryFactor, areas) { return createObjectGroupsByAreas(group, player, constraint, amount, retryFactor, areas, true); } /** * Attempts to place the given number of areas in random places of the map. * Returns actually placed areas. */ function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10) { let placeFunc = function() { randomizeCoordinates(centeredPlacer, false); return createArea(centeredPlacer, painter, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, true, false); } /** * Attempts to place the given number of areas in random places of the given areas. * Returns actually placed areas. */ function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas) { let placeFunc = function() { randomizeCoordinatesFromAreas(centeredPlacer, areas); return createArea(centeredPlacer, painter, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, true, false); } /** * Attempts to place the given number of groups in random places of the map. * Returns the number of actually placed groups. */ function createObjectGroups(group, player, constraint, amount, retryFactor = 10, behaveDeprecated = false) { let placeFunc = function() { randomizeCoordinates(group, true); return createObjectGroup(group, player, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, false, behaveDeprecated); } /** * Attempts to place the given number of groups in random places of the given areas. * Returns the number of actually placed groups. */ function createObjectGroupsByAreas(group, player, constraint, amount, retryFactor, areas, behaveDeprecated = false) { let placeFunc = function() { randomizeCoordinatesFromAreas(group, areas); return createObjectGroup(group, player, constraint); }; return retryPlacing(placeFunc, retryFactor, amount, false, behaveDeprecated); } function createTerrain(terrain) { if (!(terrain instanceof Array)) return createSimpleTerrain(terrain); return new RandomTerrain(terrain.map(t => createTerrain(t))); } function createSimpleTerrain(terrain) { if (typeof(terrain) != "string") throw new Error("createSimpleTerrain expects string as input, received " + uneval(terrain)); // Split string by pipe | character, this allows specifying terrain + tree type in single string let params = terrain.split(TERRAIN_SEPARATOR, 2); if (params.length != 2) return new SimpleTerrain(terrain); return new SimpleTerrain(params[0], params[1]); } function placeObject(x, z, type, player, angle) { if (g_Map.validT(x, z)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function placeTerrain(x, z, terrainNames) { createTerrain(terrainNames).place(x, z); } function initTerrain(terrainNames) { let terrain = createTerrain(terrainNames); for (let x = 0; x < getMapSize(); ++x) for (let z = 0; z < getMapSize(); ++z) terrain.place(x, z); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function getMapBaseHeight() { return g_MapSettings.BaseHeight; } function createTileClass() { return g_Map.createTileClass(); } function getTileClass(id) { if (!g_Map.validClass(id)) return undefined; return g_Map.tileClasses[id]; } /** * Constructs a new Area shaped by the Placer meeting the Constraint and calls the Painters there. * Supports both Centered and Non-Centered Placers. */ function createArea(placer, painter, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); let points = placer.place(constraint); if (!points) return undefined; let area = g_Map.createArea(points); if (painter instanceof Array) painter = new MultiPainter(painter); painter.paint(area); return area; } /** * @param mode is one of the HeightPlacer constants determining whether to exclude the min/max elevation. */ function paintTerrainBasedOnHeight(minHeight, maxHeight, mode, terrain) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TerrainPainter(terrain)); } function paintTileClassBasedOnHeight(minHeight, maxHeight, mode, tileClass) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TileClassPainter(getTileClass(tileClass))); } function unPaintTileClassBasedOnHeight(minHeight, maxHeight, mode, tileClass) { createArea( new HeightPlacer(mode, minHeight, maxHeight), new TileClassUnPainter(getTileClass(tileClass))); } /** * Places the Entities of the given Group if they meet the Constraint * and sets the given player as the owner. */ function createObjectGroup(group, player, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); return group.place(player, constraint); } function getMapSize() { return g_Map.size; } function getMapArea() { return Math.square(g_Map.size); } function getMapCenter() { return deepfreeze(new Vector2D(g_Map.size / 2, g_Map.size / 2)); } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(playerID) { return g_MapSettings.PlayerData[playerID].Civ; } function areAllies(playerID1, playerID2) { return ( g_MapSettings.PlayerData[playerID1].Team !== undefined && g_MapSettings.PlayerData[playerID2].Team !== undefined && g_MapSettings.PlayerData[playerID1].Team != -1 && g_MapSettings.PlayerData[playerID2].Team != -1 && g_MapSettings.PlayerData[playerID1].Team === g_MapSettings.PlayerData[playerID2].Team); } function getPlayerTeam(playerID) { if (g_MapSettings.PlayerData[playerID].Team === undefined) return -1; return g_MapSettings.PlayerData[playerID].Team; } function getHeight(x, z) { return g_Map.getHeight(x, z); } function setHeight(x, z, height) { g_Map.setHeight(x, z, height); } function initHeight(height) { g_Map.initHeight(height); } /** * Utility functions for classes */ /** * Add point to given class by id */ function addToClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.add(x, z); } /** * Remove point from the given class by id */ function removeFromClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.remove(x, z); } /** * Create a painter for the given class */ function paintClass(id) { return new TileClassPainter(getTileClass(id)); } /** * Create a painter for the given class */ function unPaintClass(id) { return new TileClassUnPainter(getTileClass(id)); } /** * Create an avoid constraint for the given classes by the given distances */ function avoidClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a stay constraint for the given classes by the given distances */ function stayClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a border constraint for the given classes by the given distances */ function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/3; ++i) ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Checks if the given tile is in class "id" */ function checkIfInClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass === null) return 0; let members = tileClass.countMembersInRadius(x, z, 1); if (members === null) return 0; return members; } function getTerrainTexture(x, y) { return g_Map.getTexture(x, y); } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/wall_builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/wall_builder.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/wall_builder.js (revision 20625) @@ -1,1032 +1,942 @@ -//////////////////////////////////////////////////////////////////// -// This file contains functionality to place walls on random maps // -//////////////////////////////////////////////////////////////////// - -// To do: -// Check if all wall placement methods work with wall elements with entity === undefined (some still might raise errors in that case) -// Rename wall elements to fit the entity names so that entity = "structures/" + "civ + "_" + wallElement.type in the common case (as far as possible) -// Perhaps add Roman army camp to style palisades and add upgraded/balanced default palisade fortress types matching civ default fortresses strength -// Perhaps add further wall elements cornerInHalf, cornerOutHalf (banding PI/4) and adjust default fortress types to better fit in the octagonal territory of a civil center -// Perhaps swap angle and width in WallElement class(?) definition -// Adjust argument order to be always the same: -// Coordinates (center/start/target) -// Wall element arguments (wall/wallPart/fortressType/cornerElement) -// playerId (optional, default is 0/gaia) -// wallStyle (optional, default is the players civ/"palisades for gaia") -// angle/orientation (optional, default is 0) -// other (all optional) arguments especially those hard to define (wallPartsAssortment, maybe make an own function for it) -// Some arguments don't clearly match to this concept: -// endWithFirst (wall or other) -// skipFirstWall (wall or other) -// gateOccurence (wall or other) -// numCorners (wall or other) -// skipFirstWall (wall or other) -// maxAngle (angle or other) -// maxBendOff (angle or other, unused ATM!!!) -// irregularity -// maxTrys -// Add treasures to wall style "others" -// Adjust documentation -// Perhaps rename "endLeft" to "start" and "endRight" to "end" -// ?Use available civ-type wall elements rather than palisades: Remove "endLeft" and "endRight" as default wall elements and adjust default palisade fortress types? -// ?Remove "endRight", "endLeft" and adjust generic fortress types palisades? -// ?Think of something to enable splitting walls into two walls so more complex walls can be build and roads can have branches/crossroads? -// ?Readjust placement angle for wall elements with bending when used in linear/circular walls by their bending? - -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// WallElement class definition -// -// Concept: If placed unrotated the wall's course is towards positive Y (top) with "outside" right (+X) and "inside" left (-X) like unrotated entities has their drop-points right (in rmgen) -// The course of the wall will be changed by corners (bending != 0) and so the "inside"/"outside" direction -// -// type Descriptive string, example: "wallLong". NOTE: Not really needed. Mainly for custom wall elements and to get the wall element type in code -// entity Optional. Template name string of the entity to be placed, example: "structures/cart_wall_long". Default is undefined (No entity placed) -// angle Optional. The angle (float) added to place the entity so "outside" is right when the wall element is placed unrotated. Default is 0 -// width Optional. How far this wall element lengthens the wall (float), if unrotated the Y space needed. Default is 0 -// indent Optional. The lateral indentation of the entity, drawn "inside" (positive values) or pushed "outside" (negative values). Default is 0 -// bending Optional. How the course of the wall is changed after this element, positive is bending "in"/left/counter clockwise (like entity placement) -// NOTE: Bending is not supported by all placement functions (see there) -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function WallElement(type, entity, angle, width, indent, bending) -{ - this.type = type; - // Default wall element type documentation: - // Lengthening straight blocking (mainly left/right symmetric) wall elements (Walls and wall fortifications) - // "wall" A blocking straight wall element that mainly lengthens the wall, self-explanatory - // "wallShort" self-explanatory - // "wallLong" self-explanatory - // "tower" A blocking straight wall element with damage potential (but for palisades) that slightly lengthens the wall, example: wall tower, palisade tower(No attack) - // "wallFort" A blocking straight wall element with massive damage potential that lengthens the wall, example: fortress, palisade fort - // Lengthening straight non/custom blocking (mainly left/right symmetric) wall elements (Gates and entries) - // "gate" A blocking straight wall element with passability determined by owner, example: gate (Functionality not yet implemented) - // "entry" A non-blocking straight wall element (same width as gate) but without an actual template or just a flag/column/obelisk - // "entryTower" A non-blocking straight wall element (same width as gate) represented by a single (maybe indented) template, example: defence tower, wall tower, outpost, watchtower - // "entryFort" A non-blocking straight wall element represented by a single (maybe indented) template, example: fortress, palisade fort - // Bending wall elements (Wall corners) - // "cornerIn" A wall element bending the wall by PI/2 "inside" (left, +, see above), example: wall tower, palisade curve - // "cornerOut" A wall element bending the wall by PI/2 "outside" (right, -, see above), example: wall tower, palisade curve - // "cornerHalfIn" A wall element bending the wall by PI/4 "inside" (left, +, see above), example: wall tower, palisade curve. NOTE: Not yet implemented - // "cornerHalfOut" A wall element bending the wall by PI/4 "outside" (right, -, see above), example: wall tower, palisade curve. NOTE: Not yet implemented - // Zero length straight indented (mainly left/right symmetric) wall elements (Outposts/watchtowers and non-defensive base structures) - // "outpost" A zero-length wall element without bending far indented so it stands outside the wall, example: outpost, defence tower, watchtower - // "house" A zero-length wall element without bending far indented so it stands inside the wall that grants population bonus, example: house, hut, longhouse - // "barracks" A zero-length wall element without bending far indented so it stands inside the wall that grants unit production, example: barracks, tavern, ... - this.entity = entity; - this.angle = angle !== undefined ? angle : 0; - this.width = width !== undefined ? width : 0; - this.indent = indent !== undefined ? indent : 0; - this.bending = bending !== undefined ? bending : 0; +/** + * @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; } -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Fortress class definition -// -// A "fortress" here is a closed wall build of multiple wall elements attached together defined in Fortress.wall -// It's mainly the abstract shape defined in a Fortress instances wall because different styles can be used for it (see wallStyles) -// -// type Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements -// wall Optional. Array of wall element strings. Can be set afterwards. Default is an epty array. -// Example: ["entrance", "wall", "cornerIn", "wall", "gate", "wall", "entrance", "wall", "cornerIn", "wall", "gate", "wall", "cornerIn", "wall"] -// centerToFirstElement Optional. Object with properties "x" and "y" representing a vector from the visual center to the first wall element. Default is undefined -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function Fortress(type, wall, centerToFirstElement) +function loadWallset(wallsetPath, civ) { - this.type = type; // Only usefull to get the type of the actual fortress - this.wall = wall !== undefined ? wall : []; - this.centerToFirstElement = undefined; -} + let newWallset = { "curves": [] }; + let wallsetData = GetTemplateDataHelper(wallsetPath).wallSet; -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// wallStyles data structure for default wall styles -// -// A wall style is an associative array with all wall elements of that style in it associated with the wall element type string -// wallStyles holds all the wall styles within an associative array with the civ string or another descriptive strings as key -// Examples: "athen", "rome_siege", "palisades", "fence", "road" -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -var wallStyles = {}; - -// Generic civ dependent wall style definition. "rome_siege" needs some tweek... -var wallScaleByType = { - "athen": 1.5, - "brit": 1.5, - "cart": 1.8, - "gaul": 1.5, - "iber": 1.5, - "mace": 1.5, - "maur": 1.5, - "pers": 1.5, - "ptol": 1.5, - "rome": 1.5, - "sele": 1.5, - "spart": 1.5, - "rome_siege": 1.5 -}; + 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); -for (let style in wallScaleByType) -{ - let civ = style; - if (style == "rome_siege") - civ = "rome"; - - wallStyles[style] = { - // Default wall elements - "tower": new WallElement("tower", "structures/" + style + "_wall_tower", PI, wallScaleByType[style]), - "endLeft": new WallElement("endLeft", "structures/" + style + "_wall_tower", PI, wallScaleByType[style]), // Same as tower. To be compatible with palisades... - "endRight": new WallElement("endRight", "structures/" + style + "_wall_tower", PI, wallScaleByType[style]), // Same as tower. To be compatible with palisades... - "cornerIn": new WallElement("cornerIn", "structures/" + style + "_wall_tower", 5/4*PI, 0, 0.35 * wallScaleByType[style], PI/2), // 2^0.5 / 4 ~= 0.35 ~= 1/3 - "cornerOut": new WallElement("cornerOut", "structures/" + style + "_wall_tower", 3/4*PI, 0.71 * wallScaleByType[style], 0, -PI/2), // 2^0.5 / 2 ~= 0.71 ~= 2/3 - "wallShort": new WallElement("wallShort", "structures/" + style + "_wall_short", 0, 2*wallScaleByType[style]), - "wall": new WallElement("wall", "structures/" + style + "_wall_medium", 0, 4*wallScaleByType[style]), - "wallMedium": new WallElement("wall", "structures/" + style + "_wall_medium", 0, 4*wallScaleByType[style]), - "wallLong": new WallElement("wallLong", "structures/" + style + "_wall_long", 0, 6*wallScaleByType[style]), - - // Gate and entrance wall elements - "gate": new WallElement("gate", "structures/" + style + "_wall_gate", PI, 6*wallScaleByType[style]), - "entry": new WallElement("entry", undefined, 0, 6*wallScaleByType[style]), - "entryTower": new WallElement("entryTower", "structures/" + civ + "_defense_tower", PI, 6*wallScaleByType[style], -4*wallScaleByType[style]), - "entryFort": new WallElement("entryFort", "structures/" + civ + "_fortress", 0, 8*wallScaleByType[style], 6*wallScaleByType[style]), - - // Defensive wall elements with 0 width outside the wall - "outpost": new WallElement("outpost", "structures/" + civ + "_outpost", PI, 0, -4*wallScaleByType[style]), - "defenseTower": new WallElement("defenseTower", "structures/" + civ + "_defense_tower", PI, 0, -4*wallScaleByType[style]), - - // Base buildings wall elements with 0 width inside the wall - "barracks": new WallElement("barracks", "structures/" + civ + "_barracks", PI, 0, 4.5*wallScaleByType[style]), - "civilCentre": new WallElement("civilCentre", "structures/" + civ + "_civil_centre", PI, 0, 4.5*wallScaleByType[style]), - "farmstead": new WallElement("farmstead", "structures/" + civ + "_farmstead", PI, 0, 4.5*wallScaleByType[style]), - "field": new WallElement("field", "structures/" + civ + "_field", PI, 0, 4.5*wallScaleByType[style]), - "fortress": new WallElement("fortress", "structures/" + civ + "_fortress", PI, 0, 4.5*wallScaleByType[style]), - "house": new WallElement("house", "structures/" + civ + "_house", PI, 0, 4.5*wallScaleByType[style]), - "market": new WallElement("market", "structures/" + civ + "_market", PI, 0, 4.5*wallScaleByType[style]), - "storehouse": new WallElement("storehouse", "structures/" + civ + "_storehouse", PI, 0, 4.5*wallScaleByType[style]), - "temple": new WallElement("temple", "structures/" + civ + "_temple", PI, 0, 4.5*wallScaleByType[style]), - - // Generic space/gap wall elements - "space1": new WallElement("space1", undefined, 0, 1*wallScaleByType[style]), - "space2": new WallElement("space2", undefined, 0, 2*wallScaleByType[style]), - "space3": new WallElement("space3", undefined, 0, 3*wallScaleByType[style]), - "space4": new WallElement("space4", undefined, 0, 4*wallScaleByType[style]) - }; + newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length; + + return newWallset; } -// Add wall fortresses for all generic styles -wallStyles.athen.wallFort = new WallElement("wallFort", "structures/athen_fortress", 2*PI/2, 5.1, 1.9); -wallStyles.brit.wallFort = new WallElement("wallFort", "structures/brit_fortress", PI, 2.8); -wallStyles.cart.wallFort = new WallElement("wallFort", "structures/cart_fortress", PI, 5.1, 1.6); -wallStyles.gaul.wallFort = new WallElement("wallFort", "structures/gaul_fortress", PI, 4.2, 1.5); -wallStyles.iber.wallFort = new WallElement("wallFort", "structures/iber_fortress", PI, 5, 0.2); -wallStyles.mace.wallFort = new WallElement("wallFort", "structures/mace_fortress", 2*PI/2, 5.1, 1.9); -wallStyles.maur.wallFort = new WallElement("wallFort", "structures/maur_fortress", PI, 5.5); -wallStyles.pers.wallFort = new WallElement("wallFort", "structures/pers_fortress", PI, 5.6, 1.9); -wallStyles.ptol.wallFort = new WallElement("wallFort", "structures/ptol_fortress", 2*PI/2, 5.1, 1.9); -wallStyles.rome.wallFort = new WallElement("wallFort", "structures/rome_fortress", PI, 6.3, 2.1); -wallStyles.sele.wallFort = new WallElement("wallFort", "structures/sele_fortress", 2*PI/2, 5.1, 1.9); -wallStyles.spart.wallFort = new WallElement("wallFort", "structures/spart_fortress", 2*PI/2, 5.1, 1.9); - -// Adjust "rome_siege" style -wallStyles.rome_siege.wallFort = new WallElement("wallFort", "structures/rome_army_camp", PI, 7.2, 2); -wallStyles.rome_siege.entryFort = new WallElement("entryFort", "structures/rome_army_camp", PI, 12, 7); -wallStyles.rome_siege.house = new WallElement("house", "structures/rome_tent", PI, 0, 4); - -// Add special wall styles not well to implement generic (and to show how custom styles can be added) - -wallScaleByType.palisades = 0.55; -let gate = new WallElement("gate", "other/palisades_rocks_gate", PI, 3.6); -wallStyles.palisades = { - "wall": new WallElement("wall", "other/palisades_rocks_medium", 0, 2.3), - "wallMedium": new WallElement("wall", "other/palisades_rocks_medium", 0, 2.3), - "wallLong": new WallElement("wall", "other/palisades_rocks_long", 0, 3.5), - "wallShort": new WallElement("wall", "other/palisades_rocks_short", 0, 1.2), - "tower": new WallElement("tower", "other/palisades_rocks_tower", -PI/2, 0.7), - "wallFort": new WallElement("wallFort", "other/palisades_rocks_fort", PI, 1.7), - "gate": gate, - "entry": new WallElement("entry", undefined, gate.angle, gate.width), - "entryTower": new WallElement("entryTower", "other/palisades_rocks_watchtower", 0, gate.width, -3), - "entryFort": new WallElement("entryFort", "other/palisades_rocks_fort", PI, 6, 3), - "cornerIn": new WallElement("cornerIn", "other/palisades_rocks_curve", 3*PI/4, 2.1, 0.7, PI/2), - "cornerOut": new WallElement("cornerOut", "other/palisades_rocks_curve", 5*PI/4, 2.1, -0.7, -PI/2), - "outpost": new WallElement("outpost", "other/palisades_rocks_outpost", PI, 0, -2), - "house": new WallElement("house", "other/celt_hut", PI, 0, 5), - "barracks": new WallElement("barracks", "structures/gaul_tavern", PI, 0, 5), - "endRight": new WallElement("endRight", "other/palisades_rocks_end", -PI/2, 0.2), - "endLeft": new WallElement("endLeft", "other/palisades_rocks_end", PI/2, 0.2) -}; - -// NOTE: This is not a wall style in the common sense. Use with care! -wallStyles.road = { - "short": new WallElement("road", "actor|props/special/eyecandy/road_temperate_short.xml", PI/2, 4.5), - "long": new WallElement("road", "actor|props/special/eyecandy/road_temperate_long.xml", PI/2, 9.5), - - // Correct width by -2*indent to fit xStraicht/corner - "cornerLeft": new WallElement("road", "actor|props/special/eyecandy/road_temperate_corner.xml", -PI/2, 4.5-2*1.25, 1.25, PI/2), - "cornerRight": new WallElement("road", "actor|props/special/eyecandy/road_temperate_corner.xml", 0, 4.5-2*1.25, -1.25, -PI/2), - "curveLeft": new WallElement("road", "actor|props/special/eyecandy/road_temperate_curve_small.xml", -PI/2, 4.5+2*0.2, -0.2, PI/2), - "curveRight": new WallElement("road", "actor|props/special/eyecandy/road_temperate_curve_small.xml", 0, 4.5+2*0.2, 0.2, -PI/2), - - "start": new WallElement("road", "actor|props/special/eyecandy/road_temperate_end.xml", PI/2, 2), - "end": new WallElement("road", "actor|props/special/eyecandy/road_temperate_end.xml", -PI/2, 2), - "xStraight": new WallElement("road", "actor|props/special/eyecandy/road_temperate_intersect_x.xml", 0, 4.5), - "xLeft": new WallElement("road", "actor|props/special/eyecandy/road_temperate_intersect_x.xml", 0, 4.5, 0, PI/2), - "xRight": new WallElement("road", "actor|props/special/eyecandy/road_temperate_intersect_x.xml", 0, 4.5, 0, -PI/2), - "tLeft": new WallElement("road", "actor|props/special/eyecandy/road_temperate_intersect_T.xml", PI, 4.5, 1.25), - "tRight": new WallElement("road", "actor|props/special/eyecandy/road_temperate_intersect_T.xml", 0, 4.5, -1.25), -}; - -// NOTE: This is not a wall style in the common sense. Use with care! -wallStyles.other = { - "fence": new WallElement("fence", "other/fence_long", -PI/2, 3.1), - "fence_medium": new WallElement("fence", "other/fence_long", -PI/2, 3.1), - "fence_short": new WallElement("fence_short", "other/fence_short", -PI/2, 1.5), - "fence_stone": new WallElement("fence_stone", "other/fence_stone", -PI/2, 2.5), - "palisade": new WallElement("palisade", "other/palisades_rocks_short", 0, 1.2), - "column": new WallElement("column", "other/column_doric", 0, 1), - "obelisk": new WallElement("obelisk", "other/obelisk", 0, 2), - "spike": new WallElement("spike", "other/palisades_angle_spike", -PI/2, 1), - "bench": new WallElement("bench", "other/bench", PI/2, 1.5), - "benchForTable": new WallElement("benchForTable", "other/bench", 0, 0.5), - "table": new WallElement("table", "other/table_rectangle", 0, 1), - "table_square": new WallElement("table_square", "other/table_square", PI/2, 1), - "flag": new WallElement("flag", "special/rallypoint", PI, 1), - "standing_stone": new WallElement("standing_stone", "gaia/special_ruins_standing_stone", PI, 1), - "settlement": new WallElement("settlement", "gaia/special_settlement", PI, 6), - "gap": new WallElement("gap", undefined, 0, 2), - "gapSmall": new WallElement("gapSmall", undefined, 0, 1), - "gapLarge": new WallElement("gapLarge", undefined, 0, 4), - "cornerIn": new WallElement("cornerIn", undefined, 0, 0, 0, PI/2), - "cornerOut": new WallElement("cornerOut", undefined, 0, 0, 0, -PI/2) -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// fortressTypes data structure for some default fortress types -// -// A fortress type is just an instance of the Fortress class with actually something in it -// fortressTypes holds all the fortresses within an associative array with a descriptive string as key (e.g. matching the map size) -// Examples: "tiny", "veryLarge" -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -var fortressTypes = {}; +/** + * 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 {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 wallParts = { - "tiny": ["gate", "tower", "wallShort", "cornerIn", "wallShort", "tower"], - "small": ["gate", "tower", "wall", "cornerIn", "wall", "tower"], - "medium": ["gate", "tower", "wallLong", "cornerIn", "wallLong", "tower"], - "normal": ["gate", "tower", "wall", "cornerIn", "wall", - "cornerOut", "wall", "cornerIn", "wall", "tower"], - "large": ["gate", "tower", "wallLong", "cornerIn", "wallLong", - "cornerOut", "wallLong", "cornerIn", "wallLong", "tower"], - "veryLarge": ["gate", "tower", "wall", "cornerIn", "wall", "cornerOut", "wallLong", - "cornerIn", "wallLong", "cornerOut", "wall", "cornerIn", "wall", "tower"], - "giant": ["gate", "tower", "wallLong", "cornerIn", "wallLong", "cornerOut", "wallLong", - "cornerIn", "wallLong", "cornerOut", "wallLong", "cornerIn", "wallLong", "tower"] - }; + let defaultFortresses = {}; - for (let type in wallParts) - { - fortressTypes[type] = new Fortress(type); + /** + * 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; - let wp = wallParts[type]; - fortressTypes[type].wall = wp.concat(wp, wp, wp); - } -} + defaultFortresses[newKey] = { "wall": [] }; + for (let j = 0; j < oldWall.length; ++j) + { + defaultFortresses[newKey].wall.push(oldWall[j]); -// Setup some better looking semi default fortresses for "palisades" style -for (let type in fortressTypes) -{ - let newKey = type + "Palisades"; - let oldWall = fortressTypes[type].wall; - fortressTypes[newKey] = new Fortress(newKey); - let fillTowersBetween = ["wallShort", "wall", "wallLong", "endLeft", "endRight", "cornerIn", "cornerOut"]; - for (let j = 0; j < oldWall.length; ++j) - { - fortressTypes[newKey].wall.push(oldWall[j]); // Only works if the first element is not in fillTowersBetween (e.g. entry or gate like it should be) - if (j+1 < oldWall.length) - if (fillTowersBetween.indexOf(oldWall[j]) > -1 && fillTowersBetween.indexOf(oldWall[j+1]) > -1) // ... > -1 means "exists" here - fortressTypes[newKey].wall.push("tower"); + if (j + 1 < oldWall.length && + fillTowersBetween.indexOf(oldWall[j]) != -1 && + fillTowersBetween.indexOf(oldWall[j + 1]) != -1) + { + defaultFortresses[newKey].wall.push("tower"); + } + } } + + return defaultFortresses; } -// Setup some balanced (to civ type fortresses) semi default fortresses for "palisades" style -// TODO +/** + * 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. + */ +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; -// Add some "fortress types" for roads (will only work with style "road") -{ - // ["start", "short", "xRight", "xLeft", "cornerLeft", "xStraight", "long", "xLeft", "xRight", "cornerRight", "tRight", "tLeft", "xRight", "xLeft", "curveLeft", "xStraight", "curveRight", "end"]; - let roadTypes = { - "road01": ["short", "curveLeft", "short", "curveLeft", "short", "curveLeft", "short", "curveLeft"], - "road02": ["short", "cornerLeft", "short", "cornerLeft", "short", "cornerLeft", "short", "cornerLeft"], - "road03": ["xStraight", "curveLeft", "xStraight", "curveLeft", "xStraight", "curveLeft", "xStraight", "curveLeft"], - "road04": ["start", "curveLeft", "tRight", "cornerLeft", "tRight", "curveRight", "short", "xRight", "curveLeft", "xRight", "short", "cornerLeft", "tRight", "short", - "curveLeft", "short", "tRight", "cornerLeft", "short", "xRight", "curveLeft", "xRight", "short", "curveRight", "tRight", "cornerLeft", "tRight", "curveLeft", "end"], - "road05": ["start", "tLeft", "short", "xRight", - "curveLeft", "xRight", "tRight", "cornerLeft", "tRight", - "curveLeft", "short", "tRight", "cornerLeft", "xRight", - "cornerLeft", "xRight", "short", "tRight", "curveLeft", "end"], - }; + if (ret.bend != quarterBend) + { + ret.angle += Math.PI / 4; + ret.indent = ret.length / 4; + ret.length = 0; + ret.bend = Math.PI / 2; + } + break; - for (let type in roadTypes) - fortressTypes[type] = new Fortress(type, roadTypes[type]); -} + 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; + } -/////////////////////////////// -// Define some helper functions -/////////////////////////////// - -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// getWallAlignment -// -// 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 -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function getWallAlignment(startX, startY, wall, style, orientation) -{ - // Graciously handle arguments - if (wall === undefined) - wall = []; - if (!wallStyles.hasOwnProperty(style)) - { - warn("Function getWallAlignment: Unknown style: " + style + ' (falling back to "athen")'); - style = "athen"; + 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" : "other/palisades_rocks_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.")); + } } - orientation = orientation || 0; + // 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, {}, g_DamageTypes, {}); + 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 {number} startX + * @param {number} startY + * @param {array} [wall] + * @param {string} [style] + * @param {number} [orientation] + * @returns {array} + */ +function getWallAlignment(startX, startY, wall = [], style = "athen_stone", orientation = 0) +{ + style = validateStyle(style); let alignment = []; let wallX = startX; let wallY = startY; + for (let i = 0; i < wall.length; ++i) { - let element = wallStyles[style][wall[i]]; - if (element === undefined && i == 0) - warn("No valid wall element: " + wall[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; + } // Indentation - let placeX = wallX - element.indent * cos(orientation); - let placeY = wallY - element.indent * sin(orientation); + let placeX = wallX - element.indent * Math.cos(orientation); + let placeY = wallY - element.indent * Math.sin(orientation); // Add wall elements entity placement arguments to the alignment alignment.push({ "x": placeX, "y": placeY, - "entity": element.entity, + "templateName": element.templateName, "angle": orientation + element.angle }); // Preset vars for the next wall element - if (i+1 < wall.length) + if (i + 1 < wall.length) { - orientation += element.bending; - let nextElement = wallStyles[style][wall[i+1]]; - if (nextElement === undefined) - warn("No valid wall element: " + wall[i+1]); - let distance = (element.width + nextElement.width)/2; + 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 bending = element.bending; - if (bending !== 0 && indent !== 0) + let bend = element.bend; + if (bend != 0 && indent != 0) { // Indent correction to adjust distance - distance += indent*sin(bending); + distance += indent * Math.sin(bend); + // Indent correction to normalize indentation - wallX += indent * cos(orientation); - wallY += indent * sin(orientation); + wallX += indent * Math.cos(orientation); + wallY += indent * Math.sin(orientation); } // Set the next coordinates of the next element in the wall without indentation adjustment - wallX -= distance * sin(orientation); - wallY += distance * cos(orientation); + wallX -= distance * Math.sin(orientation); + wallY += distance * Math.cos(orientation); } } return alignment; } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// getCenterToFirstElement -// -// Center calculation works like getting the center of mass assuming all wall elements have the same "weight" -// -// It returns the vector from the center to the first wall element -// Used to get centerToFirstElement of fortresses by default -////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * 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. + */ function getCenterToFirstElement(alignment) { let centerToFirstElement = { "x": 0, "y": 0 }; - for (let i = 0; i < alignment.length; ++i) + for (let align of alignment) { - centerToFirstElement.x -= alignment[i].x/alignment.length; - centerToFirstElement.y -= alignment[i].y/alignment.length; + centerToFirstElement.x -= align.x / alignment.length; + centerToFirstElement.y -= align.y / alignment.length; } return centerToFirstElement; } -////////////////////////////////////////////////////////////////// -// getWallLength -// -// NOTE: Does not support bending wall elements like corners! -// e.g. used by placeIrregularPolygonalWall -////////////////////////////////////////////////////////////////// -function getWallLength(wall, style) +/** + * 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) { - // Graciously handle arguments - if (wall === undefined) - wall = []; - if (!wallStyles.hasOwnProperty(style)) - { - warn("Function getWallLength: Unknown style: " + style + ' (falling back to "athen")'); - style = "athen"; - } + style = validateStyle(style); let length = 0; - for (let i = 0; i < wall.length; ++i) - length += wallStyles[style][wall[i]].width; + let overlap = g_WallStyles[style].overlap; + for (let element of wall) + length += getWallElement(element, style).length - overlap; return length; } -///////////////////////////////////////////// -// Define the different wall placer functions -///////////////////////////////////////////// - -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeWall -// -// Places a wall with wall elements attached to another like determined by WallElement properties. -// -// startX, startY Where the first wall element should be placed -// wall Array of wall element type strings. Example: ["endLeft", "wallLong", "tower", "wallLong", "endRight"] -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Number of the player the wall will be placed for. Default is 0 (gaia) -// orientation Optional. Angle the first wall element is placed. Default is 0 -// 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(startX, startY, wall, style, playerId, orientation) +/** + * 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) { - // Graciously handle arguments - if (wall === undefined) - wall = []; - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) + if (!style || !g_WallStyles[style]) { if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); + return Object.keys(g_WallStyles)[0]; + + style = getCivCode(playerId) + "_stone"; + return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style; } - orientation = orientation || 0; + return style; +} + +/** + * Define the different wall placer functions + */ + +/** + * Places an abitrary wall beginning at the location comprised of the array of elements provided. + * + * @param {number} startX + * @param {number} startY + * @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(startX, startY, wall = [], style, playerId = 0, orientation = 0) +{ + style = validateStyle(style, playerId); - // Get wall alignment let AM = getWallAlignment(startX, startY, wall, style, orientation); - // Place the wall for (let iWall = 0; iWall < wall.length; ++iWall) - { - let entity = AM[iWall].entity; - if (entity !== undefined) - placeObject(AM[iWall].x, AM[iWall].y, entity, playerId, AM[iWall].angle); - } + if (AM[iWall].templateName) + placeObject(AM[iWall].x, AM[iWall].y, AM[iWall].templateName, playerId, AM[iWall].angle); } -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeCustomFortress -// -// Place a fortress (mainly a closed wall build like placeWall) centered at centerX/centerY -// The fortress wall should always start with the main entrance (like "entry" or "gate") to get the orientation right (like placeObject) -// -// fortress An instance of Fortress with a wall defined -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Number of the player the wall will be placed for. Default is 0 (gaia) -// orientation Optional. Angle the first wall element (should be a gate or entrance) is placed. Default is BUILDING_ORIENTATION -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeCustomFortress(centerX, centerY, fortress, style, playerId = 0, orientation = BUILDING_ORIENTATION) +/** + * 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 {number} centerX + * @param {number} centerY + * @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(centerX, centerY, fortress, style, playerId = 0, orientation = 0) { - // Graciously handle arguments - fortress = fortress || fortressTypes.medium; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } + 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(0, 0, fortress.wall, style)); + // Placing the fortress wall - let startX = centerX + centerToFirstElement.x * cos(orientation) - centerToFirstElement.y * sin(orientation); - let startY = centerY + centerToFirstElement.y * cos(orientation) + centerToFirstElement.x * sin(orientation); + let startX = centerX + centerToFirstElement.x * Math.cos(orientation) - centerToFirstElement.y * Math.sin(orientation); + let startY = centerY + centerToFirstElement.y * Math.cos(orientation) + centerToFirstElement.x * Math.sin(orientation); placeWall(startX, startY, fortress.wall, style, playerId, orientation); } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeFortress -// -// Like placeCustomFortress just it takes type (a fortress type string, has to be in fortressTypes) instead of an instance of Fortress -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeFortress(centerX, centerY, type, style, playerId, orientation) +/** + * 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(centerX, centerY, type = "medium", style, playerId = 0, orientation = 0) { - // Graciously handle arguments - type = type || "medium"; - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } - orientation = orientation || 0; - // Call placeCustomFortress with the given arguments - placeCustomFortress(centerX, centerY, fortressTypes[type], style, playerId, orientation); + placeCustomFortress(centerX, centerY, g_FortressTypes[type], style, playerId, orientation); } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeLinearWall -// -// Places a straight wall from a given coordinate to an other repeatedly using the wall parts. -// -// startX/startY Coordinate of the approximate beginning of the wall (Not the place of the first wall element) -// targetX/targetY Coordinate of the approximate ending of the wall (Not the place of the last wall element) -// wallPart Optional. An array of NON-BENDING wall element type strings. Default is ["tower", "wallLong"] -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Integer number of the player. Default is 0 (gaia) -// endWithFirst Optional. A boolean value. If true the 1st wall element in the wallPart array will finalize the wall. Default is true -// -// TODO: Maybe add angle offset for more generic looking? -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeLinearWall(startX, startY, targetX, targetY, wallPart, style, playerId, endWithFirst) +/** + * 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 {number} startX - Approximate start point of the wall. + * @param {number} startY - Approximate start point of the wall. + * @param {number} targetX - Approximate end point of the wall. + * @param {number} targetY - 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(startX, startY, targetX, targetY, wallPart = undefined, style, playerId = 0, endWithFirst = true) { - // Setup optional arguments to the default - wallPart = wallPart || ["tower", "wallLong"]; - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } - endWithFirst = typeof endWithFirst == "undefined" ? true : endWithFirst; + wallPart = wallPart || ["tower", "long"]; + style = validateStyle(style, playerId); // Check arguments - for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) - { - let bending = wallStyles[style][wallPart[elementIndex]].bending; - if (bending != 0) - warn("Bending is not supported by placeLinearWall but a bending wall element is used: " + wallPart[elementIndex] + " -> wallStyles[style][wallPart[elementIndex]].entity"); - } + 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 = Math.euclidDistance2D(startX, startY, targetX, targetY); - let wallPartLength = 0; - for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) - wallPartLength += wallStyles[style][wallPart[elementIndex]].width; - let numParts = 0; + let wallPartLength = getWallLength(style, wallPart); + let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) - numParts = ceil((totalLength - wallStyles[style][wallPart[0]].width) / wallPartLength); - else - numParts = ceil(totalLength / wallPartLength); + numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); + // Setup scale factor - let scaleFactor = 1; + let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) - scaleFactor = totalLength / (numParts * wallPartLength + wallStyles[style][wallPart[0]].width); - else - scaleFactor = totalLength / (numParts * wallPartLength); + scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); + // Setup angle let wallAngle = getAngle(startX, startY, targetX, targetY); // NOTE: function "getAngle()" is about to be changed... - let placeAngle = wallAngle - PI/2; + let placeAngle = wallAngle - Math.PI / 2; + // Place wall entities let x = startX; let y = startY; + let overlap = g_WallStyles[style].overlap; for (let partIndex = 0; partIndex < numParts; ++partIndex) { for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) { - let wallEle = wallStyles[style][wallPart[elementIndex]]; - // Width correction - x += scaleFactor * wallEle.width/2 * cos(wallAngle); - y += scaleFactor * wallEle.width/2 * sin(wallAngle); + let wallEle = getWallElement(wallPart[elementIndex], style); + let wallLength = (wallEle.length - overlap) / 2; + let distX = scaleFactor * wallLength * Math.cos(wallAngle); + let distY = scaleFactor * wallLength * Math.sin(wallAngle); + + // Length correction + x += distX; + y += distY; + // Indent correction - let placeX = x - wallEle.indent * sin(wallAngle); - let placeY = y + wallEle.indent * cos(wallAngle); + let placeX = x - wallEle.indent * Math.sin(wallAngle); + let placeY = y + wallEle.indent * Math.cos(wallAngle); + // Placement - let entity = wallEle.entity; - if (entity !== undefined) - placeObject(placeX, placeY, entity, playerId, placeAngle + wallEle.angle); - x += scaleFactor * wallEle.width/2 * cos(wallAngle); - y += scaleFactor * wallEle.width/2 * sin(wallAngle); + if (wallEle.templateName) + placeObject(placeX, placeY, wallEle.templateName, playerId, placeAngle + wallEle.angle); + + // Prep for next object + x += distX; + y += distY; } } if (endWithFirst) { - let wallEle = wallStyles[style][wallPart[0]]; - x += scaleFactor * wallEle.width/2 * cos(wallAngle); - y += scaleFactor * wallEle.width/2 * sin(wallAngle); - let entity = wallEle.entity; - if (entity !== undefined) - placeObject(x, y, entity, playerId, placeAngle + wallEle.angle); + let wallEle = getWallElement(wallPart[0], style); + let wallLength = (wallEle.length - overlap) / 2; + x += scaleFactor * wallLength * Math.cos(wallAngle); + y += scaleFactor * wallLength * Math.sin(wallAngle); + if (wallEle.templateName) + placeObject(x, y, wallEle.templateName, playerId, placeAngle + wallEle.angle); } } -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeCircularWall -// -// Place a circular wall of repeated wall elements given in the argument wallPart around centerX/centerY with the given radius -// The wall can be opened forming more an arc than a circle if maxAngle < 2*PI -// The orientation then determines where this open part faces (0 means right like unrotated building's drop-points) -// -// centerX/Y Coordinates of the circle's center -// radius How wide the circle should be (approximate, especially if maxBendOff != 0) -// wallPart Optional. An array of NON-BENDING wall element type strings. Default is ["tower", "wallLong"] -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Integer number of the player. Default is 0 (gaia) -// orientation Optional. Where the open part of the (circular) arc should face (if maxAngle is < 2*PI). Default is 0 -// maxAngle Optional. How far the wall should circumvent the center. Default is 2*PI (full circle) -// endWithFirst Optional. Boolean. If true the 1st wall element in the wallPart array will finalize the wall. Default is false for full circles, else true -// maxBendOff Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle) -// -// NOTE: Don't use wall elements with bending like corners! -// TODO: Perhaps add eccentricity and maxBendOff functionality (untill now an unused argument) -// TODO: Perhaps add functionality for spirals -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeCircularWall(centerX, centerY, radius, wallPart, style, playerId, orientation, maxAngle, endWithFirst, maxBendOff) +/** + * 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 {number} centerX - Center of the circle or arc. + * @param {number} centerY - 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] - Where the open part of the arc should face, if applicable. + * @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(centerX, centerY, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = Math.PI * 2, endWithFirst, maxBendOff = 0) { - // Setup optional arguments to the default - wallPart = wallPart || ["tower", "wallLong"]; - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } - orientation = orientation || 0; - maxAngle = maxAngle || 2*PI; + wallPart = wallPart || ["tower", "long"]; + style = validateStyle(style, playerId); if (endWithFirst === undefined) - endWithFirst = maxAngle < 2*PI - 0.001; // Can this be done better? - - maxBendOff = maxBendOff || 0; + endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better? // Check arguments - if (maxBendOff > PI/2 || maxBendOff < 0) - warn("placeCircularWall maxBendOff sould satisfy 0 < maxBendOff < PI/2 (~1.5) but it is: " + maxBendOff); - for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) - { - let bending = wallStyles[style][wallPart[elementIndex]].bending; - if (bending != 0) - warn("Bending is not supported by placeCircularWall but a bending wall element is used: " + wallPart[elementIndex]); - } + 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 = 0; - for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) - wallPartLength += wallStyles[style][wallPart[elementIndex]].width; - let numParts = 0; + let wallPartLength = getWallLength(style, wallPart); + let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) - numParts = ceil((totalLength - wallStyles[style][wallPart[0]].width) / wallPartLength); - else - numParts = ceil(totalLength / wallPartLength); + numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor - let scaleFactor = 1; + let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) - scaleFactor = totalLength / (numParts * wallPartLength + wallStyles[style][wallPart[0]].width); - else - scaleFactor = totalLength / (numParts * wallPartLength); + scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Place wall entities - let actualAngle = orientation + (2*PI - maxAngle) / 2; - let x = centerX + radius*cos(actualAngle); - let y = centerY + radius*sin(actualAngle); + let actualAngle = orientation + (Math.PI * 2 - maxAngle) / 2; + let x = centerX + radius * Math.cos(actualAngle); + let y = centerY + radius * Math.sin(actualAngle); + let overlap = g_WallStyles[style].overlap; for (let partIndex = 0; partIndex < numParts; ++partIndex) - for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) + for (let wallEle of wallPart) { - let wallEle = wallStyles[style][wallPart[elementIndex]]; + wallEle = getWallElement(wallEle, style); + // Width correction - let addAngle = scaleFactor * wallEle.width / radius; - let targetX = centerX + radius * cos(actualAngle + addAngle); - let targetY = centerY + radius * sin(actualAngle + addAngle); - let placeX = x + (targetX - x)/2; - let placeY = y + (targetY - y)/2; - let placeAngle = actualAngle + addAngle/2; + let addAngle = scaleFactor * (wallEle.length - overlap) / radius; + let targetX = centerX + radius * Math.cos(actualAngle + addAngle); + let targetY = centerY + radius * Math.sin(actualAngle + addAngle); + let placeX = x + (targetX - x) / 2; + let placeY = y + (targetY - y) / 2; + let placeAngle = actualAngle + addAngle / 2; + // Indent correction - placeX -= wallEle.indent * cos(placeAngle); - placeY -= wallEle.indent * sin(placeAngle); + placeX -= wallEle.indent * Math.cos(placeAngle); + placeY -= wallEle.indent * Math.sin(placeAngle); + // Placement - let entity = wallEle.entity; - if (entity !== undefined) - placeObject(placeX, placeY, entity, playerId, placeAngle + wallEle.angle); + if (wallEle.templateName) + placeObject(placeX, placeY, wallEle.templateName, playerId, placeAngle + wallEle.angle); + // Prepare for the next wall element actualAngle += addAngle; - x = centerX + radius*cos(actualAngle); - y = centerY + radius*sin(actualAngle); + x = centerX + radius * Math.cos(actualAngle); + y = centerY + radius * Math.sin(actualAngle); } if (endWithFirst) { - let wallEle = wallStyles[style][wallPart[0]]; - let addAngle = scaleFactor * wallEle.width / radius; - let targetX = centerX + radius * cos(actualAngle + addAngle); - let targetY = centerY + radius * sin(actualAngle + addAngle); - let placeX = x + (targetX - x)/2; - let placeY = y + (targetY - y)/2; - let placeAngle = actualAngle + addAngle/2; - placeObject(placeX, placeY, wallEle.entity, playerId, placeAngle + wallEle.angle); + let wallEle = getWallElement(wallPart[0], style); + let addAngle = scaleFactor * wallEle.length / radius; + let targetX = centerX + radius * Math.cos(actualAngle + addAngle); + let targetY = centerY + radius * Math.sin(actualAngle + addAngle); + let placeX = x + (targetX - x) / 2; + let placeY = y + (targetY - y) / 2; + let placeAngle = actualAngle + addAngle / 2; + placeObject(placeX, placeY, wallEle.templateName, playerId, placeAngle + wallEle.angle); } } -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placePolygonalWall -// -// Place a polygonal wall of repeated wall elements given in the argument wallPart around centerX/centerY with the given radius -// -// centerX/Y Coordinates of the polygon's center -// radius How wide the circle should be in which the polygon fits -// wallPart Optional. An array of NON-BENDING wall element type strings. Default is ["wallLong", "tower"] -// cornerWallElement Optional. Wall element to be placed at the polygon's corners. Default is "tower" -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Integer number of the player. Default is 0 (gaia) -// orientation Optional. Angle from the center to the first linear wall part placed. Default is 0 (towards positive X/right) -// numCorners Optional. How many corners the polygon will have. Default is 8 (matching a civ centers territory) -// skipFirstWall Optional. Boolean. If the first linear wall part will be left opened as entrance. Default is true -// -// NOTE: Don't use wall elements with bending like corners! -// TODO: Replace skipFirstWall with firstWallPart to enable gate/defended entrance placement -// TODO: Check some arguments -// TODO: Add eccentricity and perhaps make it just call placeIrregularPolygonalWall with irregularity = 0 -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placePolygonalWall(centerX, centerY, radius, wallPart, cornerWallElement, style, playerId, orientation, numCorners, skipFirstWall = true) +/** + * 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 {number} centerX + * @param {number} centerY + * @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(centerX, centerY, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true) { - // Setup optional arguments to the default - wallPart = wallPart || ["wallLong", "tower"]; - cornerWallElement = cornerWallElement || "tower"; // Don't use wide elements for this. Not supported well... - playerId = playerId || 0; - - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } - orientation = orientation || 0; - numCorners = numCorners || 8; + wallPart = wallPart || ["long", "tower"]; + style = validateStyle(style, playerId); // Setup angles - let angleAdd = 2*PI/numCorners; - let angleStart = orientation - angleAdd/2; + let angleAdd = Math.PI * 2 / numCorners; + let angleStart = orientation - angleAdd / 2; + // Setup corners let corners = []; for (let i = 0; i < numCorners; ++i) - corners.push([centerX + radius*cos(angleStart + i*angleAdd), centerY + radius*sin(angleStart + i*angleAdd)]); + corners.push([ + centerX + radius * Math.cos(angleStart + i * angleAdd), + centerY + radius * Math.sin(angleStart + i * angleAdd) + ]); + // Place Corners and walls for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i][0], corners[i][1], centerX, centerY); - placeObject(corners[i][0], corners[i][1], wallStyles[style][cornerWallElement].entity, playerId, angleToCorner); + placeObject(corners[i][0], corners[i][1], getWallElement(cornerWallElement, style).templateName, playerId, angleToCorner); if (!skipFirstWall || i != 0) + { + let cornerLength = getWallElement(cornerWallElement, style).length / 2; + let cornerAngle = angleToCorner + angleAdd / 2; + let cornerX = cornerLength * Math.sin(cornerAngle); + let cornerY = cornerLength * Math.cos(cornerAngle); + let targetCorner = (i + 1) % numCorners; placeLinearWall( // Adjustment to the corner element width (approximately) - corners[i][0] + wallStyles[style][cornerWallElement].width/2 * sin(angleToCorner + angleAdd/2), // startX - corners[i][1] - wallStyles[style][cornerWallElement].width/2 * cos(angleToCorner + angleAdd/2), // startY - corners[(i+1)%numCorners][0] - wallStyles[style][cornerWallElement].width/2 * sin(angleToCorner + angleAdd/2), // targetX - corners[(i+1)%numCorners][1] + wallStyles[style][cornerWallElement].width/2 * cos(angleToCorner + angleAdd/2), // targetY + corners[i][0] + cornerX, // startX + corners[i][1] - cornerY, // startY + corners[targetCorner][0] - cornerX, // targetX + corners[targetCorner][1] + cornerY, // targetY wallPart, style, playerId); + } } } -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeIrregularPolygonalWall -// -// Place an irregular polygonal wall of some wall parts to choose from around centerX/centerY with the given radius -// -// centerX/Y Coordinates of the polygon's center -// radius How wide the circle should be in which the polygon fits -// cornerWallElement Optional. Wall element to be placed at the polygon's corners. Default is "tower" -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// playerId Optional. Integer number of the player. Default is 0 (gaia) -// orientation Optional. Angle from the center to the first linear wall part placed. Default is 0 (towards positive X/right) -// numCorners Optional. How many corners the polygon will have. Default is 8 (matching a civ centers territory) -// irregularity Optional. How irregular the polygon will be. 0 means regular, 1 means VERY irregular. Default is 0.5 -// skipFirstWall Optional. Boolean. If the first linear wall part will be left opened as entrance. Default is true -// wallPartsAssortment Optional. An array of wall part arrays to choose from for each linear wall connecting the corners. Default is hard to describe ^^ -// -// NOTE: wallPartsAssortment is put to the end because it's hardest to set -// NOTE: Don't use wall elements with bending like corners! -// TODO: Replace skipFirstWall with firstWallPart to enable gate/defended entrance placement -// TODO: Check some arguments -// TODO: Perhaps add eccentricity -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeIrregularPolygonalWall(centerX, centerY, radius, cornerWallElement, style, playerId, orientation, numCorners, irregularity, skipFirstWall, wallPartsAssortment) +/** + * 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 {number} centerX + * @param {number} centerY + * @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(centerX, centerY, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment) { - // Setup optional arguments - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } + 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 = [["wallShort"], ["wall"], ["wallLong"], ["gate", "tower", "wallShort"]]; + let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]]; let centeredWallPart = ["gate"]; - let extandingWallPartAssortment = [["tower", "wallLong"], ["tower", "wall"]]; + let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]]; defaultWallPartsAssortment.push(centeredWallPart); - for (let i = 0; i < extandingWallPartAssortment.length; ++i) + for (let assortment of extendingWallPartAssortment) { let wallPart = centeredWallPart; for (let j = 0; j < radius; ++j) { - if (j%2 == 0) - wallPart = wallPart.concat(extandingWallPartAssortment[i]); + if (j % 2 == 0) + wallPart = wallPart.concat(assortment); else { - extandingWallPartAssortment[i].reverse(); - wallPart = extandingWallPartAssortment[i].concat(wallPart); - extandingWallPartAssortment[i].reverse(); + assortment.reverse(); + wallPart = assortment.concat(wallPart); + assortment.reverse(); } defaultWallPartsAssortment.push(wallPart); } } // Setup optional arguments to the default wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment; - cornerWallElement = cornerWallElement || "tower"; // Don't use wide elements for this. Not supported well... - style = style || "palisades"; - playerId = playerId || 0; - orientation = orientation || 0; - numCorners = numCorners || randIntInclusive(5, 7); - irregularity = irregularity || 0.5; - skipFirstWall = skipFirstWall || false; // Setup angles - let angleToCover = 2*PI; + 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))); + angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity))); angleToCover -= angleAddList[angleAddList.length - 1]; } + // Setup corners let corners = []; - let angleActual = orientation - angleAddList[0]/2; + let angleActual = orientation - angleAddList[0] / 2; for (let i = 0; i < numCorners; ++i) { - corners.push([centerX + radius*cos(angleActual), centerY + radius*sin(angleActual)]); + corners.push([ + centerX + radius * Math.cos(angleActual), + centerY + radius * Math.sin(angleActual) + ]); if (i < numCorners - 1) - angleActual += angleAddList[i+1]; + angleActual += angleAddList[i + 1]; } + // Setup best wall parts for the different walls (a bit confusing naming...) let wallPartLengths = []; let maxWallPartLength = 0; - for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex) + for (let wallPart of wallPartsAssortment) { - let length = wallPartLengths[partIndex]; - wallPartLengths.push(getWallLength(wallPartsAssortment[partIndex], style)); + 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 simpel wall part not a wallPartsAssortment! - let bestWallLength = 99999999; - // NOTE: This is not exactly like the length the wall will be in the end. Has to be tweaked... - let wallLength = Math.euclidDistance2D(corners[i][0], corners[i][1], corners[(i + 1) % numCorners][0], corners[(i + 1) % numCorners][1]); - let numWallParts = ceil(wallLength/maxWallPartLength); + 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 = Math.euclidDistance2D(corners[i][0], corners[i][1], corners[targetCorner][0], corners[targetCorner][1]); + let numWallParts = Math.ceil(wallLength / maxWallPartLength); for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex) { - let linearWallLength = numWallParts*wallPartLengths[partIndex]; + let linearWallLength = numWallParts * wallPartLengths[partIndex]; if (linearWallLength < bestWallLength && linearWallLength > wallLength) { bestWallPart = wallPartsAssortment[partIndex]; bestWallLength = linearWallLength; } } wallPartList.push(bestWallPart); } + // Place Corners and walls for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i][0], corners[i][1], centerX, centerY); - placeObject(corners[i][0], corners[i][1], wallStyles[style][cornerWallElement].entity, playerId, angleToCorner); + placeObject(corners[i][0], corners[i][1], getWallElement(cornerWallElement, style).templateName, playerId, 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; placeLinearWall( // Adjustment to the corner element width (approximately) - corners[i][0] + wallStyles[style][cornerWallElement].width/2 * sin(angleToCorner + angleAddList[i]/2), // startX - corners[i][1] - wallStyles[style][cornerWallElement].width/2 * cos(angleToCorner + angleAddList[i]/2), // startY - corners[(i+1)%numCorners][0] - wallStyles[style][cornerWallElement].width/2 * sin(angleToCorner + angleAddList[(i+1)%numCorners]/2), // targetX - corners[(i+1)%numCorners][1] + wallStyles[style][cornerWallElement].width/2 * cos(angleToCorner + angleAddList[(i+1)%numCorners]/2), // targetY + corners[i][0] + cornerLength * Math.sin(startAngle), // startX + corners[i][1] - cornerLength * Math.cos(startAngle), // startY + corners[targetCorner][0] - cornerLength * Math.sin(targetAngle), // targetX + corners[targetCorner][1] + cornerLength * Math.cos(targetAngle), // targetY wallPartList[i], style, playerId, false); + } } } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// placeGenericFortress -// -// Places a generic fortress with towers at the edges connected with long walls and gates (entries until gates work) -// This is the default Iberian civ bonus starting wall -// -// centerX/Y The approximate center coordinates of the fortress -// radius The approximate radius of the wall to be placed -// playerId Optional. Integer number of the player. Default is 0 (gaia) -// style Optional. Wall style string. Default is the civ of the given player, "palisades" for gaia -// irregularity Optional. Float between 0 (circle) and 1 (very spiky), default is 1/2 -// gateOccurence Optional. Integer number, every n-th walls will be a gate instead. Default is 3 -// maxTrys Optional. How often the function tries to find a better fitting shape at max. Default is 100 -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function placeGenericFortress(centerX, centerY, radius, playerId, style, irregularity, gateOccurence, maxTrys) +/** + * 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 {number} centerX - The approximate center coordinates of the fortress + * @param {number} centerY - 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} [maxTrys] - How often the function tries to find a better fitting shape. + */ +function placeGenericFortress(centerX, centerY, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTrys = 100) { - // Setup optional arguments - radius = radius || 20; - playerId = playerId || 0; - if (!wallStyles.hasOwnProperty(style)) - { - if (playerId == 0) - style = style || "palisades"; - else - style = getCivCode(playerId); - } - irregularity = irregularity || 1/2; - gateOccurence = gateOccurence || 3; - maxTrys = maxTrys || 100; + style = validateStyle(style, playerId); // Setup some vars - let startAngle = randFloat(0, 2*PI); - let actualOffX = radius*cos(startAngle); - let actualOffY = radius*sin(startAngle); + let startAngle = randFloat(0, Math.PI * 2); + let actualOffX = radius * Math.cos(startAngle); + let actualOffY = radius * Math.sin(startAngle); let actualAngle = startAngle; - let pointDistance = wallStyles[style].wallLong.width + wallStyles[style].tower.width; + let pointDistance = getWallLength(style, ["long", "tower"]); + // Searching for a well fitting point derivation let tries = 0; - let bestPointDerivation = undefined; + let bestPointDerivation; let minOverlap = 1000; - let overlap = undefined; - while (tries < maxTrys && minOverlap > wallStyles[style].tower.width / 10) + let overlap; + while (tries < maxTrys && minOverlap > g_WallStyles[style].overlap) { let pointDerivation = []; let distanceToTarget = 1000; let targetReached = false; while (!targetReached) { - let indent = randFloat(-irregularity*pointDistance, irregularity*pointDistance); + let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance); let tmpAngle = getAngle(actualOffX, actualOffY, - (radius + indent)*cos(actualAngle + (pointDistance / radius)), - (radius + indent)*sin(actualAngle + (pointDistance / radius))); - actualOffX += pointDistance*cos(tmpAngle); - actualOffY += pointDistance*sin(tmpAngle); + (radius + indent) * Math.cos(actualAngle + pointDistance / radius), + (radius + indent) * Math.sin(actualAngle + pointDistance / radius) + ); + actualOffX += pointDistance * Math.cos(tmpAngle); + actualOffY += pointDistance * Math.sin(tmpAngle); actualAngle = getAngle(0, 0, actualOffX, actualOffY); pointDerivation.push([actualOffX, actualOffY]); distanceToTarget = Math.euclidDistance2D(actualOffX, actualOffY, ...pointDerivation[0]); let numPoints = pointDerivation.length; if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better... { targetReached = true; overlap = pointDistance - Math.euclidDistance2D(...pointDerivation[numPoints - 1], ...pointDerivation[0]); if (overlap < minOverlap) { minOverlap = overlap; bestPointDerivation = pointDerivation; } } } - +tries; + ++tries; } log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries"); + // Place wall for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex) { let startX = centerX + bestPointDerivation[pointIndex][0]; let startY = centerY + bestPointDerivation[pointIndex][1]; let targetX = centerX + bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length][0]; let targetY = centerY + bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length][1]; let angle = getAngle(startX, startY, targetX, targetY); - let wallElement = "wallLong"; - if ((pointIndex + 1) % gateOccurence == 0) - wallElement = "gate"; - let entity = wallStyles[style][wallElement].entity; - if (entity) + + let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long"; + element = getWallElement(element, style); + if (element.templateName) + { + let dist = Math.euclidDistance2D(startX, startY, targetX, targetY) / 2; placeObject( - startX + (Math.euclidDistance2D(startX, startY, targetX, targetY) / 2) * Math.cos(angle), - startY + (Math.euclidDistance2D(startX, startY, targetX, targetY) / 2) * Math.sin(angle), - entity, - playerId, - angle - Math.PI / 2 + wallStyles[style][wallElement].angle); + startX + dist * Math.cos(angle), // placeX + startY + dist * Math.sin(angle), // placeY + element.templateName, playerId, angle - Math.PI / 2 + element.angle + ); + } // Place tower startX = centerX + bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length][0]; startY = centerY + bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length][1]; angle = getAngle(startX, startY, targetX, targetY); + + let tower = getWallElement("tower", style); placeObject( centerX + bestPointDerivation[pointIndex][0], centerY + bestPointDerivation[pointIndex][1], - wallStyles[style].tower.entity, - playerId, - angle - PI/2 + wallStyles[style].tower.angle); + tower.templateName, playerId, angle - Math.PI / 2 + tower.angle + ); } } Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20625) @@ -1,2038 +1,2041 @@ function GuiInterface() {} GuiInterface.prototype.Schema = ""; GuiInterface.prototype.Serialize = function() { // This component isn't network-synchronised for the biggest part // So most of the attributes shouldn't be serialized // Return an object with a small selection of deterministic data return { "timeNotifications": this.timeNotifications, "timeNotificationID": this.timeNotificationID }; }; GuiInterface.prototype.Deserialize = function(data) { this.Init(); this.timeNotifications = data.timeNotifications; this.timeNotificationID = data.timeNotificationID; }; GuiInterface.prototype.Init = function() { this.placementEntity = undefined; // = undefined or [templateName, entityID] this.placementWallEntities = undefined; this.placementWallLastAngle = 0; this.notifications = []; this.renamedEntities = []; this.miragedEntities = []; this.timeNotificationID = 1; this.timeNotifications = []; this.entsRallyPointsDisplayed = []; this.entsWithAuraAndStatusBars = new Set(); this.enabledVisualRangeOverlayTypes = {}; }; /* * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) * from GUI scripts, and executed here with arguments (player, arg). * * CAUTION: The input to the functions in this module is not network-synchronised, so it * mustn't affect the simulation state (i.e. the data that is serialised and can affect * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. */ /** * Returns global information about the current game state. * This is used by the GUI and also by AI scripts. */ GuiInterface.prototype.GetSimulationState = function() { let ret = { "players": [] }; let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let numPlayers = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let playerEnt = cmpPlayerManager.GetPlayerByID(i); let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits); let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); // Work out what phase we are in let phase = ""; let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); if (cmpTechnologyManager) { if (cmpTechnologyManager.IsTechnologyResearched("phase_city")) phase = "city"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_town")) phase = "town"; else if (cmpTechnologyManager.IsTechnologyResearched("phase_village")) phase = "village"; } // store player ally/neutral/enemy data as arrays let allies = []; let mutualAllies = []; let neutrals = []; let enemies = []; for (let j = 0; j < numPlayers; ++j) { allies[j] = cmpPlayer.IsAlly(j); mutualAllies[j] = cmpPlayer.IsMutualAlly(j); neutrals[j] = cmpPlayer.IsNeutral(j); enemies[j] = cmpPlayer.IsEnemy(j); } ret.players.push({ "name": cmpPlayer.GetName(), "civ": cmpPlayer.GetCiv(), "color": cmpPlayer.GetColor(), "controlsAll": cmpPlayer.CanControlAllUnits(), "popCount": cmpPlayer.GetPopulationCount(), "popLimit": cmpPlayer.GetPopulationLimit(), "popMax": cmpPlayer.GetMaxPopulation(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "phase": phase, "isAlly": allies, "isMutualAlly": mutualAllies, "isNeutral": neutrals, "isEnemy": enemies, "entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null, "entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null, "entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null, "researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null, "researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i) }); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) ret.circularMap = cmpRangeManager.GetLosCircular(); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (cmpTerrain) ret.mapSize = cmpTerrain.GetMapSize(); // Add timeElapsed let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); ret.timeElapsed = cmpTimer.GetTime(); // Add ceasefire info let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (cmpCeasefireManager) { ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive(); ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0; } // Add cinema path info let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); // Add the game type and allied victory let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.gameType = cmpEndGameManager.GetGameType(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); // Add basic statistics to each player for (let i = 0; i < numPlayers; ++i) { let playerEnt = cmpPlayerManager.GetPlayerByID(i); let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics(); } return ret; }; /** * Returns global information about the current game state, plus statistics. * This is used by the GUI at the end of a game, in the summary screen. * Note: Amongst statistics, the team exploration map percentage is computed from * scratch, so the extended simulation state should not be requested too often. */ GuiInterface.prototype.GetExtendedSimulationState = function() { // Get basic simulation info let ret = this.GetSimulationState(); // Add statistics to each player let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let n = cmpPlayerManager.GetNumPlayers(); for (let i = 0; i < n; ++i) { let playerEnt = cmpPlayerManager.GetPlayerByID(i); let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } return ret; }; GuiInterface.prototype.GetRenamedEntities = function(player) { if (this.miragedEntities[player]) return this.renamedEntities.concat(this.miragedEntities[player]); else return this.renamedEntities; }; GuiInterface.prototype.ClearRenamedEntities = function() { this.renamedEntities = []; this.miragedEntities = []; }; GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage) { if (!this.miragedEntities[player]) this.miragedEntities[player] = []; this.miragedEntities[player].push({ "entity": entity, "newentity": mirage }); }; /** * Get common entity info, often used in the gui */ GuiInterface.prototype.GetEntityState = function(player, ent) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); // All units must have a template; if not then it's a nonexistent entity id let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) return null; let ret = { "id": ent, "template": template, "alertRaiser": null, "builder": null, "canGarrison": null, "identity": null, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "market": null, "mirage": null, "pack": null, "upgrade" : null, "player": -1, "position": null, "production": null, "rallyPoint": null, "resourceCarrying": null, "rotation": null, "trader": null, "unitAI": null, "visibility": null, }; let cmpMirage = Engine.QueryInterface(ent, IID_Mirage); if (cmpMirage) ret.mirage = true; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity) ret.identity = { "rank": cmpIdentity.GetRank(), "classes": cmpIdentity.GetClassesList(), "visibleClasses": cmpIdentity.GetVisibleClassesList(), "selectionGroupName": cmpIdentity.GetSelectionGroupName(), "canDelete": !cmpIdentity.IsUndeletable() }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) { ret.position = cmpPosition.GetPosition(); ret.rotation = cmpPosition.GetRotation(); } let cmpHealth = QueryMiragedInterface(ent, IID_Health); if (cmpHealth) { ret.hitpoints = cmpHealth.GetHitpoints(); ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints(); ret.needsHeal = !cmpHealth.IsUnhealable(); } let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable); if (cmpCapturable) { ret.capturePoints = cmpCapturable.GetCapturePoints(); ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints(); } let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (cmpBuilder) ret.builder = true; let cmpMarket = QueryMiragedInterface(ent, IID_Market); if (cmpMarket) ret.market = { "land": cmpMarket.HasType("land"), "naval": cmpMarket.HasType("naval"), }; let cmpPack = Engine.QueryInterface(ent, IID_Pack); if (cmpPack) ret.pack = { "packed": cmpPack.IsPacked(), "progress": cmpPack.GetProgress(), }; var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades" : cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo() }; let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "queue": cmpProductionQueue.GetQueue() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFogging = Engine.QueryInterface(ent, IID_Fogging); if (cmpFogging) ret.fogging = { "mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : null }; let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation); if (cmpFoundation) ret.foundation = { "progress": cmpFoundation.GetBuildPercentage(), "numBuilders": cmpFoundation.GetNumBuilders() }; let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() }; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) ret.player = cmpOwnership.GetOwner(); let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (cmpRallyPoint) ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); if (cmpGarrisonHolder) ret.garrisonHolder = { "entities": cmpGarrisonHolder.GetEntities(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) ret.unitAI = { "state": cmpUnitAI.GetCurrentState(), "orders": cmpUnitAI.GetOrders(), "hasWorkOrders": cmpUnitAI.HasWorkOrders(), "canGuard": cmpUnitAI.CanGuard(), "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "possibleStances": cmpUnitAI.GetPossibleStances(), "isIdle":cmpUnitAI.IsIdle(), }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); if (cmpGuard) ret.guard = { "entities": cmpGuard.GetEntities(), }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); let cmpGate = Engine.QueryInterface(ent, IID_Gate); if (cmpGate) ret.gate = { "locked": cmpGate.IsLocked(), }; let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser); if (cmpAlertRaiser) ret.alertRaiser = { "level": cmpAlertRaiser.GetLevel(), "canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(), "hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(), }; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); ret.visibility = cmpRangeManager.GetLosVisibility(ent, player); return ret; }; /** * Get additionnal entity info, rarely used in the gui */ GuiInterface.prototype.GetExtendedEntityState = function(player, ent) { let ret = { "armour": null, "attack": null, "buildingAI": null, "deathDamage": null, "heal": null, "isBarterMarket": null, "loot": null, "obstruction": null, "turretParent":null, "promotion": null, "repairRate": null, "buildRate": null, "buildTime": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, "resourceTrickle": null, "speed": null, }; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpAttack = Engine.QueryInterface(ent, IID_Attack); if (cmpAttack) { let types = cmpAttack.GetAttackTypes(); if (types.length) ret.attack = {}; for (let type of types) { ret.attack[type] = cmpAttack.GetAttackStrengths(type); ret.attack[type].splash = cmpAttack.GetSplashDamage(type); let range = cmpAttack.GetRange(type); ret.attack[type].minRange = range.min; ret.attack[type].maxRange = range.max; let timers = cmpAttack.GetTimers(type); ret.attack[type].prepareTime = timers.prepare; ret.attack[type].repeatTime = timers.repeat; if (type != "Ranged") { // not a ranged attack, set some defaults ret.attack[type].elevationBonus = 0; ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; continue; } ret.attack[type].elevationBonus = range.elevationBonus; let cmpPosition = Engine.QueryInterface(ent, IID_Position); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld()) { // For units, take the range in front of it, no spread. So angle = 0 ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0); } else if(cmpPosition && cmpPosition.IsInWorld()) { // For buildings, take the average elevation around it. So angle = 2*pi ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI); } else { // not in world, set a default? ret.attack[type].elevationAdaptedRange = ret.attack.maxRange; } } } let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); if (cmpArmour) ret.armour = cmpArmour.GetArmourStrengths(); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (cmpAuras) ret.auras = cmpAuras.GetDescriptions(); let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI); if (cmpBuildingAI) ret.buildingAI = { "defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage); if (cmpDeathDamage) ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths(); let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (cmpObstruction) ret.obstruction = { "controlGroup": cmpObstruction.GetControlGroup(), "controlGroup2": cmpObstruction.GetControlGroup2(), }; let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY) ret.turretParent = cmpPosition.GetTurretParent(); let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable); if (cmpRepairable) ret.repairRate = cmpRepairable.GetRepairRate(); let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); if (cmpFoundation) { ret.buildRate = cmpFoundation.GetBuildRate(); ret.buildTime = cmpFoundation.GetBuildTime(); } let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply); if (cmpResourceSupply) ret.resourceSupply = { "isInfinite": cmpResourceSupply.IsInfinite(), "max": cmpResourceSupply.GetMaxAmount(), "amount": cmpResourceSupply.GetCurrentAmount(), "type": cmpResourceSupply.GetType(), "killBeforeGather": cmpResourceSupply.GetKillBeforeGather(), "maxGatherers": cmpResourceSupply.GetMaxGatherers(), "numGatherers": cmpResourceSupply.GetNumGatherers() }; let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); if (cmpResourceGatherer) ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); if (cmpResourceDropsite) ret.resourceDropsite = { "types": cmpResourceDropsite.GetTypes(), "sharable": cmpResourceDropsite.IsSharable(), "shared": cmpResourceDropsite.IsShared() }; let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); if (cmpPromotion) ret.promotion = { "curr": cmpPromotion.GetCurrentXp(), "req": cmpPromotion.GetRequiredXp() }; if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "hp": cmpHeal.GetHP(), "range": cmpHeal.GetRange().max, "rate": cmpHeal.GetRate(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { let resources = cmpLoot.GetResources(); ret.loot = { "xp": cmpLoot.GetXp() }; for (let res of Resources.GetCodes()) ret.loot[res] = resources[res]; } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) { ret.resourceTrickle = { "interval": cmpResourceTrickle.GetTimer(), "rates": {} }; let rates = cmpResourceTrickle.GetRates(); for (let res in rates) ret.resourceTrickle.rates[res] = rates[res]; } let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetRunSpeed() }; return ret; }; GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); let rot = { "x": 0, "y": 0, "z": 0 }; let pos = { "x": cmd.x, "y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z), "z": cmd.z }; let elevationBonus = cmd.elevationBonus || 0; let range = cmd.range; return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI); }; GuiInterface.prototype.GetTemplateData = function(player, name) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(name); if (!template) return null; let aurasTemplate = {}; if (!template.Auras) return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes); // Add aura name and description loaded from JSON file let auraNames = template.Auras._string.split(/\s+/); let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); for (let name of auraNames) aurasTemplate[name] = cmpDataTemplateManager.GetAuraTemplate(name); return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes); }; GuiInterface.prototype.GetTechnologyData = function(player, data) { let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); let template = cmpDataTemplateManager.GetTechnologyTemplate(data.name); if (!template) { warn("Tried to get data for invalid technology: " + data.name); return null; } return GetTechnologyDataHelper(template, data.civ, Resources); }; GuiInterface.prototype.IsTechnologyResearched = function(player, data) { if (!data.tech) return true; let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.IsTechnologyResearched(data.tech); }; // Checks whether the requirements for this technology have been met GuiInterface.prototype.CheckTechnologyRequirements = function(player, data) { let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager); if (!cmpTechnologyManager) return false; return cmpTechnologyManager.CanResearch(data.tech); }; // Returns technologies that are being actively researched, along with // which entity is researching them and how far along the research is. GuiInterface.prototype.GetStartedResearch = function(player) { let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return {}; let ret = {}; for (let tech of cmpTechnologyManager.GetStartedTechs()) { ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) }; let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue); if (cmpProductionQueue) ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress; else ret[tech].progress = 0; } return ret; }; // Returns the battle state of the player. GuiInterface.prototype.GetBattleState = function(player) { let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection); if (!cmpBattleDetection) return false; return cmpBattleDetection.GetState(); }; // Returns a list of ongoing attacks against the player. GuiInterface.prototype.GetIncomingAttacks = function(player) { return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks(); }; // Used to show a red square over GUI elements you can't yet afford. GuiInterface.prototype.GetNeededResources = function(player, data) { return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost); }; /** * Add a timed notification. * Warning: timed notifacations are serialised * (to also display them on saved games or after a rejoin) * so they should allways be added and deleted in a deterministic way. */ GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); notification.endTime = duration + cmpTimer.GetTime(); notification.id = ++this.timeNotificationID; // Let all players and observers receive the notification by default if (!notification.players) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } this.timeNotifications.push(notification); this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime); cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID); return this.timeNotificationID; }; GuiInterface.prototype.DeleteTimeNotification = function(notificationID) { this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID); }; GuiInterface.prototype.GetTimeNotifications = function(player) { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); // filter on players and time, since the delete timer might be executed with a delay return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time); }; GuiInterface.prototype.PushNotification = function(notification) { if (!notification.type || notification.type == "text") this.AddTimeNotification(notification); else this.notifications.push(notification); }; GuiInterface.prototype.GetNotifications = function() { let n = this.notifications; this.notifications = []; return n; }; GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer) { return QueryPlayerIDInterface(wantedPlayer).GetFormations(); }; GuiInterface.prototype.GetFormationRequirements = function(player, data) { return GetFormationRequirements(data.formationTemplate); }; GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) { return CanMoveEntsIntoFormation(data.ents, data.formationTemplate); }; GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data) { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTemplateManager.GetTemplate(data.templateName); if (!template || !template.Formation) return {}; return { "name": template.Formation.FormationName, "tooltip": template.Formation.DisabledTooltip || "", "icon": template.Formation.Icon }; }; GuiInterface.prototype.IsFormationSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); // GetLastFormationName is named in a strange way as it (also) is // the value of the current formation (see Formation.js LoadFormation) if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate) return true; } return false; }; GuiInterface.prototype.IsStanceSelected = function(player, data) { for (let ent of data.ents) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance) return true; } return false; }; GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; for (let ent of cmd.entities) { let cmpBuilder = Engine.QueryInterface(ent, IID_Builder); if (!cmpBuilder) continue; for (let building of cmpBuilder.GetEntitiesList()) if (buildableEnts.indexOf(building) == -1) buildableEnts.push(building); } return buildableEnts; }; GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) { let playerColors = {}; // cache of owner -> color map for (let ent of cmd.entities) { let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); if (!cmpSelectable) continue; // Find the entity's owner's color: let owner = -1; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) owner = cmpOwnership.GetOwner(); let color = playerColors[owner]; if (!color) { color = { "r":1, "g":1, "b":1 }; let cmpPlayer = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization); if (!cmpRangeVisualization || player != owner && player != -1) continue; cmpRangeVisualization.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return [...this.entsWithAuraAndStatusBars]; }; GuiInterface.prototype.SetStatusBars = function(player, cmd) { let affectedEnts = new Set(); for (let ent of cmd.entities) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (!cmpStatusBars) continue; cmpStatusBars.SetEnabled(cmd.enabled); let cmpAuras = Engine.QueryInterface(ent, IID_Auras); if (!cmpAuras) continue; for (let name of cmpAuras.GetAuraNames()) { if (!cmpAuras.GetOverlayIcon(name)) continue; for (let e of cmpAuras.GetAffectedEntities(name)) affectedEnts.add(e); if (cmd.enabled) this.entsWithAuraAndStatusBars.add(ent); else this.entsWithAuraAndStatusBars.delete(ent); } } for (let ent of affectedEnts) { let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); if (cmpStatusBars) cmpStatusBars.RegenerateSprites(); } }; GuiInterface.prototype.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization); if (cmpRangeVisualization) cmpRangeVisualization.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; GuiInterface.prototype.GetPlayerEntities = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player); }; GuiInterface.prototype.GetNonGaiaEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; /** * Displays the rally points of a given list of entities (carried in cmd.entities). * * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should * be rendered, in order to support instantaneously rendering a rally point marker at a specified location * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the * RallyPoint component. */ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) { let cmpPlayer = QueryPlayerIDInterface(player); // If there are some rally points already displayed, first hide them for (let ent of this.entsRallyPointsDisplayed) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (cmpRallyPointRenderer) cmpRallyPointRenderer.SetDisplayed(false); } this.entsRallyPointsDisplayed = []; // Show the rally points for the passed entities for (let ent of cmd.entities) { let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); if (!cmpRallyPointRenderer) continue; // entity must have a rally point component to display a rally point marker // (regardless of whether cmd specifies a custom location) let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); if (!cmpRallyPoint) continue; // Verify the owner let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) if (!cmpOwnership || cmpOwnership.GetOwner() != player) continue; // If the command was passed an explicit position, use that and // override the real rally point position; otherwise use the real position let pos; if (cmd.x && cmd.z) pos = cmd; else pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set if (pos) { // Only update the position if we changed it (cmd.queued is set) if ("queued" in cmd) if (cmd.queued == true) cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z else cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z // rebuild the renderer when not set (when reading saved game or in case of building update) else if (!cmpRallyPointRenderer.IsSet()) for (let posi of cmpRallyPoint.GetPositions()) cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z }); cmpRallyPointRenderer.SetDisplayed(true); // remember which entities have their rally points displayed so we can hide them again this.entsRallyPointsDisplayed.push(ent); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * Display the building placement preview. * cmd.template is the name of the entity template, or "" to disable the preview. * cmd.x, cmd.z, cmd.angle give the location. * * Returns result object from CheckPlacement: * { * "success": true iff the placement is valid, else false * "message": message to display in UI for invalid placement, else "" * "parameters": parameters to use in the message * "translateMessage": localisation info * "translateParameters": localisation info * "pluralMessage": we might return a plural translation instead (optional) * "pluralCount": localisation info (optional) * } */ GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) { let result = { "success": false, "message": "", "parameters": {}, "translateMessage": false, "translateParameters": [], }; // See if we're changing template if (!this.placementEntity || this.placementEntity[0] != cmd.template) { // Destroy the old preview if there was one if (this.placementEntity) Engine.DestroyEntity(this.placementEntity[1]); // Load the new template if (cmd.template == "") this.placementEntity = undefined; else this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; } if (this.placementEntity) { let ent = this.placementEntity[1]; // Move the preview into the right location let pos = Engine.QueryInterface(ent, IID_Position); if (pos) { pos.JumpTo(cmd.x, cmd.z); pos.SetYRotation(cmd.angle); } let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether building placement is valid let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) error("cmpBuildRestrictions not defined"); else result = cmpBuildRestrictions.CheckPlacement(); let cmpRangeVisualization = Engine.QueryInterface(ent, IID_RangeVisualization); if (cmpRangeVisualization) cmpRangeVisualization.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // Set it to a red shade if this is an invalid location let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (cmd.actorSeed !== undefined) cmpVisual.SetActorSeed(cmd.actorSeed); if (!result.success) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } } return result; }; /** * Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not * specified. Returns an object with information about the list of entities that need to be newly constructed to complete * at least a part of the wall, or false if there are entities required to build at least part of the wall but none of * them can be validly constructed. * * It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one * another depending on things like snapping and whether some of the entities inside them can be validly positioned. * We have: * - The list of entities that previews the wall. This list is usually equal to the entities required to construct the * entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities * to preview the completed tower on top of its foundation. * * - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether * any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing * towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we * snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly * constructed. * * - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same * as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens * e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly * constructed but come after said first invalid entity are also truncated away. * * With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there * were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in * case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset * argument (see below). Otherwise, it will return an object with the following information: * * result: { * 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers. * 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this * can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side * but the wall construction was truncated before we could reach it, it won't be set here. Currently only * supports towers. * 'pieces': Array with the following data for each of the entities in the third list: * [{ * 'template': Template name of the entity. * 'x': X coordinate of the entity's position. * 'z': Z coordinate of the entity's position. * 'angle': Rotation around the Y axis of the entity (in radians). * }, * ...] * 'cost': { The total cost required for constructing all the pieces as listed above. * 'food': ..., * 'wood': ..., * 'stone': ..., * 'metal': ..., * 'population': ..., * 'populationBonus': ..., * } * } * * @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview. * @param cmd.start Starting point of the wall segment being created. * @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only * the starting point of the wall is available at this time (e.g. while the player is still in the process * of picking a starting point), and that therefore only the first entity in the wall (a tower) should be * previewed. * @param cmd.snapEntities List of candidate entities to snap the start and ending positions to. */ GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd) { let wallSet = cmd.wallSet; let start = { "pos": cmd.start, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; let end = { "pos": cmd.end, "angle": 0, "snapped": false, // did the start position snap to anything? "snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID }; // -------------------------------------------------------------------------------- // do some entity cache management and check for snapping if (!this.placementWallEntities) this.placementWallEntities = {}; if (!wallSet) { // we're clearing the preview, clear the entity cache and bail for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) Engine.DestroyEntity(ent); this.placementWallEntities[tpl].numUsed = 0; this.placementWallEntities[tpl].entities = []; // keep template data around } return false; } else { // Move all existing cached entities outside of the world and reset their use count for (let tpl in this.placementWallEntities) { for (let ent of this.placementWallEntities[tpl].entities) { let pos = Engine.QueryInterface(ent, IID_Position); if (pos) pos.MoveOutOfWorld(); } this.placementWallEntities[tpl].numUsed = 0; } // Create cache entries for templates we haven't seen before for (let type in wallSet.templates) { + if (type == "curves") + continue; + let tpl = wallSet.templates[type]; if (!(tpl in this.placementWallEntities)) { this.placementWallEntities[tpl] = { "numUsed": 0, "entities": [], "templateData": this.GetTemplateData(player, tpl), }; // ensure that the loaded template data contains a wallPiece component if (!this.placementWallEntities[tpl].templateData.wallPiece) { error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'"); return false; } } } } // prevent division by zero errors further on if the start and end positions are the same if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z)) end.pos = undefined; // See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list // of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping // data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData). if (cmd.snapEntities) { let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error let startSnapData = this.GetFoundationSnapData(player, { "x": start.pos.x, "z": start.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (startSnapData) { start.pos.x = startSnapData.x; start.pos.z = startSnapData.z; start.angle = startSnapData.angle; start.snapped = true; if (startSnapData.ent) start.snappedEnt = startSnapData.ent; } if (end.pos) { let endSnapData = this.GetFoundationSnapData(player, { "x": end.pos.x, "z": end.pos.z, "template": wallSet.templates.tower, "snapEntities": cmd.snapEntities, "snapRadius": snapRadius, }); if (endSnapData) { end.pos.x = endSnapData.x; end.pos.z = endSnapData.z; end.angle = endSnapData.angle; end.snapped = true; if (endSnapData.ent) end.snappedEnt = endSnapData.ent; } } } // clear the single-building preview entity (we'll be rolling our own) this.SetBuildingPlacementPreview(player, { "template": "" }); // -------------------------------------------------------------------------------- // calculate wall placement and position preview entities let result = { "pieces": [], "cost": { "population": 0, "populationBonus": 0, "time": 0 }, }; for (let res of Resources.GetCodes()) result.cost[res] = 0; let previewEntities = []; if (end.pos) previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js // For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would // otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of // an issue, because all preview entities have their obstruction components deactivated, meaning that their // obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview // entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces. // Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION // flag set), which is what we want. The only exception to this is when snapping to existing towers (or // foundations thereof); the wall segments that connect up to these will be found to be obstructed by the // existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this, // we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so // that they are free from mutual obstruction (per definition of obstruction control groups). This is done by // assigning them an extra "controlGroup" field, which we'll then set during the placement loop below. // Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully // constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed // by the foundation it snaps to. if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) { let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction); if (previewEntities.length > 0 && startEntObstruction) previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()]; // if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group let startEntState = this.GetEntityState(player, start.snappedEnt); if (startEntState.foundation) { let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position); if (cmpPosition) previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true, // preview only, must not appear in the result }); } } else { // Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps // when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned // wall piece. // To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the // build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece // foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list // of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate // the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and // onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates, // which this time does include the new foundations; so we snap to the entity, and rotate the preview back to // the foundation's angle. // The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until // the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice. previewEntities.unshift({ "template": wallSet.templates.tower, "pos": start.pos, "angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle) }); } if (end.pos) { // Analogous to the starting side case above if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY) { let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction); // Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the // same wall piece snapping to both a starting and an ending tower. And it might be more common than you would // expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with // the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single // '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time). if (previewEntities.length > 0 && endEntObstruction) { previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []); previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup()); } // if we're snapping to a foundation, add an extra preview tower and also set it to the same control group let endEntState = this.GetEntityState(player, end.snappedEnt); if (endEntState.foundation) { let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position); if (cmpPosition) previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": cmpPosition.GetRotation().y, "controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)], "excludeFromResult": true }); } } else previewEntities.push({ "template": wallSet.templates.tower, "pos": end.pos, "angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle) }); } let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); if (!cmpTerrain) { error("[SetWallPlacementPreview] System Terrain component not found"); return false; } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (!cmpRangeManager) { error("[SetWallPlacementPreview] System RangeManager component not found"); return false; } // Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed // to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be, // but cannot validly be, constructed). See method-level documentation for more details. let allPiecesValid = true; let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity for (let i = 0; i < previewEntities.length; ++i) { let entInfo = previewEntities[i]; let ent = null; let tpl = entInfo.template; let tplData = this.placementWallEntities[tpl].templateData; let entPool = this.placementWallEntities[tpl]; if (entPool.numUsed >= entPool.entities.length) { // allocate new entity ent = Engine.AddLocalEntity("preview|" + tpl); entPool.entities.push(ent); } else // reuse an existing one ent = entPool.entities[entPool.numUsed]; if (!ent) { error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'"); continue; } // move piece to right location // TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition) { cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z); cmpPosition.SetYRotation(entInfo.angle); // if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces if (tpl === wallSet.templates.tower) { let terrainGroundPrev = null; let terrainGroundNext = null; if (i > 0) terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z); if (i < previewEntities.length - 1) terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z); if (terrainGroundPrev != null || terrainGroundNext != null) { let targetY = Math.max(terrainGroundPrev, terrainGroundNext); cmpPosition.SetHeightFixed(targetY); } } } let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction); if (!cmpObstruction) { error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component"); continue; } // Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are // more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a // first-come first-served basis; the first value in the array is always assigned as the primary control group, and // any second value as the secondary control group. // By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't // reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently // reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was // once snapped to. let primaryControlGroup = ent; let secondaryControlGroup = INVALID_ENTITY; if (entInfo.controlGroups && entInfo.controlGroups.length > 0) { if (entInfo.controlGroups.length > 2) { error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups"); break; } primaryControlGroup = entInfo.controlGroups[0]; if (entInfo.controlGroups.length > 1) secondaryControlGroup = entInfo.controlGroups[1]; } cmpObstruction.SetControlGroup(primaryControlGroup); cmpObstruction.SetControlGroup2(secondaryControlGroup); // check whether this wall piece can be validly positioned here let validPlacement = false; let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); // Check whether it's in a visible or fogged region // TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden"); if (visible) { let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); if (!cmpBuildRestrictions) { error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'"); continue; } // TODO: Handle results of CheckPlacement validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success); // If a wall piece has two control groups, it's likely a segment that spans // between two existing towers. To avoid placing a duplicate wall segment, // check for collisions with entities that share both control groups. if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1) validPlacement = cmpObstruction.CheckDuplicateFoundation(); } allPiecesValid = allPiecesValid && validPlacement; // The requirement below that all pieces so far have to have valid positions, rather than only this single one, // ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible // for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall // through and past an existing building). // Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed // on top of foundations of incompleted towers that we snapped to; they must not be part of the result. if (!entInfo.excludeFromResult) ++numRequiredPieces; if (allPiecesValid && !entInfo.excludeFromResult) { result.pieces.push({ "template": tpl, "x": entInfo.pos.x, "z": entInfo.pos.z, "angle": entInfo.angle, }); this.placementWallLastAngle = entInfo.angle; // grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components // copied over, so we need to fetch it from the template instead). // TODO: we should really use a Cost object or at least some utility functions for this, this is mindless // boilerplate that's probably duplicated in tons of places. for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"])) result.cost[res] += tplData.cost[res]; } let canAfford = true; let cmpPlayer = QueryPlayerIDInterface(player, IID_Player); if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost)) canAfford = false; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual) { if (!allPiecesValid || !canAfford) cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1); else cmpVisual.SetShadingColor(1, 1, 1, 1); } ++entPool.numUsed; } // If any were entities required to build the wall, but none of them could be validly positioned, return failure // (see method-level documentation). if (numRequiredPieces > 0 && result.pieces.length == 0) return false; if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY) result.startSnappedEnt = start.snappedEnt; // We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed, // i.e. are included in result.pieces (see docs for the result object). if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid) result.endSnappedEnt = end.snappedEnt; return result; }; /** * Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap * it to (if necessary/useful). * * @param data.x The X position of the foundation to snap. * @param data.z The Z position of the foundation to snap. * @param data.template The template to get the foundation snapping data for. * @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius * around the entity. Only takes effect when used in conjunction with data.snapRadius. * When this option is used and the foundation is found to snap to one of the entities passed in this list * (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent", * holding the ID of the entity that was snapped to. * @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that * {data.x, data.z} must be located within to have it snap to that entity. */ GuiInterface.prototype.GetFoundationSnapData = function(player, data) { let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template); if (!template) { warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'"); return false; } if (data.snapEntities && data.snapRadius && data.snapRadius > 0) { // see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest // (TODO: break unlikely ties by choosing the lowest entity ID) let minDist2 = -1; let minDistEntitySnapData = null; let radius2 = data.snapRadius * data.snapRadius; for (let ent of data.snapEntities) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z); if (dist2 > radius2) continue; if (minDist2 < 0 || dist2 < minDist2) { minDist2 = dist2; minDistEntitySnapData = { "x": pos.x, "z": pos.z, "angle": cmpPosition.GetRotation().y, "ent": ent }; } } if (minDistEntitySnapData != null) return minDistEntitySnapData; } if (template.BuildRestrictions.PlacementType == "shore") { let angle = GetDockAngle(template, data.x, data.z); if (angle !== undefined) return { "x": data.x, "z": data.z, "angle": angle }; } return false; }; GuiInterface.prototype.PlaySound = function(player, data) { if (!data.entity) return; PlaySound(data.name, data.entity); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. var bucket = filtered.bucket; if(bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if(!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; GuiInterface.prototype.GetTradingRouteGain = function(player, data) { if (!data.firstMarket || !data.secondMarket) return null; return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template); }; GuiInterface.prototype.GetTradingDetails = function(player, data) { let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader); if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target)) return null; let firstMarket = cmpEntityTrader.GetFirstMarket(); let secondMarket = cmpEntityTrader.GetSecondMarket(); let result = null; if (data.target === firstMarket) { result = { "type": "is first", "hasBothMarkets": cmpEntityTrader.HasBothMarkets() }; if (cmpEntityTrader.HasBothMarkets()) result.gain = cmpEntityTrader.GetGoods().amount; } else if (data.target === secondMarket) { result = { "type": "is second", "gain": cmpEntityTrader.GetGoods().amount, }; } else if (!firstMarket) { result = { "type": "set first" }; } else if (!secondMarket) { result = { "type": "set second", "gain": cmpEntityTrader.CalculateGain(firstMarket, data.target), }; } else { // Else both markets are not null and target is different from them result = { "type": "set first" }; } return result; }; GuiInterface.prototype.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * Returns batch build time. */ GuiInterface.prototype.GetBatchTime = function(player, data) { let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue); if (!cmpProductionQueue) return 0; return cmpProductionQueue.GetBatchTime(data.batchSize); }; GuiInterface.prototype.IsMapRevealed = function(player) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player); }; GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled); }; GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) { for (let ent of data.entities) { let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) cmpUnitMotion.SetDebugOverlay(data.enabled); } }; GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) { Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled); }; GuiInterface.prototype.GetTraderNumber = function(player) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of traders) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpIdentity || !cmpUnitAI) continue; if (cmpIdentity.HasClass("Ship")) { ++shipTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++shipTrader.trading; } else { ++landTrader.total; if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade") ++landTrader.trading; if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison") { let holder = cmpUnitAI.order.data.target; let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI); if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade") ++landTrader.garrisoned; } } } return { "landTrader": landTrader, "shipTrader": shipTrader }; }; GuiInterface.prototype.GetTradingGoods = function(player) { return QueryPlayerIDInterface(player).GetTradingGoods(); }; GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) { this.renamedEntities.push(msg); }; // List the GuiInterface functions that can be safely called by GUI scripts. // (GUI scripts are non-deterministic and untrusted, so these functions must be // appropriately careful. They are called with a first argument "player", which is // trusted and indicates the player associated with the current client; no data should // be returned unless this player is meant to be able to see it.) let exposedFunctions = { "GetSimulationState": 1, "GetExtendedSimulationState": 1, "GetRenamedEntities": 1, "ClearRenamedEntities": 1, "GetEntityState": 1, "GetExtendedEntityState": 1, "GetAverageRangeForBuildings": 1, "GetTemplateData": 1, "GetTechnologyData": 1, "IsTechnologyResearched": 1, "CheckTechnologyRequirements": 1, "GetStartedResearch": 1, "GetBattleState": 1, "GetIncomingAttacks": 1, "GetNeededResources": 1, "GetNotifications": 1, "GetTimeNotifications": 1, "GetAvailableFormations": 1, "GetFormationRequirements": 1, "CanMoveEntsIntoFormation": 1, "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, "SetSelectionHighlight": 1, "GetAllBuildableEntities": 1, "SetStatusBars": 1, "GetPlayerEntities": 1, "GetNonGaiaEntities": 1, "DisplayRallyPoint": 1, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 1, "GetTraderNumber": 1, "GetTradingGoods": 1, }; GuiInterface.prototype.ScriptCall = function(player, name, args) { if (exposedFunctions[name]) return this[name](player, args); else throw new Error("Invalid GuiInterface Call name \""+name+"\""); }; Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); Index: ps/trunk/binaries/data/mods/public/simulation/components/WallSet.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/WallSet.js (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/components/WallSet.js (revision 20625) @@ -1,40 +1,55 @@ function WallSet() {} WallSet.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + "" + "" + "0.01.0" + "" + "" + "0.01.0" + ""; WallSet.prototype.Init = function() { }; WallSet.prototype.Serialize = null; Engine.RegisterComponentType(IID_WallSet, "WallSet", WallSet); Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/athen.json (revision 20625) @@ -1,172 +1,177 @@ { "Code":"athen", "Culture":"hele", "Name":"Athenians", "Emblem":"session/portraits/emblems/emblem_athenians.png", "History":"As the cradle of Western civilization and the birthplace of democracy, Athens was famed as a center for the arts, learning and philosophy. The Athenians were also powerful warriors, particularly at sea. At its peak, Athens dominated a large part of the Hellenic world for several decades.", "Music":[ {"File":"Harvest_Festival.ogg", "Type":"peace"}, {"File":"Forging_a_City-State.ogg", "Type":"peace"}, {"File":"Highland_Mist.ogg", "Type":"peace"}, {"File":"The_Hellespont.ogg", "Type":"peace"} ], "Factions": [ { "Name":"Athenians", "Description":"A Hellenic people of the Ionian tribe.", "Technologies": [ { "Name":"Iphicratean Reforms", "History":"", "Description":"Athenian triremes can train Marines (Epibastes Athenaikos)." }, { "Name":"Long Walls", "History":"The Long Walls of Athens were constructed under the auspices of the wily Themistocles and extended 6 km from the city to the port of Piraeus. This secured the city's sea supply routes and prevented an enemy from starving out the city during a siege.", "Description":"Stone walls can be built in neutral territory. Construction time for walls is reduced by 50%." }, { "Name":"Othismos", "History":"The classical phalanx formation was developed about VIII century BC. It was eight men deep and up to eight hundred men wide. The men within overlapped their shields, presenting a formidable shield wall brimming with 8 foot spears.", "Description":"The player gains the ability to order spear-armed troops into Phalanx formation, providing greater attack and armor." } ], "Heroes": [ { "Name":"Themistocles", "Class":"", "Armament":"", "Emblem":"", "History":"The general who persuaded the Athenians to invest their income from silver mines in a war navy of 200 Triremes. A key figure during the Persian Wars, he commanded the victorious Athenian navy at the decisive battle of Salamis in 479 B.C. Later, he pursued an active policy against the Persians in the Aegean, thereby laying the foundations of future Athenian power. However, he was eventually ostracised by the Athenians and forced to flee to the protection of the Persians." }, { "Name":"Pericles", "Class":"", "Armament":"", "Emblem":"", "History":"Pericles was the foremost Athenian politician of the 5th Century B.C." }, { "Name":"Iphicrates", "Class":"", "Armament":"", "Emblem":"", "History":"." } ] } ], "CivBonuses": [ { "Name":"Silver Owls", "History":"The mines at Laureion in Attica provided Athens with a wealth of silver from which to mint her famous and highly prized coin, The Athenian Owl.", "Description":"Metal mining gathering rates increased by +10% for each passing age." }, { "Name":"Hellenization", "History":"The Greeks were highly successful in Hellenising various foreigners. During the Hellenistic Age, Greek was the lingua franca of the Ancient World, spoken widely from Spain to India.", "Description":"Constructing a Theatron increases the territory expanse of all buildings by +20%." } ], "TeamBonuses": [ { "Name":"Delian League", "History":"Shortly after the great naval victories at Salamis and Mykale, the Greek city-states instituted the so-called Delian League in 478 B.C., whose purpose was to push the Persians out of the Aegean region. The allied states contributed ships and money, while the Athenians offered their entire navy.", "Description":"Ships construct 25% faster." } ], "Structures": [ { "Name":"Theatron", "Class":"", "Emblem":"", "History":"Greek theatres were places where the immortal tragedies of Aeschylus, Sophocles and many other talented dramatists were staged to the delight of the populace. They were instrumental in enriching Hellenic culture.", "Requirements":"", "Phase":"City", "Special":"The Hellenization civ bonus. Building a Theatron increases the territory effect of all buildings by 25%." }, { "Name":"Gymnasion", "Class":"", "Emblem":"", "History":"The Gymnasion was a vital place in Hellenic cities, where physical exercises were performed and social contacts established.", "Requirements":"", "Phase":"City", "Special":"Train champion units and research technologies pertaining to champion units." }, { "Name":"Prytaneion", "Class":"", "Emblem":"", "History":"The Prytaneion is the meeting place for the city elders to dine and to make swift decisions.", "Requirements":"", "Phase":"City", "Special":"Train heroes and research technology pertaining to heroes." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/athen_wallset_stone" + ], "StartEntities": [ { "Template": "structures/athen_civil_centre" }, { "Template": "units/athen_support_female_citizen", "Count": 4 }, { "Template": "units/athen_infantry_spearman_b", "Count": 2 }, { "Template": "units/athen_infantry_slinger_b", "Count": 2 }, { "Template": "units/athen_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/phalanx" ], "AINames": [ "Themistocles", "Pericles", "Cimon", "Aristides", "Xenophon", "Hippias", "Cleisthenes", "Thucydides", "Alcibiades", "Miltiades", "Cleon", "Cleophon", "Thrasybulus", "Iphicrates", "Demosthenes" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/athen_infantry_slinger_b", "skirmish/structures/default_house_10": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/brit.json (revision 20625) @@ -1,150 +1,155 @@ { "Code": "brit", "Culture": "celt", "Name": "Britons", "Emblem": "session/portraits/emblems/emblem_britons.png", "History": "The Britons were the Celtic tribes of the British Isles. Using chariots, longswordsmen and powerful melee soldiers, they staged fearsome revolts against Rome to protect their customs and interests. Also, they built thousands of unique structures such as hill forts, crannogs and brochs.", "Music":[ {"File":"Highland_Mist.ogg", "Type":"peace"}, {"File":"Water's_Edge.ogg", "Type":"peace"}, {"File":"Celtic_Pride.ogg", "Type":"peace"}, {"File":"Cisalpine_Gaul.ogg", "Type":"peace"}, {"File":"Celtica.ogg", "Type":"peace"} ], "Factions": [ { "Name": "Britons", "Description": "The Celts of the British Isles.", "Technologies": [ { "Name": "Sevili Dusios", "History": "The Britons took up the practice of either making permanent marks on their body in the form of tattoos or temporarily painted their bodies with woad paint. The effect was very frightening.", "Description": "Increased attack and movement rate for melee soldiers." }, { "Name": "Turos Maros", "History": "'Great Tower'; Celtic legends abound with stories of massive tall towers built by the most powerful kings, and the remains of some very large towers have been found.", "Description": "Increases the height bonus of units garrisoned in a tower." } ], "Heroes": [ { "Name": "Karatakos", "Class": "", "Armament": "", "Emblem": "", "History": "Caractacus, the Roman form, is a simple change from Karatakos, his actual name, which was printed on his many, many coins. Under this name he is remembered as a fierce defender of Britain against the Romans after their invasion in 43 A.D. Son of King Cunobelin of the Catuvellauni tribal confederation, Karatakos fought for nine years against the Romans with little success, eventually fleeing to the tribes in Wales, where he was defeated decisively. Finally he entered Northern Britain, where was handed over to the Romans. Taken to Rome, Karatakos was allowed to live by the Emperor Claudius and died in Italy. Tradition states he converted to Christianity when his wife did, but there is nothing known of this as definite. Probably more notable is the matter that he was allowed to live once captured. Roman policy was typically to have such men killed in public displays to celebrate. Karatakos was brought before the Emperor and Senate at his request to explain himself. What he said is not known for certainty, but Tacitus applies to him a famous speech..." }, { "Name": "Kunobelinos", "Class": "", "Armament": "", "Emblem": "", "History": "Kunobelinos (perhaps better known by the latinized form of Cunobelin) was a powerful ruler of the Catuvellauni. He was referred to by Romans as the King of the Britons. His domains extended around the city of Kamulodunon (known as Camulodunum), modern day Colchester. The Roman defeat in the battle of Teutoburg Forest (Germania) allowed Kunobelinos to conquer a neighboring tribe, the Trinovantes, who were Roman allies and couldn't receive Roman aide. Kunobelinos seems to have been indifferent to the Romans. He traded with them freely, but had few qualms subjugating known Roman allies, and even sent his youngest son Adminius as a fosterling to be educated in Roman Gaul. This accounted for Adminius's friendships among the Romans. Adminius was given lordship over the Cantaci, who inhabited Kent, by his father. This area was the prime area of Roman influence and trade in Britain, and Kunobelinos shrewdly observed his youngest son's friendship with powerful Roman and Gallo-Roman politicians and traders would be of use administrating the region. His other sons though had no love for the Romans. When Kunobelinos died of disease, he was replaced by his son Togdumnos, who arrested, executed, or expelled numerous Roman sympathizers. These included his own brother Adminius, and the deposed Atrebates king, Verica, who appealed to their connections in the Roman Empire for aide in recovering their lands. Togdumnos died in battle with the Romans, and was subsequently replaced by his brother, Karatakos. It is an irony that it was his third son that initially invited this Roman reprisal. Kunobelinos in his own time though was possibly one of the greatest of all British kings. He conquered the great majority of the southern half of Britain (his coins were being minted as far as the borders of what would become Wales). He started ruling over only four minor tribes in a confederation, the Catuvellauni, and ended up achieving recognition as king of Britain. This recognition was so great that tribes in Cambria even came to assist his sons against the Romans and their British allies, and Kunobelinos was held up by the post-Roman Britons as one of their great heroes; a conqueror and uniter of petty kingdoms, something the post-Roman Britons or Romano-British sorely needed." }, { "Name": "Boudicca", "Class": "", "Armament": "", "Emblem": "", "History": "Ammianus Marcellinus described how difficult it would be for a band of foreigners to deal with a Celt if he called in the help of his wife. For she was stronger than he was and could rain blows and kicks upon the assailants equal in force to the shots of a catapult. Boudicca, queen of the Iceni, was said to be 'very tall and terrifying in appearance; her voice was very harsh and a great mass of red hair fell over her shoulders. She wore a tunic of many colors over which a thick cloak was fastened by a brooch. Boudicca had actually at first been a Roman ally, along with her husband, Prasutagus, king of the Iceni. Prasutagus had been a close Roman ally after a brief uprising, respected as being forethinking even by his former enemies, now allied Romans, and free to rule his kingdom as their native tradition dictated, except in one case. Prasutagus, realizing he was going to die, agreed upon a will with his wife and subordinates; his daughters would inherit the physical running of the territory, under Boudicca's stewardship until they were adults, and the Emperor of Rome would have overlordship, collecting taxes and being allowed to request military aid. Much the same situation as he already held. The problem lay in that the Romans did not recognize female heirs, and thus asserted, upon Prasutagus's death, that only the Emperor's claim to the kingdom of Icenia was valid. They further noted it was regular Roman practice to only allow a client kingdom to be independent for the lifetime of the initial king, such as had occurred in Galatia. The Empire formally annexed the kingdom, and began extracting harsh taxes immediately, citing that Prasutagus was indebted to the Romans, having taken several loans during his lifetime that he had failed to repay. Boudicca's complaint about this treatment and the defiance of her deceased husband's will was met with brutality; Roman soldiers flogged her, and her daughters, only children, were raped. Boudicca and her subjects were infuriated at the disgrace done to their queen and the children. With the Roman governor of Britain engaged with the druids in Cambria, now Wales, Boudicca was able to attract more followers from outside the Iceni, as they were hardly the only British tribe growing rapidly disillusioned with the Romans. Boudicca and her army laid waste to three cities, routed a Roman legion, and called on the memory of Arminius, a German who had routed the Romans from his lands, and their own ancestors who had driven off Caesar near a century earlier. Boudicca was defeated by a major tactical blunder in the Battle of Watling Street, leading to much of her force being slaughtered as they could not withdraw to safety. Boudicca herself escaped, and then slew her daughters, and then herself, to avoid further shame at Roman hands." } ] } ], "CivBonuses": [ { "Name": "Ardiosmanae", "History": "Represents Celtic farming methods. ", "Description": "Enhanced food gained from ranching and farming. " }, { "Name": "Deas Celtica", "History": "Celtic religion and druidry inspired their warlike mindset. ", "Description": "Druids increase attack rates of soldiers near them slightly." } ], "TeamBonuses": [ { "Name": "Druids", "History": "The Druids of the Celts maintained an organized religion that advanced the technology of their people even during wartime.", "Description": "-20% resources cost for allies healers." } ], "Structures": [ { "Name": "Kennel", "Class": "", "Emblem": "", "History": "The Britons were known for breeding war dogs.", "Requirements": "", "Phase": "", "Special": "" } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/brit_wallset_stone" + ], "StartEntities": [ { "Template": "structures/brit_civil_centre" }, { "Template": "units/brit_support_female_citizen", "Count": 4 }, { "Template": "units/brit_infantry_spearman_b", "Count": 2 }, { "Template": "units/brit_infantry_slinger_b", "Count": 2 }, { "Template": "units/brit_cavalry_javelinist_b", "Count": 1 }, { "Template": "units/brit_war_dog_e" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Karatakos", "Kunobelinos", "Boudicca", "Prasutagus", "Venutius", "Cogidubnus", "Commius", "Comux", "Adminius", "Dubnovellaunus", "Vosenius" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/brit_infantry_slinger_b", "skirmish/units/special_starting_unit": "units/brit_war_dog_b", "skirmish/structures/default_house_5": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/cart.json (revision 20625) @@ -1,175 +1,181 @@ { "Code": "cart", "Culture": "cart", "Name": "Carthaginians", "Emblem": "session/portraits/emblems/emblem_carthaginians.png", "History": "Carthage, a city-state in modern-day Tunisia, was a formidable force in the western Mediterranean, eventually taking over much of North Africa and modern-day Spain in the third century B.C. The sailors of Carthage were among the fiercest contenders on the high seas, and masters of naval trade. They deployed towered War Elephants on the battlefield to fearsome effect, and had defensive walls so strong, they were never breached.", "Music":[ {"File":"Mediterranean_Waves.ogg", "Type":"peace"}, {"File":"Harsh_Lands_Rugged_People.ogg", "Type":"peace"}, {"File":"Peaks_of_Atlas.ogg", "Type":"peace"} ], "Factions": [ { "Name": "", "Description": "", "Technologies": [ { "Name": "Exploration", "History": "Nobody knew better than the Carthaginians where in the ancient world they were going and going to go; their merchant traders had missions to everywhere.", "Description": "All Traders and Ships +25% vision range." }, { "Name": "Colonization", "History": "Carthaginians established many trading centers as colonies and ultimately held dominion over 300 cities and towns in North Africa alone.", "Description": "Civic Centers, Temples, and Houses -25% build time." } ], "Heroes": [ { "Name": "Hannibal Barca", "Class": "", "Armament": "", "Emblem": "", "History": "Carthage's most famous son. Hannibal Barca was the eldest son of Hamilcar Barca and proved an even greater commander than his father. Lived 247-182 B.C. While he ultimately lost the Second Punic War his victories at Trebia, Lake Trasimene, and Cannae, and the feat of crossing the Alps have secured his position as among the best tacticians and strategists in history." }, { "Name": "Hamilcar Barca", "Class": "", "Armament": "", "Emblem": "", "History": "Father of Hannibal and virtual military dictator. Hamilcar Barca was a soldier and politician who excelled along his entire career. Lived 275-228 B.C. While overshadowed by his sons, Hamilcar was great general in his own right, earning the nickname Baraq or Barca for the lightning speed of his advance." }, { "Name": "Maharbal", "Class": "", "Armament": "", "Emblem": "", "History": "Maharbal was Hannibal Barca's 'brash young cavalry commander' during the 2nd Punic War. He is credited with turning the wing of the legions at Cannae resulting in defeat in which 30,000 of 50,000 Romans were lost, as well as significant contributions to the winning of many other battles during the 2nd Punic War. He is known for having said, after the battle of Cannae, 'Hannibal, you know how to win the victory; just not what to do with it.'" } ] } ], "CivBonuses": [ { "Name": "Triple Walls", "History": "Carthaginians built triple city walls.", "Description": "Carthaginian walls, gates, and towers have 3x health of a standard wall, but also 2x build time." }, { "Name": "Roundup", "History": "Not unlike the Iberian Peninsula, North Africa was known as horse country, capable of producing up to 100,000 new mounts each year. It was also the home of the North African Forest Elephant.", "Description": "The resource cost of training elephant-mounted (war elephant) or horse-mounted units (cavalry) is reduced by 5% per animal corralled (as appropriate)." } ], "TeamBonuses": [ { "Name": "Trademasters", "History": "The Phoenicians and Carthaginians were broadly known as the greatest trading civilization of the ancient and classical world.", "Description": "10% bonus of trade profit for allies' international routes." } ], "Structures": [ { "Name":"Naval Shipyard", "Class":"", "Emblem":"", "History":"The structure is based upon the center island of the inner harbour constructed to house the war fleet of the Carthaginian navy at Carthage.", "Requirements":".", "Phase":"", "Special":"Construct the powerful warships of the Carthaginian navy." }, { "Name":"Celtic Embassy", "Class":"", "Emblem":"", "History":"The Celts supplied fierce warrior mercenaries for Carthaginian armies.", "Requirements":".", "Phase":"", "Special":"Hire Celtic mercenaries." }, { "Name":"Italiote Embassy", "Class":"", "Emblem":"", "History":"When Hannibal invaded Italy and defeated the Romans in a series of battles, many of the Italian peoples subject to Rome, including the Italian Greeks and powerful Samnites, revolted and joined the Carthaginian cause.", "Requirements":".", "Phase":"", "Special":"Hire Italian mercenaries." }, { "Name":"Iberian Embassy", "Class":"", "Emblem":"", "History":"The Iberians were known as fierce mercenaries, loyal to their paymasters.", "Requirements":".", "Phase":"", "Special":"Hire Iberian mercenaries." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/cart_wallset_short", + "structures/cart_wallset_stone" + ], "StartEntities": [ { "Template": "structures/cart_civil_centre" }, { "Template": "units/cart_support_female_citizen", "Count": 4 }, { "Template": "units/cart_infantry_spearman_b", "Count": 2 }, { "Template": "units/cart_infantry_archer_b", "Count": 2 }, { "Template": "units/cart_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Hannibal Barca", "Hamilcar Barca", "Hasdrubal Barca", "Hasdrubal Gisco", "Hanno the Elder", "Maharbal", "Mago Barca", "Hasdrubal the Fair", "Hanno the Great", "Himilco", "Hampsicora", "Hannibal Gisco", "Dido", "Xanthippus", "Himilco Phameas", "Hasdrubal the Boetharch" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/cart_infantry_archer_b", "skirmish/structures/default_house_10": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/gaul.json (revision 20625) @@ -1,143 +1,148 @@ { "Code": "gaul", "Culture": "celt", "Name": "Gauls", "Emblem": "session/portraits/emblems/emblem_celts.png", "History": "The Gauls were the Celtic tribes of continental Europe. Dominated by a priestly class of Druids, they featured a sophisticated culture of advanced metalworking, agriculture, trade and even road engineering. With heavy infantry and cavalry, Gallic warriors valiantly resisted Caesar's campaign of conquest and Rome's authoritarian rule.", "Music":[ {"File":"Celtic_Pride.ogg", "Type":"peace"}, {"File":"Cisalpine_Gaul.ogg", "Type":"peace"}, {"File":"Harvest_Festival.ogg", "Type":"peace"}, {"File":"Water's_Edge.ogg", "Type":"peace"}, {"File":"Sunrise.ogg", "Type":"peace"} ], "Factions": [ { "Name": "Gauls", "Description": "The Celts of mainland Europe.", "Technologies": [ { "Name": "Vae Victis", "History": "Woe to the Defeated! The Gallic chieftain Brennus seized Rome (with the exception of a garrison on Capitoline Hill). When Camillus besieged him with a relief force from Veii, he negotiated his surrender for 1000 pounds of gold, and adding the weight of his sword on the scale when the Romans complained that he used false weights he uttered those famous words.", "Description": "A set amount of metal and food from every structure destroyed or captured." }, { "Name": "Carnutes", "History": "The Carnutes were druids from Aulercia. They fought when needed, and were largely responsible for turning back the Belgae incursions into Armorica and Aulercia.", "Description": "Gallic druids gain a small melee attack." } ], "Heroes": [ { "Name": "Viridomarus", "Class": "", "Armament": "", "Emblem": "", "History": "When Celtic armies met the enemy, before the battle would start, the Celtic leader would go to the first line and challenge the bravest of the enemy warriors to a single combat. The story of how Marcus Claudius Marcellus killed a Gallic leader at Clastidium (222 B.C.) is typical of such encounters. Advancing with a small army, Marcellus met a combined force of Insubrian Gauls and Gaesatae at Clastidium. The Gallic army advanced with the usual rush and terrifying cries, and their king, Viridomarus, picking out Marcellus by means of his badges of rank, made for him, shouting a challenge and brandishing his spear. Viridomarus was an outstanding figure not only for his size but also for his adornments; for he was resplendent in bright colors and his armor shone with gold and silver. This armor, thought Marcellus, would be a fitting offering to the gods. He charged the Gaul, pierced his bright breastplate and cast him to the ground. It was an easy task to kill Viridomarus and strip him of his armor." }, { "Name": "Brennus", "Class": "", "Armament": "", "Emblem": "", "History": "Brennus is the name which the Roman historians give to the famous leader of the Gauls who took Rome in the time of Camillus. According to Geoffrey of Monmouth, the cleric who wrote “History of the Kings of Britain”, Brennus and his brother Belinus invaded Gaul and sacked Rome in 390 B.C., 'proving' that Britons had conquered Rome, the greatest civilization in the world, long before Rome conquered the Britons. We know from many ancient sources which predate Geoffrey that Rome was indeed sacked, but in 387 not 390, and that the raid was led by a man named Brennus, but he and his invading horde were Gallic Senones, not British. In this episode several features of Geoffrey's editing method can be seen: he modified the historical Brennus, created the brother Belinus, borrowed the Gallic invasion, but omitted the parts where the Celts seemed weak or foolish. His technique is both additive and subtractive. Like the tale of Trojan origin, the story of the sack of Rome is not pure fabrication; it is a creative rearrangement of the available facts, with details added as necessary. By virtue of their historical association, Beli and Bran are often muddled with the earlier brothers Belinus and Brennus (the sons of Dunvallo Molmutius) who contended for power in northern Britain in around 390 B.C., and were regarded as gods in old Celtic tradition." }, { "Name": "Vercingetorix", "Class": "", "Armament": "", "Emblem": "", "History": "Vercingetorix (Gaulish: Ver-Rix Cingetos) was the chieftain of the Arverni tribe in Gaul (modern France). Starting in 52 B.C. he led a revolt against the invading Romans under Julius Caesar, his actions during the revolt are remembered to this day. Vercingetorix was probably born near his tribes capital (Gergovia). From what little info we have Vercingetorix was probably born in 72 B.C., his father was Celtius and we don't know who his mother was. Because we only know of him from Roman sources we don't know much about Vercingetorix as a child or young man, except that perhaps he was probably very high spirited and probably gained some renown in deeds." } ] } ], "CivBonuses": [ { "Name": "Ardiosmanae", "History": "Represents Celtic farming methods. ", "Description": "Enhanced food gained from ranching and farming. " }, { "Name": "Deas Celtica", "History": "Celtic religion and druidry inspired their warlike mindset. ", "Description": "Druids increase attack rates of soldiers near them slightly." } ], "TeamBonuses": [ { "Name": "Druidic Wisdom", "History": "The Druids of the Gauls maintained an organized religion that advanced the technology of their people even during wartime.", "Description": "-10% research time for technologies of allies." } ], "Structures": [ { "Name": "Melonas", "Class": "", "Emblem": "", "History": "The Celts developed the first rotary flour mill.", "Requirements": "", "Phase": "", "Special": "" } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/gaul_wallset_stone" + ], "StartEntities": [ { "Template": "structures/gaul_civil_centre" }, { "Template": "units/gaul_support_female_citizen", "Count": 4 }, { "Template": "units/gaul_infantry_spearman_b", "Count": 2 }, { "Template": "units/gaul_infantry_javelinist_b", "Count": 2 }, { "Template": "units/gaul_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Viridomarus", "Brennus", "Cativolcus", "Cingetorix", "Vercingetorix", "Divico", "Ambiorix", "Liscus", "Valetiacus", "Viridovix" ], "SkirmishReplacements": { "skirmish/structures/default_house_5": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/iber.json (revision 20625) @@ -1,145 +1,150 @@ { "Code": "iber", "Culture": "iber", "Name": "Iberians", "Emblem": "session/portraits/emblems/emblem_iberians.png", "History": "The Iberians were a people of mysterious origins and language, with a strong tradition of horsemanship and metalworking. A relatively peaceful culture, they usually fought in other's battles only as mercenaries. However, they proved tenacious when Rome sought to take their land and freedom from them, and employed pioneering guerrilla tactics and flaming javelins as they fought back.", "Music":[ {"File":"An_old_Warhorse_goes_to_Pasture.ogg", "Type":"peace"}, {"File":"Celtica.ogg", "Type":"peace"}, {"File":"Harsh_Lands_Rugged_People.ogg", "Type":"peace"} ], "Factions": [ { "Name": "", "Description": "", "Technologies": [ { "Name": "Suzko Txabalina", "History": "Iberian tribesmen were noted for wrapping bundles of grass about the shafts of their throwing spears, soaking that in some sort of flammable pitch, then setting it afire just before throwing.", "Description": "Causes targets struck to become inflamed and lose hitpoints at a constant rate until and if either healed or repaired, as appropriate." }, { "Name": "Maisu Burdina Langileak", "History": "The Iberians were known to produce the finest iron and steel implements and weapons of the age. The famous 'Toledo Steel.'", "Description": "Metal costs for units and technologies reduced by 50%." } ], "Heroes": [ { "Name": "Viriato", "Class": "", "Armament": "", "Emblem": "", "History": "Viriato, like Vercingetorix amongst the Gauls, was the most famous of the Iberian tribal war leaders, having conducted at least 7 campaigns against the Romans in the southern half of the peninsula during the 'Lusitani Wars' from 147-139 B.C. He surfaced as a survivor of the treacherous massacre of 9,000 men and the selling into slavery of 21,000 elderly, women, and children of the Lusitani. They had signed a treaty of peace with the Romans, conducted by Servius Sulpicius Galba, governor of Hispania Ulterior, as the 'final solution' to the Lusitani problem. He emerged from humble beginnings in 151 B.C. to become war chief of the Lusitani. He was intelligent and a superior tactician, never really defeated in any encounter (though suffered losses in some requiring retreat). He succumbed instead to another treachery arranged by a later Roman commander, Q. Servilius Caepio, to have him assassinated by three comrades that were close to him." }, { "Name": "Karos", "Class": "", "Armament": "", "Emblem": "", "History": "Karos was a chief of the Belli tribe located just east of the Celtiberi (Numantines at the center). Leading the confederated tribes of the meseta central (central upland plain) he concealed 20,000 foot and 5,000 mounted troops along a densely wooded track. Q. Fulvius Nobilior neglected proper reconnaissance and lead his army into the trap strung out in a long column. Some 10,000 of 15,000 Roman legionaries fell in the massive ambush that was sprung upon them. The date was 23 August of 153 B.C., the day when Rome celebrated the feast of Vulcan. By later Senatorial Decree it was ever thereafter known as dies ater, a 'sinister day', and Rome never again fought a battle on the 23rd of August. Karos was wounded in an after battle small cavalry action the same evening and soon died thereafter, but he had carried off one of the most humiliating defeats that Rome ever suffered." }, { "Name": "Indibil", "Class": "", "Armament": "", "Emblem": "", "History": "Indibil was king of the Ilergetes, a large federation ranged principally along the Ebro River in the northwest of the Iberian Peninsula. During the Barcid expansion, from 212 B.C. he had initially been talked into allying himself with the Carthaginians who had taken control of a lot of territory to the south and west, however after loss and his capture in a major battle he was convinced, some say tricked, to switch to the Roman side by Scipio Africanus. But that alliance didn't last long, as Roman promises were hollow and the Romans acted more like conquerors than allies. So, while the Romans and their allies had ended Carthaginian presence in 'Hispania' in 206 B.C., Indibil and another tribal prince by the name of Mandonio, who may have been his brother, rose up in rebellion against the Romans. They were defeated in battle, but rose up in a 2nd even larger rebellion that had unified all the Ilergetes again in 205 B.C. Outnumbered and outarmed they were again defeated, Indibil losing his life in the final battle and Mandonio being captured then later put to death. From that date onward the Ilergetes remained a pacified tribe under Roman rule." } ] } ], "CivBonuses": [ { "Name": "Harritsu Leku", "History": "With exception to alluvial plains and river valleys, stone is abundant in the Iberian Peninsula and was greatly used in construction of structures of all types.", "Description": "Iberians start with a powerful prefabricated circuit of stone walls." }, { "Name": "Zaldi Saldoa", "History": "Not unlike Numidia in North Africa, the Iberian Peninsula was known as 'horse country', capable of producing up to 100,000 new mounts each year.", "Description": "The resource cost of training horse-mounted units (cavalry) is reduced by 5% per animal corralled." } ], "TeamBonuses": [ { "Name": "Saripeko", "History": "The Iberians were long known to provide mercenary soldiers to other nations to serve as auxiliaries to their armies in foreign wars. Carthage is the most well known example, and we have evidence of them serving in such a capacity in Aquitania.", "Description": "Citizen-soldier infantry skirmishers and cavalry skirmishers -20% cost for allies." } ], "Structures": [ { "Name": "Gur Oroigarri", "Class": "", "Emblem": "", "History": "'Revered Monument' The Iberians were a religious people who built small monuments to their various gods. These monuments could also serve as family tombs.", "Requirements": "", "Phase": "", "Special": "Defensive Aura - Gives all Iberian units and buildings within vision range of the monument a 10-15% attack boost. Build Limit: Only 5 may be built per map." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/iber_wallset_stone" + ], "StartEntities": [ { "Template": "structures/iber_civil_centre" }, { "Template": "units/iber_support_female_citizen", "Count": 4 }, { "Template": "units/iber_infantry_swordsman_b", "Count": 2 }, { "Template": "units/iber_infantry_javelinist_b", "Count": 2 }, { "Template": "units/iber_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Viriato", "Viriato", "Karos", "Indibil", "Audax", "Ditalcus", "Minurus", "Tautalus" ], "SkirmishReplacements": { "skirmish/units/default_infantry_melee_b": "units/iber_infantry_swordsman_b", "skirmish/structures/default_house_5": "structures/{civ}_house", "skirmish/structures/iber_wall_short": "structures/iber_wall_short", "skirmish/structures/iber_wall_medium": "structures/iber_wall_medium", "skirmish/structures/iber_wall_long": "structures/iber_wall_long", "skirmish/structures/iber_wall_gate": "structures/iber_wall_gate", "skirmish/structures/iber_wall_tower": "structures/iber_wall_tower" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/mace.json (revision 20625) @@ -1,176 +1,181 @@ { "Code":"mace", "Culture":"hele", "Name":"Macedonians", "Emblem":"session/portraits/emblems/emblem_macedonians.png", "History":"Macedonia was an ancient Greek kingdom, centered in the northeastern part of the Greek peninsula. Under the leadership of Alexander the Great, Macedonian forces and allies took over most of the world they knew, including Egypt, Persia and parts of the Indian subcontinent, allowing a diffusion of Hellenic and eastern cultures for years to come.", "Music":[ {"File":"Rise_of_Macedon.ogg", "Type":"peace"}, {"File":"In_the_Shadow_of_Olympus.ogg", "Type":"peace"}, {"File":"Elysian_Fields.ogg", "Type":"peace"}, {"File":"The_Hellespont.ogg", "Type":"peace"} ], "Factions": [ { "Name":"Macedonians", "Description":"A Hellenistic kingdom bordering the Greek city-states.", "Technologies": [ { "Name":"Military Reforms", "History":"When Philip II came to the Macedonian throne he began a total reorganization of the Macedonian army. His reforms created a powerful cavalry arm to his army that would prove useful to both himself and his son Alexander's conquests.", "Description":"Each subsequent Barracks constructed comes with 5 free (random) Macedonian military units. This also applies to the Barracks of allied players (they receive 5 free units of their own culture for each new Barracks constructed)." }, { "Name":"Royal Gift", "History":"In India near the end of his long anabasis, Alexander gifted to the Royal Hypaspist corps shields of silver for their long and valiant service in his army.", "Description":"Upgrade Hypaspist Champion Infantry to Silver Shields, with greater attack and armor, but also greater cost." }, { "Name":"Hellenistic Metropolises", "History":"Beginning with Alexander, the Hellenistic monarchs founded many cities throughout their empires, where Greek culture and art blended with local customs to create the motley Hellenistic civilization.", "Description":"Civic Centers have double Health and double default arrows." } ], "Heroes": [ { "Name":"Philip II of Macedon", "Class":"", "Armament":"", "Emblem":"", "History":"The king of Macedonia (359-336 B.C.), he carried out vast monetary and military reforms in order to make his kingdom the most powerful force in the Greek world. Greatly enlarged the size of Macedonia by conquering much of Thrace and subduing the Greeks. Murdered in Aegae while planning a campaign against Persia." }, { "Name":"Alexander the Great", "Class":"", "Armament":"", "Emblem":"", "History":"The most powerful hero of them all - son of Philip II, king of Macedonia (336-323 B.C.). After conquering the rest of the Thracians and quelling the unrest of the Greeks, Alexander embarked on a world-conquest march. Defeating the Persian forces at Granicus (334 B.C.), Issus (333 B.C.) and Gaugamela (331 B.C.), he became master of the Persian Empire. Entering India, he defeated king Porus at Hydaspes (326 B.C.), but his weary troops made him halt. Died in Babylon at the age of 33 while planning a campaign against Arabia." }, { "Name":"Demetrios the Besieger", "Class":"", "Armament":"", "Emblem":"", "History":"One of the Diadochi, king of Macedonia (294-288 B.C.), Demetrios was renowned as one of the bravest and most able successors of Alexander. As the son of Antigonus I Monophthalmus, he fought and won many important battles early on and was proclaimed king, along with his father, in 306 B.C. Losing his Asian possessions after the battle of Ipsos, he later won the Macedonian throne. Fearing lest they should be overpowered by Demetrios, the other Diadochi united against him and defeated him." } ] } ], "CivBonuses": [ { "Name":"Hellenic League", "History":"After the unification of Greece, Philip II gathered all the city-states together to form the Hellenic League, with Macedon as the its leader. With this Pan-Hellenic federation he planned to launch an expedition to punish Persia for past wrongs. Although assassinated before he could carry out the invasion, his son Alexander the Great took up the mantle and completed his fathers plans.", "Description":"Macedonian units have +10% attack bonus vs. Persian and Hellenic factions, but -5% attack debonus vs. Romans." }, { "Name":"Syntagma", "History":"Based upon the Theban Oblique Order phalanx, the Syntagma was the formation that proved invincible against the armies of Hellas and the East.", "Description":"Infantry pike units can use the slow and powerful Syntagma formation." }, { "Name":"Hellenization", "History":"The Greeks were highly successful in Hellenizing various foreigners. During the Hellenistic Age, Greek was the lingua franca of the Ancient World, spoken widely from Spain to India.", "Description":"Constructing a Theatron increases the territory expanse of all buildings by +20%." } ], "TeamBonuses": [ { "Name":"Standardized Currency", "History":"The Macedonians and the Diadochi minted coins of very high quality. On their currency the Diadochi in particular frequently depicted themselves as the rightful successor to Alexander the Great, attempting to legitimize their rule.", "Description":"+20% sell prices at market." } ], "Structures": [ { "Name":"Theatron", "Class":"", "Emblem":"", "History":"Greek theatres were places where the immortal tragedies of Aeschylus, Sophocles and many other talented dramatists were staged to the delight of the populace. They were instrumental in enriching Hellenic culture.", "Requirements":"", "Phase":"City", "Special":"The Hellenization civ bonus. Building a Theatron increases the territory effect of all buildings by 25%. Build limit: 1." }, { "Name":"Library", "Class":"", "Emblem":"", "History":"Alexander the Great founded libraries all over his new empire. These became a center of learning for an entirely new synthesized culture: the Hellenistic culture.", "Requirements":"", "Phase":"City", "Special":"All Special Technologies are researched here. Building one reduces the cost of all other remaining technologies by 10%. Build limit: 1." }, { "Name":"Siege Workshop", "Class":"", "Emblem":"", "History":"The Macedonians were innovators in area of siegecraft.", "Requirements":"", "Phase":"City", "Special":"Constructs and upgrades all Macedonian siege engines." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/mace_wallset_stone" + ], "StartEntities": [ { "Template": "structures/mace_civil_centre" }, { "Template": "units/mace_support_female_citizen", "Count": 4 }, { "Template": "units/mace_infantry_pikeman_b", "Count": 2 }, { "Template": "units/mace_infantry_javelinist_b", "Count": 2 }, { "Template": "units/mace_cavalry_spearman_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Alexander the Great", "Philip II", "Antipater", "Philip IV", "Lysander", "Lysimachus", "Pyrrhus of Epirus", "Antigonus II Gonatas", "Demetrius II Aetolicus", "Philip V", "Perseus", "Craterus", "Meleager" ], "SkirmishReplacements": { "skirmish/units/default_infantry_melee_b": "units/mace_infantry_pikeman_b", "skirmish/structures/default_house_10": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/maur.json (revision 20625) @@ -1,159 +1,164 @@ { "Code": "maur", "Culture": "maur", "Name": "Mauryans", "Emblem": "session/portraits/emblems/emblem_mauryans.png", "History": "Founded in 322 B.C. by Chandragupta Maurya, the Mauryan Empire was the first to rule most of the Indian subcontinent, and was one of the largest and most populous empires of antiquity. Its military featured bowmen who used the long-range bamboo longbow, fierce female warriors, chariots, and thousands of armored war elephants. Its philosophers, especially the famous Acharya Chanakya, contributed to such varied fields such as economics, religion, diplomacy, warfare, and good governance. Under the rule of Ashoka the Great, the empire saw 40 years of peace, harmony, and prosperity.", "Music":[ {"File":"An_old_Warhorse_goes_to_Pasture.ogg", "Type":"peace"}, {"File":"Land_between_the_two_Seas.ogg", "Type":"peace"}, {"File":"Eastern_Dreams.ogg", "Type":"peace"}, {"File":"Karmic_Confluence.ogg", "Type":"peace"} ], "Factions": [ { "Name": "Mauryan Indians", "Description": "", "Technologies": [ { "Name":"Elephant Roundup", "History":".", "Description":"Capture up to 5 Gaia elephants and garrison them in the Elephant Stables to gain up to a 25% bonus in cost and train time of elephant units." }, { "Name":"Archery Tradition", "History":"India was a land of archery. The bulk of any Indian army was made up of highly skilled archers, armed with bamboo longbows.", "Description":"Greater range and faster train time for Mauryan infantry archers." } ], "Heroes": [ { "Name":"Chandragupta Maurya", "Class":"", "Armament":"", "Emblem":"", "History":"Founder of the Mauryan Empire." }, { "Name":"Ashoka the Great", "Class":"", "Armament":"", "Emblem":"", "History":"Last great emperor of the Mauryan dynasty." }, { "Name":"Acharya Chāṇakya", "Class":"", "Armament":"", "Emblem":"", "History":"Great teacher and advisor to Chandragupta Maurya." } ] } ], "CivBonuses": [ { "Name": "Emperor of Emperors.", "History": "The Mauryan Empire encompassed dozens of formerly independent kingdoms over an area of 5 million square kilometers, with a population of close to 60 million people. The Mauryan regents held the title Emperor of Emperors and commanded a standing army of 600,000 infantry, 9000 elephants, 8000 chariots, and 30,000 cavalry, making it arguably the largest army of its time.", "Description": "Mauryans have a +10% population cap bonus (i.e., 330 pop cap instead of the usual 300)." }, { "Name": "Kṣhatriya Warrior Caste.", "History": "Kshatriya or Kashtriya, meaning warrior, is one of the four varnas (social orders) in Hinduism. Traditionally Kshatriya constitute the military and ruling elite of the Vedic-Hindu social system outlined by the Vedas and the Laws of Manu.", "Description": "The Mauryans enjoy access to 4 champions." } ], "TeamBonuses": [ { "Name": "Ashoka's Religious Support", "History": "Ashoka the Great sent embassies West to spread knowledge of the Buddha.", "Description": "Allied temples and temple technologies -50% cost and research time." } ], "Structures": [ { "Name":"Elephant Stables", "Class":"SB1", "Emblem":"", "History":".", "Requirements":"Temple", "Phase":"Town", "Special":"Trains Elephant Archer and Worker Elephant at Town Phase, then adds the champion War Elephant at the City phase." }, { "Name":"Edict Pillar of Ashoka", "Class":"SB2", "Emblem":"", "History":".", "Requirements":"Built by the Ashoka hero unit.", "Phase":"City", "Special":"Contentment: +10% Health and +10% resource gathering rates for all citizens and allied citizens within its range. Can be built anywhere except in enemy territory. Max Built: 10." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/maur_wallset_stone" + ], "StartEntities": [ { "Template": "structures/maur_civil_centre" }, { "Template": "units/maur_support_female_citizen", "Count": 4 }, { "Template": "units/maur_infantry_spearman_b", "Count": 2 }, { "Template": "units/maur_infantry_archer_b", "Count": 2 }, { "Template": "units/maur_cavalry_javelinist_b", "Count": 1 }, { "Template": "units/maur_support_elephant", "Count": 1 } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Chandragupta Maurya", "Ashoka the Great", "Ashokavardhan Maurya", "Acharya Bhadrabahu", "Bindusara Maurya", "Dasaratha Maurya", "Samprati Maurya", "Salisuka Maurya", "Devavarman Maurya", "Satadhanvan Maurya", "Brihadratha Maurya" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/maur_infantry_archer_b", "skirmish/units/special_starting_unit": "units/maur_support_elephant", "skirmish/structures/default_house_5": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/pers.json (revision 20625) @@ -1,164 +1,169 @@ { "Code": "pers", "Culture": "pers", "Name": "Persians", "Emblem": "session/portraits/emblems/emblem_persians.png", "History": "The Persian Empire, when ruled by the Achaemenid dynasty, was one of the greatest empires of antiquity, stretching at its zenith from the Indus Valley in the east to Greece in the west. The Persians were the pioneers of empire-building of the ancient world, successfully imposing a centralized rule over various peoples with different customs, laws, religions and languages, and building a cosmopolitan army made up of contingents from each of these nations.", "Music":[ {"File":"Eastern_Dreams.ogg", "Type":"peace"}, {"File":"Valley_of_the_Nile.ogg", "Type":"peace"}, {"File":"Land_between_the_two_Seas.ogg", "Type":"peace"}, {"File":"Sands_of_Time.ogg", "Type":"peace"} ], "Factions": [ { "Name": "", "Description": "", "Technologies": [ { "Name": "Naval Craftsmanship", "History": "Early Achaemenid rulers acted towards making Persia the first great Asian empire to rule the seas. The Great King behaved favourably towards the various sea peoples in order to secure their services, but also carried out various marine initiatives. During the reign of Darius the Great, for example, a canal was built in Egypt and a Persian navy was sent exploring the Indus river. According to Herodotus, some 300 ships in the Persian navy were retrofitted to carry horses and their riders.", "Description": "Phoenician triremes gain the unique ability to train cavalry units." }, { "Name": "Persian Architecture", "History": "The Persians built the wonderful 2700 kilometer-long Royal Highway from Sardis to Susa; Darius the Great and Xerxes also built the magnificent Persepolis; Cyrus the Great greatly improved Ecbatana and virtually 'rebuilt' the old Elamite capital of Susa.", "Description": "Increases hitpoints of all structures, but build time increased appropriately." }, { "Name": "Immortals", "History": ".", "Description": "Reduces train time for Anusiya champion infantry by half." }, { "Name": "Nisean War Horses", "History": "The beautiful and powerful breed of Nisean horses increases health for Persian cavalry.", "Description": "+25% health for cavalry, but +10% train time." } ], "Heroes": [ { "Name": "Kurush II", "Class": "", "Armament": "", "Emblem": "", "History": "Cyrus (ruled 559-530 B.C.) The son of a Median princess and the ruler of Anshan; justly called the 'Father of the Empire', Cyrus the Great conquered Media, Lydia, Babylonia and Bactria, thereby establishing the Persian Empire. He was also renown as a benevolent conqueror. (OP - Kurush). Technically the second ruler of the Persians by that name, and so appears as Kurush II on his documents and coins. Kurush I was his grandfather." }, { "Name": "Darayavahush I", "Class": "", "Armament": "", "Emblem": "", "History": "Darius (ruled 522-486 B.C.) The son of Vishtaspa (Hystaspes), the satrap of Parthia and Hyrcania; a great administrator as well as a decent general, Darius introduced the division of the empire into satrapies and conquered NW India, Thrace and Macedonia. He was called the 'Merchant of the Empire'." }, { "Name": "Xsayarsa I", "Class": "", "Armament": "", "Emblem": "", "History": "Xerxes (ruled 485-465 B.C.) The son of Darius the Great and Atoosa, a daughter of Cyrus the Great, Xerxes was an able administrator, who also extended Imperial rule into Chorasmia. Apart from his failed invasion of Greece, he was famous for his extensive building programme, especially at Persepolis." } ] } ], "CivBonuses": [ { "Name": "Corral Camels and Horses", "History": "While the Persians employed camelry only in a few cases, its use was always accompanied by great success (most notably during the battle of Sardis in 546 B.C.) The satrapy of Bactria was a rich source of 'two-hump' camels, while Northern Arabia supplied 'one-hump' camels.", "Description": "The resource cost of training camel-mounted (trader) or horse-mounted units (cavalry) is reduced by 5% per animal (as appropriate) corralled." }, { "Name": "Great King's Levy", "History": "The Persians could and did levy a large number of infantry during wartime due to the sheer size of the Achaemenid Empire and the way in which it was set-up. In general the Persian infantry was well trained and fought with great tenacity. However while this was true the infantry were poor hand-to-hand, close combat fighters. Also, with the exception of the elite regiments, the Persian infantry was not a standing professional force.", "Description": "Persians have a +10% population cap bonus (e.g. 330 pop cap instead of the usual 300)." } ], "TeamBonuses": [ { "Name": "Royal Road", "History": "Coinage was invented by the Lydians in 7th Century B.C., but it was not very common until the Persian period. Darius the Great standardized coined money and his golden coins (known as 'darics') became commonplace not only throughout his empire, but as far to the west as Central Europe.", "Description": "Higher income (+15%) from land trade routes for ally's traders." } ], "Structures": [ { "Name": "Cavalry Stables", "Class": "", "Emblem": "", "History": "The Persian Empire's best soldiers were Eastern horsemen.", "Requirements": "", "Phase": "Town", "Special": "Train Cavalry citizen-soldiers." }, { "Name": "Apadana", "Class": "", "Emblem": "", "History": "The term Apadana designates a large hypostyle palace found in Persia. The best known example, and by far the largest, was the great Apadana at Persepolis. Functioning as the empire's central audience hall, the palace is famous for the reliefs of the tribute-bearers and of the army, including the Immortals.", "Requirements": "", "Phase": "City", "Special": "Train heroes and Persian Immortals. Gives a slow trickle of all resources as 'Satrapy Tribute.'" } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/pers_wallset_stone" + ], "StartEntities": [ { "Template": "structures/pers_civil_centre" }, { "Template": "units/pers_support_female_citizen", "Count": 4 }, { "Template": "units/pers_infantry_spearman_b", "Count": 2 }, { "Template": "units/pers_infantry_archer_b", "Count": 2 }, { "Template": "units/pers_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line" ], "AINames": [ "Kurush II the Great", "Darayavahush I", "Cambyses II", "Bardiya", "Xsayarsa I", "Artaxshacha I", "Darayavahush II", "Darayavahush III", "Artaxshacha II", "Artaxshacha III", "Haxamanish", "Xsayarsa II" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/pers_infantry_archer_b", "skirmish/structures/default_house_10" : "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/ptol.json (revision 20625) @@ -1,184 +1,189 @@ { "Code":"ptol", "Culture":"ptol", "Name":"Ptolemies", "Emblem":"session/portraits/emblems/emblem_ptolemies.png", "History":"The Ptolemaic dynasty was a Macedonian Greek royal family which ruled the Ptolemaic Empire in Egypt during the Hellenistic period. Their rule lasted for 275 years, from 305 BC to 30 BC. They were the last dynasty of ancient Egypt.", "Music":[ {"File":"Ammon-Ra.ogg", "Type":"peace"}, {"File":"Sands_of_Time.ogg", "Type":"peace"}, {"File":"Land_between_the_two_Seas.ogg", "Type":"peace"}, {"File":"Valley_of_the_Nile.ogg", "Type":"peace"} ], "Factions": [ { "Name":"Ptolemaic Egyptians", "Description":"The great Greek-Macedonian dynastic rule over Ancient Egypt.", "Technologies": [ { "Name":"Hellenistic Metropolises", "History":"Beginning with Alexander, the Hellenistic monarchs founded many cities throughout their empires, where Greek culture and art blended with local customs to create the motley Hellenistic civilization.", "Description":"Civic Centers have double Health and double default arrows." }, { "Name":"Pharaonic Cult.", "History":"The Macedonian-Greek rulers of the Ptolemaic dynasty observed many ancient Egyptian traditions in order to satiate the local populace and ingratiate themselves to the powerful priestly class in the country.", "Description":"Hero aura range boosted by 50%." }, { "Name":"Nile Delta", "History":"The Nile Delta had rich soil for farming, due to centuries of seasonal floods from the Nile depositing rich silt across the landscape.", "Description":"The Ptolemaic Egyptians receive 3 additional farming technologies above and beyond the maximum number of farming technologies usually available to a faction." } ], "Heroes": [ { "Name":"Ptolemaios A' Soter", "Class":"", "Armament":"", "Emblem":"", "History":"." }, { "Name":"Ptolemaios D' Philopater", "Class":"", "Armament":"", "Emblem":"", "History":"." }, { "Name":"Kleopatra H' Philopater", "Class":"", "Armament":"", "Emblem":"", "History":"." } ] } ], "CivBonuses": [ { "Name":"Mercenary Army", "History":"The Greco-Macedonian Ptolemy Dynasty relied on large numbers of Greek and foreign mercenaries for the bulk of its military force, mainly because the loyalty of native Egyptian units was often suspect. Indeed, during one native uprising, Upper Egypt was lost to the Ptolemies for decades. Mercenaries were often battle-hardened and their loyalty could be bought, sometimes cheaply, sometimes not cheaply. This was of no matter, since Egypt under the Ptolemies was so prosperous as to be the richest of Alexander's successor states.", "Description":"The Ptolemies receive the Mercenary Camp, a barracks that is constructed in neutral territory and trains mercenary soldiers." }, { "Name":"Nile Delta", "History":"Unknown.", "Description":"The Ptolemaic Egyptians receive 3 additional farming technologies." }, { "Name":"Roundup", "History":"Unknown.", "Description":"Can capture gaia elephants and camels to reduce their training cost." } ], "TeamBonuses": [ { "Name":"Breadbasket of the Mediterranean", "History":"Egypt was a net exporter of grain, so much so that large cities such as Athens, Antioch, and Rome came to rely upon Egyptian grain in order to feed their masses.", "Description":"All allies automatically gain a slow trickle of food income." } ], "Structures": [ { "Name":"Library", "Class":"", "Emblem":"", "History":"Alexander the Great founded libraries all over his new empire. These became a center of learning for an entirely new synthesized culture: the Hellenistic culture.", "Requirements":".", "Phase":"City", "Special":"Maximum of 1 built. All Special Technologies and some regular city-phase technologies are researched here. Building one reduces the cost of all other remaining technologies by 10%." }, { "Name":"Stratópedo Misthophóron", "Class":"", "Emblem":"", "History":"The Greco-Macedonian Ptolemy Dynasty relied on large numbers of Greek and foreign mercenaries for the bulk of its military force, mainly because the loyalty of native Egyptian units was often suspect. Indeed, during one native uprising, Upper Egypt was lost to the Ptolemies for decades. Mercenaries were often battle-hardened and their loyalty can be bought, sometimes cheaply, sometimes not cheaply. This was of no matter, since Egypt under the Ptolemies was so prosperous as to be the richest of Alexander's successor states.", "Requirements":"", "Phase":"Town", "Special":"Must be constructed in neutral territory. Has no territory radius effect. Trains all 'mercenary' units." }, { "Name":"Lighthouse", "Class":"", "Emblem":"", "History":"The Ptolemaic dynasty in Egypt built the magnificent Lighthouse of Alexandria near the harbor mouth of that Nile Delta city. This structure could be seen for many kilometers out to sea and was one of the Seven Wonders of the World.", "Requirements":".", "Phase":"City", "Special":"When built along the shoreline, removes shroud of darkness over all the water, revealing all the coast lines on the map. Limit: 1." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/ptol_wallset_stone" + ], "StartEntities": [ { "Template": "structures/ptol_civil_centre" }, { "Template": "units/ptol_support_female_citizen", "Count": 4 }, { "Template": "units/ptol_infantry_pikeman_b", "Count": 2 }, { "Template": "units/ptol_infantry_javelinist_b", "Count": 2 }, { "Template": "units/ptol_cavalry_archer_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Ptolemy Soter", "Ptolemy Philadelphus", "Ptolemy Epigone", "Ptolemy Eurgetes", "Ptolemy Philopater", "Ptolemy Epiphanes", "Ptolemy Philometor", "Ptolemy Eupator", "Ptolemy Alexander", "Ptolemy Neos Dionysos", "Ptolemy Neos Philopater", "Berenice Philopater", "Cleopatra Tryphaena", "Berenice Epiphaneia", "Cleopatra Philopater", "Cleopatra Selene", "Cleopatra II Philometora Soteira", "Arsinoe IV", "Arsinoe II" ], "SkirmishReplacements": { "skirmish/units/default_infantry_ranged_b": "units/ptol_infantry_javelinist_b", "skirmish/units/default_infantry_melee_b": "units/ptol_infantry_pikeman_b", "skirmish/units/default_cavalry": "units/ptol_cavalry_archer_b", "skirmish/structures/default_house_5": "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/rome.json (revision 20625) @@ -1,152 +1,157 @@ { "Code": "rome", "Culture": "rome", "Name": "Romans", "Emblem": "session/portraits/emblems/emblem_romans.png", "History": "The Romans controlled one of the largest empires of the ancient world, stretching at its peak from southern Scotland to the Sahara Desert, and containing between 60 million and 80 million inhabitants, one quarter of the Earth's population at that time. Rome also remained one of the strongest nations on earth for almost 800 years. The Romans were the supreme builders of the ancient world, excelled at siege warfare and had an exquisite infantry and navy.", "Music":[ {"File":"Juno_Protect_You.ogg", "Type":"peace"}, {"File":"Mediterranean_Waves.ogg", "Type":"peace"}, {"File":"Elysian_Fields.ogg", "Type":"peace"} ], "Factions": [ { "Name": "", "Description": "", "Technologies": [ { "Name": "Divide et Impera", "History": "'Divide and conquer' was the main principle in Rome's foreign politics throughout its long history. The Romans lured enemies or neutral factions to their side by offering them certain privileges. In due period of time, friends as well as foes were subjugated.", "Description": "Roman heroes can convert enemy units with great cost." } ], "Heroes": [ { "Name": "Quintus Fabius Maximus", "Class": "", "Armament": "", "Emblem": "", "History": "Dictator for six months during the Second Punic War. Instead of attacking the most powerful Hannibal, he started a very effective war of attrition against him." }, { "Name": "Marcus Claudius Marcellus", "Class": "", "Armament": "", "Emblem": "", "History": "A soldier of the first war with Carthage, a hero of the Second Punic War, and victor over the Gauls at Clastidium. Plutarch describes him as a man of war, strong in body and constitution, with an iron will to fight on. As a general he was immensely capable, standing alongside Scipio Africanus and Claudius Nero as the most effective Roman generals of the entire Second Punic War. In addition to his military achievements Marcellus was a fan of Greek culture and arts, which he enthusiastically promoted in Rome. He met his demise when his men were ambushed near Venusia. In honor of the respect the people held for him, Marcellus was granted the title of 'Sword of Rome.'" }, { "Name": "Scipio Africanus", "Class": "", "Armament": "", "Emblem": "", "History": "He was the first really successful Roman general against the Carthaginians. His campaigns in Spain and Africa helped to bring Carthage to its knees during the Second Punic War. He defeated Hannibal at the Battle of Zama in 202 B.C." } ] } ], "CivBonuses": [ { "Name": "Testudo Formation", "History": "The Romans commonly used the Testudo or 'turtle' formation for defense: Legionaries were formed into hollow squares with twelve men on each side, standing so close together that their shields overlapped like fish scales.", "Description": "Roman Legionaries can form a Testudo." }, { "Name": "Citizenship", "History": "Roman Citizenship was highly prized in the ancient world. Basic rights and privileges were afforded Roman citizens that were denied other conquered peoples. It is said that harming a Roman citizen was akin to harming Rome herself, and would cause the entire might of Rome to fall upon the perpetrator.", "Description": "Any Roman citizen-soldier fighting within Roman territory gains a non-permanent +10% bonus in armor." } ], "TeamBonuses": [ { "Name": "Conscription", "History": "Many Roman soldiers were conscripted into service. While volunteers were preferred, the Roman state did maintain an annual military draft. During an emergency the draft and the terms of service were enlarged. The importance of military service in Republican Rome was so great it was a prerequisite for a political career. Members of the Senate were called Conscript Fathers because of this, reflected in how the ordo equester was said to have been \"conscripted\" into the Senate.", "Description": "-20% training time for allied infantry." } ], "Structures": [ { "Name": "Entrenched Camp", "Class": "", "Emblem": "", "History": "Sometimes it was a temporary camp built facing the route by which the army is to march, other times a defensive or offensive (for sieges) structure. Within the Praetorian gate, which should either front the east or the enemy, the tents of the first centuries or cohorts are pitched, and the dracos (ensigns of cohorts) and other ensigns planted. The Decumane gate is directly opposite to the Praetorian in the rear of the camp, and through this the soldiers are conducted to the place appointed for punishment or execution. It has a turf wall, and it's surrounded by a canal filled with water whenever possible for extra defense. Many towns started up as bigger military camps to evolve to more complicated cities.", "Requirements": "", "Phase": "", "Special": "Trains citizen-soldiers from neutral or enemy territory." }, { "Name": "Murus Latericius", "Class": "", "Emblem": "", "History": "Turf walls built by legionaries during sieges.", "Requirements": "", "Phase": "", "Special": "Can be built in neutral and enemy territory to strangle enemy towns." } ], + "WallSets": + [ + "structures/rome_wallset_stone", + "structures/rome_wallset_siege" + ], "StartEntities": [ { "Template": "structures/rome_civil_centre" }, { "Template": "units/rome_support_female_citizen", "Count": 4 }, { "Template": "units/rome_infantry_swordsman_b", "Count": 2 }, { "Template": "units/rome_infantry_javelinist_b", "Count": 2 }, { "Template": "units/rome_cavalry_spearman_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/testudo" ], "AINames": [ "Lucius Junius Brutus", "Lucius Tarquinius Collatinus", "Gaius Julius Caesar Octavianus", "Marcus Vipsanius Agrippa", "Gaius Iulius Iullus", "Gaius Servilius Structus Ahala", "Publius Cornelius Rufinus", "Lucius Papirius Cursor", "Aulus Manlius Capitolinus", "Publius Cornelius Scipio Africanus", "Publius Sempronius Tuditanus", "Marcus Cornelius Cethegus", "Quintus Caecilius Metellus Pius", "Marcus Licinius Crassus" ], "SkirmishReplacements": { "skirmish/units/default_cavalry" : "units/rome_cavalry_spearman_b", "skirmish/units/default_infantry_melee_b": "units/rome_infantry_swordsman_b", "skirmish/structures/default_house_10" : "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/sele.json (revision 20625) @@ -1,191 +1,196 @@ { "Code":"sele", "Culture":"sele", "Name":"Seleucids", "Emblem":"session/portraits/emblems/emblem_seleucids.png", "History":"The Macedonian-Greek dynasty that ruled most of Alexander's former empire.", "Music":[ {"File":"Rise_of_Macedon.ogg", "Type":"peace"}, {"File":"In_the_Shadow_of_Olympus.ogg", "Type":"peace"}, {"File":"The_Hellespont.ogg", "Type":"peace"} ], "Factions": [ { "Name":"Seleucids", "Description":"The Macedonian-Greek dynasty that ruled the Eastern part of Alexander's former empire.", "Technologies": [ { "Name":"Hellenistic Metropolises", "History":"Beginning with Alexander, the Hellenistic monarchs founded many cities throughout their empires, where Greek culture and art blended with local customs to create the motley Hellenistic civilization.", "Description":"Civic Centers have double Health and double default arrows." }, { "Name":"Traditional Army vs. Reform Army", "History":"Seleucid and indeed Successor warfare evolved over the course of the 3rd and 2nd centuries. Contact with Eastern upstarts such as the Parthians and constant revolts of peripheral satrapies such as Bactria caused the Seleucids to reform their military and change their tactics, specifically in the cavalry arm. War with the Romans from the West and invasions from the Galatians also forced the Seleucids to reform their infantry regiments to be more flexible.", "Description":"Traditional Army unlocks Silver Shields and Scythed Chariots, Reform Army unlocks Romanized Heavy Swordsmen and Cataphracts." }, { "Name":"Marriage Alliance", "History":"Seleucus I Nicator invaded the Punjab region of India in 305 BC, confronting Chandragupta Maurya (Sandrokottos), founder of the Mauryan empire. It is said that Chandragupta fielded an army of 600,000 men and 9,000 war elephants (Pliny, Natural History VI, 22.4). Seleucus met with no success and to establish peace between the two great powers and to formalize their alliance, he married his daughter to Chandragupta. In return, Chandragupta gifted Seleucus a corps of 500 war elephants, which would prove a decisive military asset for Seleucus as he fought the rest of Alexander's successors.", "Description":"A one-time purchase of 20 Indian War Elephants from the Mauryan Empire." }, { "Name": "Nisean War Horses", "History": "The beautiful and powerful breed of Nisean horses increases health for Seleucid cavalry.", "Description": "+25% health for cavalry, but +10% train time." } ], "Heroes": [ { "Name":"Seleukos A' Nikator", "Class":"", "Armament":"", "Emblem":"", "History":"Always lying in wait for the neighboring nations, strong in arms and persuasive in council, he (Seleucus) acquired Mesopotamia, Armenia, 'Seleucid' Cappadocia, Persis, Parthia, Bactria, Arabia, Tapouria, Sogdia, Arachosia, Hyrcania, and other adjacent peoples that had been subdued by Alexander, as far as the river Indus, so that the boundaries of his empire were the most extensive in Asia after that of Alexander. The whole region from Phrygia to the Indus was subject to Seleucus. — Appian, 'The Syrian Wars'." }, { "Name":"Antiokhos G' Megas", "Class":"", "Armament":"", "Emblem":"", "History":"Antiochus inherited a troubled kingdom upon the beginning of his reign. From the verge of collapse he managed to weld back together the empire Seleukus I fought so hard to found. The rebellious eastern satraps of Bactria and Parthia were brought to heel , temporarily securing his eastern borders. He then turned his attention to mother Greece, attempting to fulfill the dreams of his fathers by invading Greece under the pretext of liberation. The Achaean League and the Kingdom of Pergamon banded together with the Romans to defeat him at the Battle of Magnesia, forever burying the dream of reuniting Alexander's empire." }, { "Name":"Antiokhos D' Epiphanes", "Class":"", "Armament":"", "Emblem":"", "History":"Antiochus IV Epiphanes was a son of Antiochus III the Great and brother of Seleucus IV Philopator. Originally named Mithridates, he assumed the name Antiochus either upon his accession to the throne or after the death of his elder brother Antiochus. Notable events during his reign include the near-conquest of Egypt (twice), which was halted by the threat of Roman intervention, and the beginning of the Jewish revolt of the Maccabees. He died of sudden illness while fighting off a Parthian invasion from the East." } ] } ], "CivBonuses": [ { "Name":"Cleruchy", "History":".", "Description":"This unlocks the Seleucid expansion building, the Klēroukhia or Military Colony, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster." }, { "Name":"Military Reforms", "History":"Seleucid and indeed Successor warfare evolved over the course of the 3rd and 2nd centuries. Contact with Eastern upstarts such as the Parthians and constant revolts of peripheral satrapies such as Bactria caused the Seleucids to reform their military and change their tactics, specifically in the cavalry arm. War with the Romans from the West and invasions from the Galatians also forced the Seleucids to reform their infantry regiments to be more flexible.", "Description":"Choose between Traditional Army and Reform Army technologies that unlock different Champions." } ], "TeamBonuses": [ { "Name":"Syrian Tetrapolis", "History":"The political core of the Seleucid Empire consisted of four 'sister' cities: Antioch (the capital), Seleucia Pieria, Apamea, and Laodicea.", "Description":"Allied Civic Centers are 20% cheaper." } ], "Structures": [ { "Name":"Library", "Class":"", "Emblem":"", "History":"Alexander the Great founded libraries all over his new empire. These became a center of learning for an entirely new synthesized culture: the Hellenistic culture.", "Requirements":".", "Phase":"City", "Special":"Maximum of 1 built. All Special Technologies and some regular city-phase technologies are researched here. Building one reduces the cost of all other remaining technologies by 10%." }, { "Name":"Cavalry Stables", "Class":"", "Emblem":"", "History":".", "Requirements":"", "Phase":"Village", "Special":"Trains all cavalry units except Citizen-Militia Cavalry." }, { "Name":"Military Colony", "Class":"", "Emblem":"", "History":"The Seleucid kings invited Greeks, Macedonians, Galatians (Gauls), Cretans, and Thracians alike to settle in within the vast territories of the empire. They settled in military colonies called cleruchies (klēroukhia). Under this arrangement, the settlers were given a plot of land, or a kleros, and in return were required to serve in the great king's army when called to duty. This created a upper-middle class of military settlers who owed their livelihoods and fortunes to the Syrian kings and helped grow the available manpower for the imperial Seleucid army. A side effect of this system was that it drained the Greek homeland of military-aged men, a contributing factor to Greece's eventual conquest by Rome.", "Requirements":".", "Phase":"Town", "Special":"This is the Seleucid expansion building, similar to Civic Centers for other factions. It is weaker and carries a smaller territory influence, but is cheaper and built faster." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/sele_wallset_stone" + ], "StartEntities": [ { "Template": "structures/sele_civil_centre" }, { "Template": "units/sele_support_female_citizen", "Count": 4 }, { "Template": "units/sele_infantry_spearman_b", "Count": 2 }, { "Template": "units/sele_infantry_javelinist_b", "Count": 2 }, { "Template": "units/sele_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/phalanx", "special/formations/syntagma" ], "AINames": [ "Seleucus I Nicator", "Antiochus I Soter", "Antiochus II Theos", "Seleucus II Callinicus", "Seleucus III Ceraunus", "Antiochus III Megas", "Seleucus IV Philopator", "Antiochus IV Epiphanes", "Antiochus V Eupator", "Demetrius I Soter", "Alexander I Balas", "Demetrius II Nicator", "Antiochus VI Dionysus", "Diodotus Tryphon", "Antiochus VII Sidetes", "Demetrius II Nicator", "Alexander II Zabinas", "Cleopatra Thea", "Seleucus V Philometor", "Antiochus VIII Grypus", "Antiochus IX Cyzicenus", "Seleucus VI Epiphanes", "Antiochus X Eusebes", "Demetrius III Eucaerus", "Antiochus XI Epiphanes", "Philip I Philadelphus", "Antiochus XII Dionysus", "Seleucus VII Kybiosaktes", "Antiochus XIII Asiaticus", "Philip II Philoromaeus" ], "SkirmishReplacements": { "skirmish/structures/default_house_10" : "structures/{civ}_house" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/data/civs/spart.json (revision 20625) @@ -1,169 +1,174 @@ { "Code":"spart", "Culture":"hele", "Name":"Spartans", "Emblem":"session/portraits/emblems/emblem_spartans.png", "History":"Sparta was a prominent city-state in ancient Greece, and its dominant military power on land from circa 650 B.C. Spartan culture was obsessed with military training and excellence, with rigorous training for boys beginning at age seven. Thanks to its military might, Sparta led a coalition of Greek forces during the Greco-Persian Wars, and won over Athens in the Peloponnesian Wars, though at great cost.", "Music":[ {"File":"Helen_Leaves_Sparta.ogg", "Type":"peace"}, {"File":"Peaks_of_Atlas.ogg", "Type":"peace"}, {"File":"Forging_a_City-State.ogg", "Type":"peace"}, {"File":"The_Hellespont.ogg", "Type":"peace"} ], "Factions": [ { "Name":"Spartans", "Description":"", "Technologies": [ { "Name":"Feminine Mystique", "History":"Spartan women were some of the freest in the ancient world. They could own land and slaves and even exercise naked like Spartan men. It is said that only Spartan women gave birth to real men. Such tough-as-nails women more than once helped save their city from disaster, for example when after a lost battle against Pyrrhus of Epirus they overnight built an earthen rampart to protect the city while their men slept in preparation for the next day's siege.", "Description":"Spartan female citizens cannot be captured and will doggedly fight back against any attackers. They are also capable of constructing defense towers and palisades." }, { "Name":"Tyrtean Paeans", "History":"Paeans were battle hymns that were sung by the hoplites when they charged the enemy lines. One of the first known Paeans were composed by Tirteus, a warrior poet of Sparta, during the First Messenian War.", "Description":"Units in phalanx formation move faster." }, { "Name":"The Agoge", "History":"Spartans were housed and trained from a young age to be superlative warriors and to endure any hardship a military life can give them.", "Description":"+25% health for spear infantry, but also +10% train time." } ], "Heroes": [ { "Name":"Leonidas I", "Class":"", "Armament":"", "Emblem":"", "History":"The king of Sparta, who fought and died at the battle of Thermopylae in 480 B.C. He successfully blocked the way of the huge Persian army through the narrow passage with his 7000 men, until Xerxes was made aware of a secret unobstructed path. Finding the enemy at his rear, Leonidas sent home most of his troops, choosing to stay behind with 300 hand-picked hoplites and win time for the others to withdraw." }, { "Name":"Brasidas", "Class":"", "Armament":"", "Emblem":"", "History":"Because Brasidas has sponsored their citizenship in return for service, Helot Skirmishers fight longer and harder for Sparta while within range of him." }, { "Name":"Agis III", "Class":"", "Armament":"", "Emblem":"", "History":"Agis III was the 20th Spartan king of the Eurypontid lineage. Agis cobbled together an alliance of Southern Greek states to fight off Macedonian hegemony while Alexander the Great was away in Asia on his conquest march. After securing Crete as a Spartan tributary, Agis then moved to besiege the city of Megalopolis in the Peloponnese, who was an ally of Macedon. Antipater, the Macedonian regent, lead an army to stop this new uprising. In the Battle of Megalopolis, the Macedonians prevailed in a long and bloody battle. Much like Leonidas 150 years earlier, instead of surrendering, Agis made a heroic final stand in order to buy time for his troops to retreat." } ] } ], "CivBonuses": [ { "Name":"Othismos", "History":"The Spartans were undisputed masters of phalanx warfare. The Spartans were so feared for their discipline that the enemy army would sometimes break up and run away before a single shield clashed. 'Othismos' refers to the point in a phalanx battle where both sides try to shove each other out of formation, attempting to breaking up the enemy lines and routing them.", "Description":"Spartans can use the powerful Phalanx formation." }, { "Name":"Laws of Lycurgus", "History":"Under the Constitution written by the mythical law-giver Lycurgus, the institution of The Agoge was established, where Spartans were trained from the age of 6 to be superior warriors in defense of the Spartan state.", "Description":"The Spartan rank upgrades at the Barracks cost no resources, except time." }, { "Name":"Hellenization", "History":"The Greeks were highly successful in Hellenising various foreigners. During the Hellenistic Age, Greek was the lingua franca of the Ancient World, spoken widely from Spain to India.", "Description":"Constructing a Theatron increases the territory expanse of all buildings by +20%." } ], "TeamBonuses": [ { "Name":"Peloponnesian League", "History":"Much of the Peloponnese was subject to Sparta in one way or another. This loose confederation, with Sparta as its leader, was later dubbed the Peloponnesian League by historians, but in ancient times was called 'The Lacedaemonians and their allies.'", "Description":"Increases the health for citizen-soldier infantry spearmen by 10% for allies of Spartan players." } ], "Structures": [ { "Name":"Theatron", "Class":"", "Emblem":"", "History":"Greek theatres were places where the immortal tragedies of Aeschylus, Sophocles and many other talented dramatists were staged to the delight of the populace. They were instrumental in enriching Hellenic culture.", "Requirements":"", "Phase":"City", "Special":"The Hellenization civ bonus. Building a Theatron increases the territory effect of all buildings by 25%." }, { "Name":"Syssition", "Class":"", "Emblem":"", "History":"The Syssition was the Mess Hall for full-blooded Spartiates. Every Spartan peer, even kings, belonged to one.", "Requirements":"", "Phase":"City", "Special":"Train heroes and Spartiates and research technologies related to them." } ], + "WallSets": + [ + "other/wallset_palisade", + "structures/spart_wallset_stone" + ], "StartEntities": [ { "Template": "structures/spart_civil_centre" }, { "Template": "units/spart_support_female_citizen", "Count": 4 }, { "Template": "units/spart_infantry_spearman_b", "Count": 2 }, { "Template": "units/spart_infantry_javelinist_b", "Count": 2 }, { "Template": "units/spart_cavalry_javelinist_b" } ], "Formations": [ "special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/skirmish", "special/formations/wedge", "special/formations/battle_line", "special/formations/phalanx" ], "AINames": [ "Leonidas", "Dienekes", "Brasidas", "Agis", "Archidamus", "Lysander", "Pausanias", "Agesilaus", "Echestratus", "Eurycrates", "Eucleidas", "Agesipolis" ], "SkirmishReplacements": { "skirmish/structures/default_house_10" : "structures/{civ}_house", "skirmish/structures/default_wall_tower": "", "skirmish/structures/default_wall_gate": "", "skirmish/structures/default_wall_short": "", "skirmish/structures/default_wall_medium": "", "skirmish/structures/default_wall_long": "" }, "SelectableInGameSetup": true } Index: ps/trunk/binaries/data/mods/public/simulation/templates/other/wallset_palisade.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/other/wallset_palisade.xml (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/templates/other/wallset_palisade.xml (revision 20625) @@ -1,20 +1,25 @@ gaia Wooden Wall Palisade -StoneWall Palisade structures/palisade_wall.png phase_village other/palisades_rocks_tower other/palisades_rocks_gate + other/palisades_rocks_fort other/palisades_rocks_long other/palisades_rocks_medium other/palisades_rocks_short + + other/palisades_rocks_curve + + other/palisades_rocks_end Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml (revision 20624) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_wallset_siege.xml (revision 20625) @@ -1,22 +1,23 @@ rome Siege Wall Murus Latericius structures/siege_wall.png A wooden and turf palisade buildable in enemy and neutral territories. phase_city structures/rome_siege_wall_tower structures/rome_siege_wall_gate + structures/rome_army_camp structures/rome_siege_wall_long structures/rome_siege_wall_medium structures/rome_siege_wall_short 1.00 0.05