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