Index: ps/trunk/binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 23518) +++ ps/trunk/binaries/data/mods/public/globalscripts/Templates.js (revision 23519) @@ -1,551 +1,552 @@ /** * Loads history and gameplay data of all civs. * * @param selectableOnly {boolean} - Only load civs that can be selected * in the gamesetup. Scenario maps might set non-selectable civs. */ function loadCivFiles(selectableOnly) { let propertyNames = [ "Code", "Culture", "Name", "Emblem", "History", "Music", "Factions", "CivBonuses", "TeamBonuses", "Structures", "StartEntities", "Formations", "AINames", "SkirmishReplacements", "SelectableInGameSetup"]; let civData = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/civs/", "*.json", false)) { let data = Engine.ReadJSONFile(filename); for (let prop of propertyNames) if (data[prop] === undefined) throw new Error(filename + " doesn't contain " + prop); if (!selectableOnly || data.SelectableInGameSetup) civData[data.Code] = data; } return civData; } /** * 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 && modifiers[mod_key]) current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); } /** * Get information about a template with or without technology modifications. * * NOTICE: The data returned here should have the same structure as * the object returned by GetEntityState and GetExtendedEntityState! * * @param {object} template - A valid template as returned by the template loader. * @param {number} player - An optional player id to get the technology modifications * of properties. * @param {object} auraTemplates - In the form of { key: { "auraName": "", "auraDescription": "" } }. * @param {object} modifiers - Modifications from auto-researched techs, unit upgrades * etc. Optional as only used if there's no player * id provided. */ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {}) { // Return data either from template (in tech tree) or sim state (ingame). // @param {string} value_path - Route to the value within the template. // @param {string} mod_key - Modification key, if not the same as the value_path. let getEntityValue = function(value_path, mod_key) { return GetModifiedTemplateDataValue(template, value_path, mod_key, player, modifiers); }; let ret = {}; if (template.Armour) { ret.armour = {}; for (let damageType in template.Armour) if (damageType != "Foundation") ret.armour[damageType] = getEntityValue("Armour/" + damageType); } let getAttackEffects = (temp, path) => { let effects = {}; if (temp.Capture) effects.Capture = getEntityValue(path + "/Capture"); if (temp.Damage) { effects.Damage = {}; for (let damageType in temp.Damage) effects.Damage[damageType] = getEntityValue(path + "/Damage/" + damageType); } if (temp.ApplyStatus) effects.ApplyStatus = temp.ApplyStatus; return effects; }; if (template.Attack) { ret.attack = {}; for (let type in template.Attack) { let getAttackStat = function(stat) { return getEntityValue("Attack/" + type + "/" + stat); }; ret.attack[type] = { "minRange": getAttackStat("MinRange"), "maxRange": getAttackStat("MaxRange"), "elevationBonus": getAttackStat("ElevationBonus"), }; ret.attack[type].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange * (2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange)); ret.attack[type].repeatTime = getAttackStat("RepeatTime"); + if (template.Attack[type].Projectile) + ret.attack[type].friendlyFire = template.Attack[type].Projectile.FriendlyFire == "true"; Object.assign(ret.attack[type], getAttackEffects(template.Attack[type], "Attack/" + type)); if (template.Attack[type].Splash) { ret.attack[type].splash = { - // true if undefined "friendlyFire": template.Attack[type].Splash.FriendlyFire != "false", "shape": template.Attack[type].Splash.Shape, }; Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash")); } } } if (template.DeathDamage) { ret.deathDamage = { "friendlyFire": template.DeathDamage.FriendlyFire != "false", }; Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage")); } if (template.Auras && auraTemplates) { ret.auras = {}; for (let auraID of template.Auras._string.split(/\s+/)) { let aura = auraTemplates[auraID]; ret.auras[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": aura.radius || null }; } } if (template.BuildingAI) ret.buildingAI = { "defaultArrowCount": Math.round(getEntityValue("BuildingAI/DefaultArrowCount")), "garrisonArrowMultiplier": getEntityValue("BuildingAI/GarrisonArrowMultiplier"), "maxArrowCount": Math.round(getEntityValue("BuildingAI/MaxArrowCount")) }; if (template.BuildRestrictions) { // required properties ret.buildRestrictions = { "placementType": template.BuildRestrictions.PlacementType, "territory": template.BuildRestrictions.Territory, "category": template.BuildRestrictions.Category, }; // optional properties if (template.BuildRestrictions.Distance) { ret.buildRestrictions.distance = { "fromClass": template.BuildRestrictions.Distance.FromClass, }; if (template.BuildRestrictions.Distance.MinDistance) ret.buildRestrictions.distance.min = getEntityValue("BuildRestrictions/Distance/MinDistance"); if (template.BuildRestrictions.Distance.MaxDistance) ret.buildRestrictions.distance.max = getEntityValue("BuildRestrictions/Distance/MaxDistance"); } } if (template.TrainingRestrictions) ret.trainingRestrictions = { "category": template.TrainingRestrictions.Category, }; if (template.Cost) { ret.cost = {}; for (let resCode in template.Cost.Resources) ret.cost[resCode] = getEntityValue("Cost/Resources/" + resCode); if (template.Cost.Population) ret.cost.population = getEntityValue("Cost/Population"); if (template.Cost.PopulationBonus) ret.cost.populationBonus = getEntityValue("Cost/PopulationBonus"); if (template.Cost.BuildTime) ret.cost.time = getEntityValue("Cost/BuildTime"); } if (template.Footprint) { ret.footprint = { "height": template.Footprint.Height }; if (template.Footprint.Square) ret.footprint.square = { "width": +template.Footprint.Square["@width"], "depth": +template.Footprint.Square["@depth"] }; else if (template.Footprint.Circle) ret.footprint.circle = { "radius": +template.Footprint.Circle["@radius"] }; else warn("GetTemplateDataHelper(): Unrecognized Footprint type"); } if (template.GarrisonHolder) { ret.garrisonHolder = { "buffHeal": getEntityValue("GarrisonHolder/BuffHeal") }; if (template.GarrisonHolder.Max) ret.garrisonHolder.capacity = getEntityValue("GarrisonHolder/Max"); } if (template.Heal) ret.heal = { "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); ret.nativeCiv = template.Identity.Civ; } if (template.UnitMotion) { ret.speed = { "walk": getEntityValue("UnitMotion/WalkSpeed"), }; ret.speed.run = getEntityValue("UnitMotion/WalkSpeed"); if (template.UnitMotion.RunMultiplier) ret.speed.run *= getEntityValue("UnitMotion/RunMultiplier"); } if (template.Upgrade) { ret.upgrades = []; for (let upgradeName in template.Upgrade) { let upgrade = template.Upgrade[upgradeName]; let cost = {}; if (upgrade.Cost) for (let res in upgrade.Cost) cost[res] = getEntityValue("Upgrade/" + upgradeName + "/Cost/" + res, "Upgrade/Cost/" + res); if (upgrade.Time) cost.time = getEntityValue("Upgrade/" + upgradeName + "/Time", "Upgrade/Time"); ret.upgrades.push({ "entity": upgrade.Entity, "tooltip": upgrade.Tooltip, "cost": cost, "icon": upgrade.Icon || undefined, "requiredTechnology": upgrade.RequiredTechnology || undefined }); } } if (template.ProductionQueue) { ret.techCostMultiplier = {}; for (let res in template.ProductionQueue.TechCostMultiplier) ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res); } if (template.Trader) ret.trader = { "GainMultiplier": getEntityValue("Trader/GainMultiplier") }; if (template.WallSet) { ret.wallSet = { "templates": { "tower": template.WallSet.Templates.Tower, "gate": template.WallSet.Templates.Gate, "fort": template.WallSet.Templates.Fort || "structures/" + template.Identity.Civ + "_fortress", "long": template.WallSet.Templates.WallLong, "medium": template.WallSet.Templates.WallMedium, "short": template.WallSet.Templates.WallShort }, "maxTowerOverlap": +template.WallSet.MaxTowerOverlap, "minTowerOverlap": +template.WallSet.MinTowerOverlap }; if (template.WallSet.Templates.WallEnd) ret.wallSet.templates.end = template.WallSet.Templates.WallEnd; if (template.WallSet.Templates.WallCurves) ret.wallSet.templates.curves = template.WallSet.Templates.WallCurves.split(" "); } if (template.WallPiece) ret.wallPiece = { "length": +template.WallPiece.Length, "angle": +(template.WallPiece.Orientation || 1) * Math.PI, "indent": +(template.WallPiece.Indent || 0), "bend": +(template.WallPiece.Bend || 0) * Math.PI }; return ret; } /** * Get basic information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the tech requirements should be calculated. */ function GetTechnologyBasicDataHelper(template, civ) { return { "name": { "generic": template.genericName }, "icon": template.icon ? "technologies/" + template.icon : undefined, "description": template.description, "reqs": DeriveTechnologyRequirements(template, civ), "modifications": template.modifications, "affects": template.affects, "replaces": template.replaces }; } /** * Get information about a technology template. * @param {object} template - A valid template as obtained by loading the tech JSON file. * @param {string} civ - Civilization for which the specific name and tech requirements should be returned. */ function GetTechnologyDataHelper(template, civ, resources) { let ret = GetTechnologyBasicDataHelper(template, civ); if (template.specificName) ret.name.specific = template.specificName[civ] || template.specificName.generic; ret.cost = { "time": template.researchTime ? +template.researchTime : 0 }; for (let type of resources.GetCodes()) ret.cost[type] = +(template.cost && template.cost[type] || 0); ret.tooltip = template.tooltip; ret.requirementsTooltip = template.requirementsTooltip || ""; return ret; } function calculateCarriedResources(carriedResources, tradingGoods) { var resources = {}; if (carriedResources) for (let resource of carriedResources) resources[resource.type] = (resources[resource.type] || 0) + resource.amount; if (tradingGoods && tradingGoods.amount) resources[tradingGoods.type] = (resources[tradingGoods.type] || 0) + (tradingGoods.amount.traderGain || 0) + (tradingGoods.amount.market1Gain || 0) + (tradingGoods.amount.market2Gain || 0); return resources; } /** * Remove filter prefix (mirage, corpse, etc) from template name. * * ie. filter|dir/to/template -> dir/to/template */ function removeFiltersFromTemplateName(templateName) { return templateName.split("|").pop(); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 23519) @@ -1,614 +1,617 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; Attack.prototype.preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "" + "10.0" + "0.0" + "5.0" + "" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "" + "0.0" + "10.0" + "0.0" + "" + "44.0" + "20.0" + "15.0" + "800" + "1600" + "1000" + "" + "" + "Cavalry" + "2" + "" + "" + "" + "50.0" + "2.5" + "props/units/weapons/rock_flaming.xml" + "props/units/weapons/rock_explosion.xml" + "0.1" + + "false" + "" + "Champion" + "" + "Circular" + "20" + "false" + "" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "" + "" + "1000.0" + "0.0" + "0.0" + "" + "4.0" + "" + "" + "" + "" + "" + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + Attacking.BuildAttackEffectsSchema() + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attacking.BuildAttackEffectsSchema() + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + Attacking.BuildAttackEffectsSchema() + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + Attacking.BuildAttackEffectsSchema() + "" + // TODO: how do these work? Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); if (!wantedTypes) return types; let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) return this.template[type].PreferredClasses._string.split(/\s+/); return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) return this.template[type].RestrictedClasses._string.split(/\s+/); return []; }; Attack.prototype.CanAttack = function(target, wantedTypes) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return true; let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; let cmpIdentity = QueryMiragedInterface(target, IID_Identity); if (!cmpIdentity) return false; let cmpHealth = QueryMiragedInterface(target, IID_Health); let targetClasses = cmpIdentity.GetClassesList(); if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) return true; let cmpEntityPlayer = QueryOwnerInterface(this.entity); let cmpTargetPlayer = QueryOwnerInterface(target); if (!cmpTargetPlayer || !cmpEntityPlayer) return false; let types = this.GetAttackTypes(wantedTypes); let entityOwner = cmpEntityPlayer.GetPlayerID(); let targetOwner = cmpTargetPlayer.GetPlayerID(); let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); for (let type of types) { if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) continue; if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; if (heightDiff > this.GetRange(type).max) continue; let restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) return true; if (!MatchesClassList(targetClasses, restrictedClasses)) return true; } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let targetClasses = cmpIdentity.GetClassesList(); let minPref = null; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); for (let targetClass of targetClasses) { let pref = preferredClasses.indexOf(targetClass); if (pref === 0) return pref; if (pref != -1 && (minPref === null || minPref > pref)) minPref = pref; } } return minPref; }; /** * Get the full range of attack using all available attack types. */ Attack.prototype.GetFullAttackRange = function() { let ret = { "min": Infinity, "max": 0 }; for (let type of this.GetAttackTypes()) { let range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); } return ret; }; Attack.prototype.GetAttackEffectsData = function(type, splash) { let template = this.template[type]; if (splash) template = template.Splash; return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); }; /** * Find the best attack against a target. * @param {number} target - The entity-ID of the target. * @param {boolean} allowCapture - Whether capturing is allowed. * @return {string} - The preferred attack type. */ Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) { // TODO: Formation against formation needs review let types = this.GetAttackTypes(); return g_AttackTypes.find(attack => types.indexOf(attack) != -1); } let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; // Always slaughter domestic animals instead of using a normal attack if (this.template.Slaughter && cmpIdentity.HasClass("Domestic")) return "Slaughter"; let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type])); // Check whether the target is capturable and prefer that when it is allowed. let captureIndex = types.indexOf("Capture"); if (captureIndex != -1) { if (allowCapture) return "Capture"; types.splice(captureIndex, 1); } let targetClasses = cmpIdentity.GetClassesList(); let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType)); return types.sort((a, b) => (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { let aPreference = this.GetPreference(a); let bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { let prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); let repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); return { "prepare": prepare, "repeat": repeat }; }; Attack.prototype.GetSplashData = function(type) { if (!this.template[type].Splash) return; return { "attackData": this.GetAttackEffectsData(type, true), - "friendlyFire": this.template[type].Splash.FriendlyFire != "false", + "friendlyFire": this.template[type].Splash.FriendlyFire == "true", "radius": ApplyValueModificationsToEntity("Attack/" + type + "/Splash/Range", +this.template[type].Splash.Range, this.entity), "shape": this.template[type].Splash.Shape, }; }; Attack.prototype.GetRange = function(type) { if (!type) return this.GetFullAttackRange(); let max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); let min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); let elevationBonus = +(this.template[type].ElevationBonus || 0); elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); // If this is a ranged attack, then launch a projectile if (type == "Ranged") { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength()/1000; // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely let horizSpeed = +this.template[type].Projectile.Speed; let gravity = +this.template[type].Projectile.Gravity; // horizSpeed /= 2; gravity /= 2; // slow it down for testing let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let selfPosition = cmpPosition.GetPosition(); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition(); let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) * predictedPosition.horizDistanceTo(selfPosition) / 100; let randNorm = randomNormal2D(); let offsetX = randNorm[0] * distanceModifiedSpread; let offsetZ = randNorm[1] * distanceModifiedSpread; let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); // Recalculate when the missile will hit the target position. let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); timeToTarget = realHorizDistance / horizSpeed; let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); // Launch the graphical projectile. let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); let actorName = ""; let impactActorName = ""; let impactAnimationLifetime = 0; actorName = this.template[type].Projectile.ActorName || ""; impactActorName = this.template[type].Projectile.ImpactActorName || ""; impactAnimationLifetime = this.template[type].Projectile.ImpactAnimationLifetime || 0; // TODO: Use unit rotation to implement x/z offsets. let deltaLaunchPoint = new Vector3D(0, +this.template[type].Projectile.LaunchPoint["@y"], 0); let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) { // if the projectile definition is missing from the template // then fallback to the projectile name and launchpoint in the visual actor if (!actorName) actorName = cmpVisual.GetProjectileActor(); let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint(); if (visualActorLaunchPoint.length() > 0) launchPoint = visualActorLaunchPoint; } let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); let attackImpactSound = ""; let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (cmpSound) attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()); let data = { "type": type, "attackData": this.GetAttackEffectsData(type), "target": target, "attacker": this.entity, "attackerOwner": attackerOwner, "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "attackImpactSound": attackImpactSound, "splash": this.GetSplashData(type), + "friendlyFire": this.template[type].Projectile.FriendlyFire == "true", }; cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data); } else Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner); }; /** * Get the predicted time of collision between a projectile (or a chaser) * and its target, assuming they both move in straight line at a constant speed. * Vertical component of movement is ignored. * @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser). * @param {number} horizSpeed - the horizontal speed of the projectile (or chaser). * @param {Vector3D} targetPosition - the 3D position of the target. * @param {Vector3D} targetVelocity - the 3D velocity vector of the target. * @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen. */ Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity) { let relativePosition = new Vector3D.sub(targetPosition, selfPosition); let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed; let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z; let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z; // The predicted time to reach the target is the smallest non negative solution // (when it exists) of the equation a t^2 + 2 b t + c = 0. // Using c>=0, we can straightly compute the right solution. if (c == 0) return 0; let disc = b * b - a * c; if (a < 0 || b < 0 && disc >= 0) return c / (Math.sqrt(disc) - b); return false; }; Attack.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (!cmpUnitAI) return; if (this.GetAttackTypes().some(type => msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1)) cmpUnitAI.UpdateRangeQueries(); }; Attack.prototype.GetRangeOverlays = function() { if (!this.template.Ranged || !this.template.Ranged.RangeOverlay) return []; let range = this.GetRange("Ranged"); let rangeOverlays = []; for (let i in range) if ((i == "min" || i == "max") && range[i]) rangeOverlays.push({ "radius": range[i], "texture": this.template.Ranged.RangeOverlay.LineTexture, "textureMask": this.template.Ranged.RangeOverlay.LineTextureMask, "thickness": +this.template.Ranged.RangeOverlay.LineThickness, }); return rangeOverlays; }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/components/DelayedDamage.js (revision 23519) @@ -1,85 +1,88 @@ function DelayedDamage() {} DelayedDamage.prototype.Schema = ""; DelayedDamage.prototype.Init = function() { }; /** * Handles hit logic after the projectile travel time has passed. * @param {Object} data - The data sent by the caller. * @param {string} data.type - The type of damage. * @param {Object} data.attackData - Data of the form { 'effectType': { ...opaque effect data... }, 'Bonuses': {...} }. * @param {number} data.target - The entity id of the target. * @param {number} data.attacker - The entity id of the attacker. * @param {number} data.attackerOwner - The player id of the owner of the attacker. * @param {Vector2D} data.origin - The origin of the projectile hit. * @param {Vector3D} data.position - The expected position of the target. * @param {number} data.projectileId - The id of the projectile. * @param {Vector3D} data.direction - The unit vector defining the direction. * @param {string} data.attackImpactSound - The name of the sound emited on impact. + * @param {boolean} data.friendlyFire - A flag indicating whether allied entities can also be damaged. * ***When splash damage*** * @param {boolean} data.splash.friendlyFire - A flag indicating if allied entities are also damaged. * @param {number} data.splash.radius - The radius of the splash damage. * @param {string} data.splash.shape - The shape of the splash range. * @param {Object} data.splash.attackData - same as attackData, for splash. */ DelayedDamage.prototype.MissileHit = function(data, lateness) { if (!data.position) return; let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); if (cmpSoundManager && data.attackImpactSound) cmpSoundManager.PlaySoundGroupAtPosition(data.attackImpactSound, data.position); // Do this first in case the direct hit kills the target if (data.splash) { Attacking.CauseDamageOverArea({ "type": data.type, "attackData": data.splash.attackData, "attacker": data.attacker, "attackerOwner": data.attackerOwner, "origin": Vector2D.from3D(data.position), "radius": data.splash.radius, "shape": data.splash.shape, "direction": data.direction, "friendlyFire": data.splash.friendlyFire }); } let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); // Deal direct damage if we hit the main target // and if the target has Resistance (not the case for a mirage for example) if (Attacking.TestCollision(data.target, data.position, lateness)) { cmpProjectileManager.RemoveProjectile(data.projectileId); Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); return; } let targetPosition = Attacking.InterpolatedLocation(data.target, lateness); if (!targetPosition) return; - // If we didn't hit the main target look for nearby units - let cmpPlayer = QueryPlayerIDInterface(data.attackerOwner); - let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies()); + // If we didn't hit the main target look for nearby units. + let ents = Attacking.EntitiesNearPoint(Vector2D.from3D(data.position), + targetPosition.horizDistanceTo(data.position) * 2, + Attacking.GetPlayersToDamage(data.attackerOwner, data.friendlyFire)); + for (let ent of ents) { if (!Attacking.TestCollision(ent, data.position, lateness)) continue; Attacking.HandleAttackEffects(data.type, data.attackData, ent, data.attacker, data.attackerOwner); cmpProjectileManager.RemoveProjectile(data.projectileId); break; } }; Engine.RegisterSystemComponentType(IID_DelayedDamage, "DelayedDamage", DelayedDamage); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 23519) @@ -1,324 +1,325 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("Attack.js"); let entityID = 903; function attackComponentTest(defenderClass, isEnemy, test_function) { ResetState(); { let playerEnt1 = 5; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt1 }); AddMock(playerEnt1, IID_Player, { "GetPlayerID": () => 1, "IsEnemy": () => isEnemy }); } let attacker = entityID; AddMock(attacker, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { "GetOwner": () => 1 }); let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee": { "Damage": { "Hack": 11, "Pierce": 5, "Crush": 0 }, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen" }, "RestrictedClasses": { "_string": "Elephant Archer" }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 2 } } }, "Ranged": { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 }, "MinRange": 10, "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, "Projectile": { "Speed": 10, "Spread": 2, - "Gravity": 1 + "Gravity": 1, + "FriendlyFire": "false" }, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" }, "Splash": { "Shape": "Circular", "Range": 10, "FriendlyFire": "false", "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } } }, "Capture": { "Capture": 8, "MaxRange": 10, }, "Slaughter": {} }); let defender = ++entityID; AddMock(defender, IID_Identity, { "GetClassesList": () => [defenderClass], "HasClass": className => className == defenderClass }); AddMock(defender, IID_Ownership, { "GetOwner": () => 1 }); AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 }); AddMock(defender, IID_Health, { "GetHitpoints": () => 100 }); test_function(attacker, cmpAttack, defender); } // Validate template getter functions attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), { "Damage": { "Hack": 0, "Pierce": 10, "Crush": 0 } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged", true), { "Damage": { "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, "repeat": 1000 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashData("Ranged"), { "attackData": { "Damage": { "Hack": 0, "Pierce": 15, "Crush": 35, }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } }, "friendlyFire": false, "radius": 10, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2); TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null); let getAttackBonus = (s, t, e, splash) => GetAttackBonus(s, e, t, cmpAttack.GetAttackEffectsData(t, splash).Bonuses || null); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Ranged", defender, true), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus(attacker, "Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, bestAllyAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAllyAttack); }); } testGetBestAttackAgainst("FemaleCitizen", "Melee", undefined); testGetBestAttackAgainst("Archer", "Ranged", undefined); testGetBestAttackAgainst("Domestic", "Slaughter", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", "Capture", true); testGetBestAttackAgainst("Structure", "Ranged", undefined, false); function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { ResetState(); let cmpAttack = ConstructComponent(1, "Attack", {}); let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); if (timeToTarget === false) return; // Position of the target after that time. let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Time that the projectile need to reach it. let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed; TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1)); } testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Damage.js (revision 23519) @@ -1,575 +1,623 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Attacking.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AttackDetection.js"); Engine.LoadComponentScript("interfaces/DelayedDamage.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/Loot.js"); Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("DelayedDamage.js"); Engine.LoadComponentScript("Timer.js"); function Test_Generic() { ResetState(); let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer"); cmpTimer.OnUpdate({ "turnLength": 1 }); let attacker = 11; let atkPlayerEntity = 1; let attackerOwner = 6; let cmpAttack = ConstructComponent(attacker, "Attack", { "Ranged": { "Damage": { "Crush": 5, }, "MaxRange": 50, "MinRange": 0, "Delay": 0, "Projectile": { "Speed": 75.0, "Spread": 0.5, "Gravity": 9.81, + "FriendlyFire": "false", "LaunchPoint": { "@y": 3 } } } }); let damage = 5; let target = 21; let targetOwner = 7; let targetPos = new Vector3D(3, 0, 3); let type = "Melee"; let damageTaken = false; cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage }); let data = { "type": "Melee", "attackData": { "Damage": { "Hack": 0, "Pierce": 0, "Crush": damage }, }, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "position": targetPos, "projectileId": 9, "direction": new Vector3D(1, 0, 0) }; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [targetOwner] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => atkPlayerEntity, "GetAllPlayers": () => [0, 1, 2, 3, 4] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(target, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(target, IID_Health, { "TakeDamage": (effectData, __, ___, bonusMultiplier) => { damageTaken = true; return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush }; }, }); AddMock(SYSTEM_ENTITY, IID_DelayedDamage, { "MissileHit": () => { damageTaken = true; }, }); Engine.PostMessage = function(ent, iid, message) { TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [], "fromStatusEffect": false }, message); }; AddMock(target, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(attacker, IID_Ownership, { "GetOwner": () => attackerOwner, }); AddMock(attacker, IID_Position, { "GetPosition": () => new Vector3D(2, 0, 3), "GetRotation": () => new Vector3D(1, 2, 3), "IsInWorld": () => true, }); function TestDamage() { cmpTimer.OnUpdate({ "turnLength": 1 }); TS_ASSERT(damageTaken); damageTaken = false; } Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); data.type = "Ranged"; type = data.type; Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner); TestDamage(); // Check for damage still being dealt if the attacker dies cmpAttack.PerformAttack("Ranged", target); Engine.DestroyEntity(attacker); TestDamage(); atkPlayerEntity = 1; AddMock(atkPlayerEntity, IID_Player, { "GetEnemies": () => [2, 3] }); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]); TS_ASSERT_UNEVAL_EQUALS(Attacking.GetPlayersToDamage(atkPlayerEntity, false), [2, 3]); } Test_Generic(); function TestLinearSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const attacker = 50; const attackerOwner = 1; const origin = new Vector2D(0, 0); let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": attacker, "attackerOwner": attackerOwner, "origin": origin, "radius": 10, "shape": "Linear", "direction": new Vector3D(1, 747, 0), "friendlyFire": false, }; let fallOff = function(x, y) { return (1 - x * x / (data.radius * data.radius)) * (1 - 25 * y * y / (data.radius * data.radius)); }; let hitEnts = new Set(); AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(2.2, -0.4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(5, 2), }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(2.2, -0.4)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0, 0)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); data.direction = new Vector3D(0.6, 747, 0.8); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1, 2)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea(data); TS_ASSERT(hitEnts.has(60)); TS_ASSERT(hitEnts.has(61)); TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); } TestLinearSplashDamage(); function TestCircularSplashDamage() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; const radius = 10; let attackerOwner = 1; let fallOff = function(r) { return 1 - r * r / (radius * radius); }; AddMock(attackerOwner, IID_Player, { "GetEnemies": () => [2] }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => attackerOwner, "GetAllPlayers": () => [0, 1, 2] }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [60, 61, 62, 64], }); AddMock(60, IID_Position, { "GetPosition2D": () => new Vector2D(3, 4), }); AddMock(61, IID_Position, { "GetPosition2D": () => new Vector2D(0, 0), }); AddMock(62, IID_Position, { "GetPosition2D": () => new Vector2D(3.6, 3.2), }); AddMock(63, IID_Position, { "GetPosition2D": () => new Vector2D(10, -10), }); // Target on the frontier of the shape AddMock(64, IID_Position, { "GetPosition2D": () => new Vector2D(9, -4), }); AddMock(60, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(0)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(5)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100 * fallOff(1)); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(63, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT(false); } }); AddMock(64, IID_Resistance, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); Attacking.CauseDamageOverArea({ "type": "Ranged", "attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } }, "attacker": 50, "attackerOwner": attackerOwner, "origin": new Vector2D(3, 4), "radius": radius, "shape": "Circular", "friendlyFire": false, }); } TestCircularSplashDamage(); function Test_MissileHit() { ResetState(); Engine.PostMessage = (ent, iid, message) => {}; let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage"); let target = 60; let targetOwner = 1; let targetPos = new Vector3D(3, 10, 0); let hitEnts = new Set(); AddMock(SYSTEM_ENTITY, IID_Timer, { "GetLatestTurnLength": () => 500 }); const radius = 10; let data = { "type": "Ranged", "attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } }, "target": 60, "attacker": 70, "attackerOwner": 1, "position": targetPos, "direction": new Vector3D(1, 0, 0), "projectileId": 9, + "friendlyFire": "false", }; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id == 1 ? 10 : 11, "GetAllPlayers": () => [0, 1] }); AddMock(SYSTEM_ENTITY, IID_ProjectileManager, { "RemoveProjectile": () => {}, "LaunchProjectileAtPoint": (ent, pos, speed, gravity) => {}, }); AddMock(60, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.From(targetPos), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(60); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(60, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); AddMock(70, IID_Ownership, { "GetOwner": () => 1, }); AddMock(70, IID_Position, { "GetPosition": () => new Vector3D(0, 0, 0), "GetRotation": () => new Vector3D(0, 0, 0), "IsInWorld": () => true, }); AddMock(10, IID_Player, { "GetEnemies": () => [2] }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(60)); hitEnts.clear(); // The main target is not hit but another one is hit. AddMock(60, IID_Position, { "GetPosition": () => new Vector3D(900, 10, 0), "GetPreviousPosition": () => new Vector3D(900, 10, 0), "GetPosition2D": () => new Vector2D(900, 0), "IsInWorld": () => true, }); AddMock(60, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { TS_ASSERT_EQUALS(false); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); AddMock(61, IID_Position, { "GetPosition": () => targetPos, "GetPreviousPosition": () => targetPos, "GetPosition2D": () => Vector2D.from3D(targetPos), "IsInWorld": () => true, }); AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(61, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); hitEnts.clear(); // Add a splash damage. data.splash = {}; data.splash.friendlyFire = false; data.splash.radius = 10; data.splash.shape = "Circular"; data.splash.attackData = { "Damage": { "Hack": 0, "Pierce": 0, "Crush": 200 } }; AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61, 62] }); let dealtDamage = 0; AddMock(61, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(61); dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Position, { "GetPosition": () => new Vector3D(8, 10, 0), "GetPreviousPosition": () => new Vector3D(8, 10, 0), "GetPosition2D": () => new Vector2D(8, 0), "IsInWorld": () => true, }); AddMock(62, IID_Health, { "TakeDamage": (effectData, __, ___, mult) => { hitEnts.add(62); TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75); return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) }; } }); AddMock(62, IID_Footprint, { "GetShape": () => ({ "type": "circle", "radius": 20 }), }); cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 200); dealtDamage = 0; TS_ASSERT(hitEnts.has(62)); hitEnts.clear(); // Add some hard counters bonus. Engine.DestroyEntity(62); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "ExecuteQueryAroundPos": () => [61] }); let bonus= { "BonusCav": { "Classes": "Cavalry", "Multiplier": 400 } }; let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 10000 } }; AddMock(61, IID_Identity, { "GetClassesList": () => ["Cavalry"] }); data.attackData.Bonuses = bonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 200); dealtDamage = 0; hitEnts.clear(); data.splash.attackData.Bonuses = splashBonus; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 400 * 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = undefined; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = null; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); data.attackData.Bonuses = {}; cmpDelayedDamage.MissileHit(data, 0); TS_ASSERT(hitEnts.has(61)); TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200); dealtDamage = 0; hitEnts.clear(); + + // Test splash damage with friendly fire. + data.splash = {}; + data.splash.friendlyFire = true; + data.splash.radius = 10; + data.splash.shape = "Circular"; + data.splash.attackData = { "Damage": { "Pierce": 0, "Crush": 200 } }; + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ExecuteQueryAroundPos": () => [61, 62] + }); + + dealtDamage = 0; + AddMock(61, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { + hitEnts.add(61); + dealtDamage += mult * (effectData.Pierce + effectData.Crush); + return { "killed": false, "change": -mult * (effectData.Pierce + effectData.Crush) }; + } + }); + + AddMock(62, IID_Position, { + "GetPosition": () => new Vector3D(8, 10, 0), + "GetPreviousPosition": () => new Vector3D(8, 10, 0), + "GetPosition2D": () => new Vector2D(8, 0), + "IsInWorld": () => true, + }); + + AddMock(62, IID_Health, { + "TakeDamage": (effectData, __, ___, mult) => { + hitEnts.add(62); + TS_ASSERT_EQUALS(mult * (effectData.Pierce + effectData.Crush), 200 * 0.75); + return { "killed": false, "change": -mult * (effectData.Pierce + effectData.Crush) }; + } + }); + + AddMock(62, IID_Footprint, { + "GetShape": () => ({ "type": "circle", "radius": 20 }), + }); + + cmpDelayedDamage.MissileHit(data, 0); + TS_ASSERT(hitEnts.has(61)); + TS_ASSERT_EQUALS(dealtDamage, 100 + 2 * 200); + dealtDamage = 0; + TS_ASSERT(hitEnts.has(62)); + hitEnts.clear(); } Test_MissileHit(); Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_minor_test.xml (revision 23519) @@ -1,52 +1,53 @@ 0.0 25.0 0.0 50.0 1.0 1200 2000 75.0 + false 3 0.5 8.0 athen Market Settlement Minor Greek Polis This is a minor Greek city. -units/{civ}_support_female_citizen campaigns/army_mace_hero_alexander campaigns/army_mace_standard true 150 35000 campaigns/structures/hellenes/settlement_curtainwall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/campaigns/campaign_city_test.xml (revision 23519) @@ -1,53 +1,54 @@ 0.0 25.0 0.0 50.0 1.0 1200 2000 75.0 + false 5 0.5 8.0 athen Market Settlement Greek Polis This is a major Greek city. -units/{civ}_support_female_citizen campaigns/army_mace_hero_alexander campaigns/army_mace_standard units/{civ}_support_trader true 300 35000 campaigns/structures/hellenes/settlement_curtainwall.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_boltshooter.xml (revision 23519) @@ -1,89 +1,90 @@ 0.0 150.0 25.0 80.0 26.0 5000 6000 0 Linear 8.0 false 0.0 75.0 5.0 150.0 2.0 9.81 + false Human Siege 20 20 250 250 2 2.0 200 Ranged BoltShooter Bolt Shooter 200 0 10 0 10 circle/256x256.png circle/256x256_mask.png attack/impact/arrow_metal.xml attack/weapon/arrowfly.xml standground 0.9 120 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_tower.xml (revision 23519) @@ -1,90 +1,91 @@ 0.0 12.0 2.5 55.0 10.0 10 1200 2000 0 75.0 2.0 9.81 + false Human outline_border.png outline_border_mask.png 0.175 0 1 10 Infantry 40 500 300 20.0 20 0.1 Unit Support Infantry 0 2 500 Ranged SiegeTower Siege Tower Garrison units for transport and to increase firepower. circle/256x256.png circle/256x256_mask.png attack/siege/ram_move.xml attack/siege/ram_move.xml attack/impact/arrow_metal.xml attack/weapon/arrowfly.xml attack/siege/ram_trained.xml 12.0 0.7 80 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/theb_siege_fireraiser.xml (revision 23519) @@ -1,48 +1,49 @@ 50.0 0.0 50.0 12 8.0 2000 2000 0 10.0 2.0 9.81 + false Circular 20 true 200.0 200.0 200.0 4.5 Fire Raiser Pyrobolos units/hele_siege_lithobolos.png 60 units/thebans/siege_fireraiser.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/rome_army_camp.xml (revision 23519) @@ -1,129 +1,130 @@ 15 25 2 1 5 1 0.0 25.0 0.0 60.0 0.0 1200 2000 0 75.0 1.5 9.81 + false outline_border.png outline_border_mask.png 0.175 1 15 1 neutral enemy ArmyCamp ArmyCamp 80 1500 10.0 3.0 5 250 500 0 12.0 40 Support Infantry Cavalry Siege 1 6 2500 decay|rubble/rubble_rome_sb rome Army Camp Castra ArmyCamp ConquestCritical structures/roman_camp.png Build in neutral or enemy territory. Train Citizen-Soldiers and construct Siege Engines. Garrison Soldiers for additional arrows. 100 100 0.7 units/{civ}_infantry_swordsman_b units/{civ}_infantry_spearman_a units/{civ}_infantry_javelinist_b units/{civ}_cavalry_spearman_b units/{civ}_siege_ballista_packed units/{civ}_siege_scorpio_packed units/{civ}_siege_oxybeles_packed units/{civ}_siege_lithobolos_packed units/{civ}_siege_ram units/{civ}_siege_tower interface/complete/building/complete_broch.xml 37.5 60 structures/romans/camp.xml structures/fndn_8x8.xml 29.5 8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml (revision 23519) @@ -1,138 +1,139 @@ FemaleCitizen 140 190 100 5 5 5 15 3 0.0 12.0 0.0 72.0 0.0 1200 2000 0 75.0 1.5 9.81 + false Human outline_border.png outline_border_mask.png 0.175 3 1 own neutral CivilCentre CivilCentre 200 2500 5.0 20 500 0 500 500 500 8.0 20 0.1 Unit Support Infantry Cavalry 1 1 3000 decay|rubble/rubble_stone_6x6 Civic Center Build to acquire large tracts of territory. Train citizens. Defensive CivCentre CivilCentre structures/civic_centre.png 200 0 200 200 200 0.8 units/{civ}_support_female_citizen phase_town_{civ} phase_city_{civ} unlock_spies spy_counter food wood stone metal true interface/complete/building/complete_civ_center.xml interface/alarm/alarm_alert_0.xml interface/alarm/alarm_alert_1.xml true 140 10000 90 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_tower.xml (revision 23519) @@ -1,73 +1,74 @@ 0 0 0 1200 2000 0 75.0 1.5 39.81 + false Human outline_border.png outline_border_mask.png 0.175 1 1 Infantry DefenseTower DefenseTower 60 0.1 Unit Support Infantry 0 2 1000 decay|rubble/rubble_stone_2x2 Tower 0.7 interface/complete/building/complete_tower.xml 6.0 0.6 18.0 80 structures/fndn_3x3.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_defensive_wall_tower.xml (revision 23519) @@ -1,82 +1,83 @@ 0.0 8.0 0.0 72.0 12.0 1200 2000 0 75.0 1.5 39.81 + false Human outline_border.png outline_border_mask.png 0.175 0 1 Infantry 80 90 2 0.1 Unit Support Infantry 0 2 4000 decay|rubble/rubble_stone_wall_tower Wall Turret Shoots arrows. Garrison to defend a city wall against attackers. Tower structures/tower.png 0.8 pair_walls_01 interface/complete/building/complete_tower.xml 20.0 60 structures/fndn_3x3_tower.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_military_fortress.xml (revision 23519) @@ -1,108 +1,109 @@ 5 5 3 0.0 16.0 0.0 72.0 0.0 1200 2000 0 75.0 1.5 9.81 + false Human outline_border.png outline_border_mask.png 0.175 3 1 Fortress Fortress 80 4000 10.0 10 500 0 1000 8.0 20 0.075 Support Infantry Cavalry Siege 6 4200 decay|rubble/rubble_stone_6x6 Fortress Defensive Fortress GarrisonFortress City structures/fortress.png phase_city Train Champions and Heroes and research technologies. Garrison Soldiers for additional arrows. 100 0 65 0.8 attack_soldiers_will interface/complete/building/complete_fortress.xml 100 80 structures/fndn_8x8.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry_ranged.xml (revision 23519) @@ -1,34 +1,35 @@ 0 9.0 0 16.0 0.0 1000 1500 0 75.0 3.0 9.81 + false Human 100 Ranged Ranged Cavalry special/formations/skirmish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_archer.xml (revision 23519) @@ -1,41 +1,42 @@ 0 14.0 0 76.0 0.0 500 1000 0 75.0 1.0 39.81 + false Human 100 Ranged Archer Champion Cavalry Archer special/formations/skirmish actor/fauna/death/death_horse.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry_javelinist.xml (revision 23519) @@ -1,43 +1,44 @@ 0.0 36.0 0.0 32.0 0.0 750 1250 0 62.5 1.0 39.81 + false Human 100 Ranged Javelinist Champion Cavalry Skirmisher special/formations/skirmish actor/fauna/death/death_horse.xml attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_archer.xml (revision 23519) @@ -1,42 +1,43 @@ 0 6.5 0 76 0.0 300 500 0 75.0 1.0 39.81 + false Human 100 120 Ranged Archer Champion Archer special/formations/skirmish 1.2 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_infantry_javelinist.xml (revision 23519) @@ -1,48 +1,49 @@ 0 26.0 0 28.0 0.0 500 1000 0 62.5 1.0 39.81 + false Human 100 120 Ranged Javelinist Champion Skirmisher special/formations/skirmish attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml 1.75 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_archer.xml (revision 23519) @@ -1,31 +1,32 @@ 0 35 0 80.0 0.0 1200 2000 0 75.0 0.5 39.81 + false Human Ranged Archer Hero Cavalry Archer special/formations/skirmish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry_javelinist.xml (revision 23519) @@ -1,47 +1,48 @@ 0.0 60.0 0.0 36.0 0.0 750 1250 0 62.5 0.5 39.81 + false Human 3.0 1500 Ranged Javelinist special/formations/skirmish Hero Cavalry Skirmisher attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml 1.05 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_archer.xml (revision 23519) @@ -1,31 +1,32 @@ 0 8.0 0 80 0.0 200 300 0 75.0 0.5 39.81 + false Human Ranged Archer Hero Archer special/formations/skirmish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_infantry_javelinist.xml (revision 23519) @@ -1,37 +1,38 @@ 0 50.0 0 32.0 0.0 600 1000 0 62.5 0.5 39.81 + false Human Ranged Javelinist Hero Skirmisher special/formations/skirmish attack/weapon/javelin_attack.xml attack/impact/javelin_impact.xml Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_infantry_ranged.xml (revision 23519) @@ -1,39 +1,40 @@ 1 1 10 0 1.5 0 10.0 0.0 750 1250 0 75.0 3.0 9.81 + false Human 50 Ranged Ranged special/formations/skirmish Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_bireme.xml (revision 23519) @@ -1,75 +1,76 @@ 0.0 35.0 0.0 45.0 0.0 1000 2000 0 75.0 2.0 39.81 + false Ship Human 2 10 1 Infantry Cavalry 2 20 125 50 10.0 20 0 FemaleCitizen Infantry Healer Dog Support Infantry Cavalry Dog 0 10 true 800 Ranged Warship Bireme Light Warship phase_town Garrison units for transport and to increase firepower. 75 25 15 attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml 1.55 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_quinquereme.xml (revision 23519) @@ -1,88 +1,89 @@ 0.0 10.0 100.0 72.0 10.0 2000 5000 0 Circular 10 false 0.0 15.0 35.0 37.5 4.0 9.81 + false Ship Structure 1 10 1 StoneThrower 3 30 350 200 350 8.0 50 0 FemaleCitizen Infantry Healer Dog Support Infantry Cavalry Dog Siege Elephant 0 10 true 2000 Ranged Warship Quinquereme Heavy Warship phase_city Garrison units for transport and to increase firepower. 150 40 30 attack/siege/ballist_attack.xml 1.8 110 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_ship_trireme.xml (revision 23519) @@ -1,75 +1,76 @@ 0.0 35.0 0.0 55.0 0.0 1000 2000 0 75.0 2.0 39.81 + false Ship Human 3 13 1 Infantry Cavalry 3 25 150 150 8.0 30 0 FemaleCitizen Infantry Healer Dog Support Infantry Cavalry Dog Siege Elephant 0 10 true 1400 Ranged Warship Trireme Medium Warship phase_town Garrison units for transport and to increase firepower. 100 30 20 attack/impact/arrow_impact.xml attack/weapon/bow_attack.xml 1.8 Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_siege_stonethrower.xml (revision 23519) @@ -1,90 +1,91 @@ 0.0 10.0 100.0 80.0 26.0 6000 7000 0 37.5 4.0 9.81 + false props/units/weapons/rock_explosion.xml 0.1 Circular 10 false 0.0 15.0 35.0 Structure 20 25 400 250 4.5 250 Ranged StoneThrower Siege Catapult 300 0 20 10 0 square/256x256.png square/256x256_mask.png attack/impact/siegeprojectilehit.xml attack/siege/ballist_attack.xml standground 0.8 120 Index: ps/trunk/binaries/data/mods/public/simulation/templates/units/plane.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/units/plane.xml (revision 23518) +++ ps/trunk/binaries/data/mods/public/simulation/templates/units/plane.xml (revision 23519) @@ -1,82 +1,83 @@ 0.0 100.0 227.0 120 80 0 10000 0 75.0 1.0 9.81 + false 3 1 Infantry true 0.0 3.0 7.0 1 0 Support Infantry 1 5 100 true P-51 Mustang A World War 2 American fighter plane. units/global_mustang.png 1.0 true 0.0 3.0 60.0 50.0 40.0 25.0 5.0 10.0 2.0 2.0 50.0 15.0 true unrestricted 100 units/global/plane.xml