Generalise Attack effects. All attacks, including death damage and splash, can deal any number of attack effects (damaging, capture, giving status effects.)

This moves most of what was in the Damage system component to a helper,
and renames that component DelayedDamage.
It also introduces a new global script with all possible attack effects.

Comments Taken From: Freagarach, Stan, bb

Differential Revision: https://code.wildfiregames.com/D2092
This was SVN commit r22754.
This commit is contained in:
wraitii 2019-08-22 18:00:33 +00:00
parent cc1ea7cca0
commit 16b452cf91
36 changed files with 794 additions and 809 deletions

View File

@ -0,0 +1,16 @@
// TODO: could be worth putting this in json files someday
const g_EffectTypes = ["Damage", "Capture", "GiveStatus"];
const g_EffectReceiver = {
"Damage": {
"IID": "IID_Health",
"method": "TakeDamage"
},
"Capture": {
"IID": "IID_Capturable",
"method": "Capture"
},
"GiveStatus": {
"IID": "IID_StatusEffectsReceiver",
"method": "GiveStatus"
}
};

View File

@ -170,6 +170,22 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag
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);
}
// TODO: status effects
return effects;
};
if (template.Attack)
{
ret.attack = {};
@ -179,36 +195,27 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag
return getEntityValue("Attack/" + type + "/" + stat);
};
if (type == "Capture")
ret.attack.Capture = {
"value": getAttackStat("Value")
};
else
{
ret.attack[type] = {
"minRange": getAttackStat("MinRange"),
"maxRange": getAttackStat("MaxRange"),
"elevationBonus": getAttackStat("ElevationBonus"),
"damage": {}
};
for (let damageType in template.Attack[type].Damage)
ret.attack[type].damage[damageType] = getAttackStat("Damage/" + damageType);
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].elevationAdaptedRange = Math.sqrt(ret.attack[type].maxRange *
(2 * ret.attack[type].elevationBonus + ret.attack[type].maxRange));
}
ret.attack[type].repeatTime = getAttackStat("RepeatTime");
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,
"damage": {}
};
for (let damageType in template.Attack[type].Splash.Damage)
ret.attack[type].splash.damage[damageType] = getAttackStat("Splash/Damage/" + damageType);
Object.assign(ret.attack[type].splash, getAttackEffects(template.Attack[type].Splash, "Attack/" + type + "/Splash"));
}
}
}
@ -217,10 +224,9 @@ function GetTemplateDataHelper(template, player, auraTemplates, resources, damag
{
ret.deathDamage = {
"friendlyFire": template.DeathDamage.FriendlyFire != "false",
"damage": {}
};
for (let damageType in template.DeathDamage.Damage)
ret.deathDamage.damage[damageType] = getEntityValue("DeathDamage/Damage/" + damageType);
Object.assign(ret.deathDamage, getAttackEffects(template.DeathDamage, "DeathDamage"));
}
if (template.Auras && auraTemplates)

View File

@ -270,8 +270,8 @@ function getAttackTooltip(template)
"attackLabel": attackLabel,
"details":
type == "Capture" ?
template.attack.Capture.value :
damageTypesToText(template.attack[type].damage),
template.attack.Capture.Capture :
damageTypesToText(template.attack[type].Damage),
"rate": rate
}));
continue;
@ -283,7 +283,7 @@ function getAttackTooltip(template)
let relativeRange = realRange ? Math.round(realRange - maxRange) : 0;
tooltips.push(sprintf(g_RangeTooltipString[relativeRange ? "relative" : "non-relative"][minRange ? "minRange" : "no-minRange"], {
"attackLabel": attackLabel,
"damageTypes": damageTypesToText(template.attack[type].damage),
"damageTypes": damageTypesToText(template.attack[type].Damage),
"rangeLabel": headerFont(translate("Range:")),
"minRange": minRange,
"maxRange": maxRange,
@ -313,7 +313,7 @@ function getSplashDamageTooltip(template)
let splashDamageTooltip = sprintf(translate("%(label)s: %(value)s"), {
"label": headerFont(g_SplashDamageTypes[splash.shape]),
"value": damageTypesToText(splash.damage)
"value": damageTypesToText(splash.Damage)
});
if (g_AlwaysDisplayFriendlyFire || splash.friendlyFire)

View File

@ -37,7 +37,6 @@ Trigger.prototype.SpawnWolvesAndAttack = function()
let allTargets;
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
let players = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0).map((v, i) => i + 1);
for (let spawnPoint in attackers)
@ -52,7 +51,7 @@ Trigger.prototype.SpawnWolvesAndAttack = function()
continue;
// The returned entities are sorted by RangeManager already
let targets = cmpDamage.EntitiesNearPoint(attackerPos, 200, players).filter(ent => {
let targets = Attack.EntitiesNearPoint(attackerPos, 200, players).filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses);
});

View File

@ -152,7 +152,7 @@ Trigger.prototype.InitStartingUnits = function()
{
this.playerCivicCenter[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "CivilCentre")[0];
this.treasureFemale[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "FemaleCitizen")[0];
Engine.QueryInterface(this.treasureFemale[playerID], IID_DamageReceiver).SetInvulnerability(true);
Engine.QueryInterface(this.treasureFemale[playerID], IID_Resistance).SetInvulnerability(true);
}
};

View File

@ -20,8 +20,8 @@ Trigger.prototype.InitCaptureTheRelic = function()
{
this.relics[i] = TriggerHelper.SpawnUnits(pickRandom(potentialSpawnPoints), catafalqueTemplates[i], 1, 0)[0];
let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
let cmpResistance = Engine.QueryInterface(this.relics[i], IID_Resistance);
cmpResistance.SetInvulnerability(true);
let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position);
cmpPositionRelic.SetYRotation(randomAngle());

View File

@ -246,7 +246,7 @@ m.Template = m.Class({
if (!this.get("Attack/Capture"))
return undefined;
return +this.get("Attack/Capture/Value") || 0;
return +this.get("Attack/Capture/Capture") || 0;
},
"attackTimes": function(type) {

View File

@ -260,9 +260,9 @@ AIProxy.prototype.GetFullRepresentation = function()
ret.hitpoints = cmpHealth.GetHitpoints();
}
let cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
if (cmpDamageReceiver)
ret.invulnerability = cmpDamageReceiver.IsInvulnerable();
let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
if (cmpResistance)
ret.invulnerability = cmpResistance.IsInvulnerable();
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)

View File

@ -1,5 +1,15 @@
function Armour() {}
Armour.prototype.DamageResistanceSchema = "" +
"<oneOrMore>" +
"<element a:help='Resistance against any number of damage types'>" +
"<anyName>" +
"<except><name>Foundation</name></except>" +
"</anyName>" +
"<ref name='nonNegativeDecimal' />" +
"</element>" +
"</oneOrMore>";
Armour.prototype.Schema =
"<a:help>Controls the damage resistance of the unit.</a:help>" +
"<a:example>" +
@ -7,12 +17,10 @@ Armour.prototype.Schema =
"<Pierce>0.0</Pierce>" +
"<Crush>5.0</Crush>" +
"</a:example>" +
BuildDamageTypesSchema("damage protection") +
Armour.prototype.DamageResistanceSchema +
"<optional>" +
"<element name='Foundation' a:help='Armour given to building foundations'>" +
"<interleave>" +
BuildDamageTypesSchema("damage protection") +
"</interleave>" +
Armour.prototype.DamageResistanceSchema +
"</element>" +
"</optional>";
@ -32,32 +40,7 @@ Armour.prototype.SetInvulnerability = function(invulnerability)
Engine.PostMessage(this.entity, MT_InvulnerabilityChanged, { "entity": this.entity, "invulnerability": invulnerability });
};
/**
* Take damage according to the entity's armor.
* @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that.
* @param {number} multiplier - the damage multiplier.
* Returns object of the form { "killed": false, "change": -12 }.
*/
Armour.prototype.TakeDamage = function(strengths, multiplier = 1)
{
if (this.invulnerable)
return { "killed": false, "change": 0 };
// Adjust damage values based on armour; exponential armour: damage = attack * 0.9^armour
var armourStrengths = this.GetArmourStrengths();
// Total is sum of individual damages
// Don't bother rounding, since HP is no longer integral.
var total = 0;
for (let type in strengths)
total += strengths[type] * multiplier * Math.pow(0.9, armourStrengths[type] || 0);
// Reduce health
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
return cmpHealth.Reduce(total);
};
Armour.prototype.GetArmourStrengths = function()
Armour.prototype.GetArmourStrengths = function(effectType)
{
// Work out the armour values with technology effects.
let applyMods = (type, foundation) => {
@ -70,12 +53,16 @@ Armour.prototype.GetArmourStrengths = function()
else
strength = +this.template[type];
return ApplyValueModificationsToEntity("Armour/" + type, strength, this.entity);
return ApplyValueModificationsToEntity("Armour/" + effectType + "/" + type, strength, this.entity);
};
let foundation = Engine.QueryInterface(this.entity, IID_Foundation) && this.template.Foundation;
let ret = {};
if (effectType != "Damage")
return ret;
for (let damageType in this.template)
if (damageType != "Foundation")
ret[damageType] = applyMods(damageType, foundation);
@ -83,4 +70,4 @@ Armour.prototype.GetArmourStrengths = function()
return ret;
};
Engine.RegisterComponentType(IID_DamageReceiver, "Armour", Armour);
Engine.RegisterComponentType(IID_Resistance, "Armour", Armour);

View File

@ -2,40 +2,6 @@ function Attack() {}
var g_AttackTypes = ["Melee", "Ranged", "Capture"];
Attack.prototype.statusEffectsSchema =
"<optional>" +
"<element name='StatusEffects' a:help='Effects like poisioning or burning a unit.'>" +
"<oneOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Interval' a:help='Interval between the occurances of the effect.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Damage' a:help='Damage caused by the effect.'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
"</element>" +
"</optional>";
Attack.prototype.bonusesSchema =
"<optional>" +
"<element name='Bonuses'>" +
"<zeroOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<optional>" +
"<element name='Civ' a:help='If an entity has this civ then the bonus is applied'><text/></element>" +
"</optional>" +
"<element name='Classes' a:help='If an entity has all these classes then the bonus is applied'><text/></element>" +
"<element name='Multiplier' a:help='The attackers attack strength is multiplied by this'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</zeroOrMore>" +
"</element>" +
"</optional>";
Attack.prototype.preferredClassesSchema =
"<optional>" +
"<element name='PreferredClasses' a:help='Space delimited list of classes preferred for attacking. If an entity has any of theses classes, it is preferred. The classes are in decending order of preference'>" +
@ -130,9 +96,7 @@ Attack.prototype.Schema =
"<optional>" +
"<element name='Melee'>" +
"<interleave>" +
"<element name='Damage'>" +
BuildDamageTypesSchema("damage strength") +
"</element>" +
Attacking.BuildAttackEffectsSchema() +
"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='PrepareTime' a:help='Time from the start of the attack command until the attack actually occurs (in milliseconds). This value relative to RepeatTime should closely match the \"event\" point in the actor&apos;s attack animation'>" +
"<data type='nonNegativeInteger'/>" +
@ -140,7 +104,6 @@ Attack.prototype.Schema =
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + // TODO: it shouldn't be stretched
"<data type='positiveInteger'/>" +
"</element>" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"</interleave>" +
@ -149,9 +112,7 @@ Attack.prototype.Schema =
"<optional>" +
"<element name='Ranged'>" +
"<interleave>" +
"<element name='Damage'>" +
BuildDamageTypesSchema("damage strength") +
"</element>" +
Attacking.BuildAttackEffectsSchema() +
"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" +
"<optional>"+
@ -179,10 +140,7 @@ Attack.prototype.Schema =
"<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
"<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
"<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
"<element name='Damage'>" +
BuildDamageTypesSchema("damage strength") +
"</element>" +
Attack.prototype.bonusesSchema +
Attacking.BuildAttackEffectsSchema() +
"</interleave>" +
"</element>" +
"</optional>" +
@ -217,8 +175,6 @@ Attack.prototype.Schema =
"</optional>" +
"</interleave>" +
"</element>" +
Attack.prototype.statusEffectsSchema +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"</interleave>" +
@ -227,12 +183,11 @@ Attack.prototype.Schema =
"<optional>" +
"<element name='Capture'>" +
"<interleave>" +
"<element name='Value' a:help='Capture points value'><ref name='nonNegativeDecimal'/></element>" +
Attacking.BuildAttackEffectsSchema() +
"<element name='MaxRange' a:help='Maximum attack range (in meters)'><ref name='nonNegativeDecimal'/></element>" +
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + // TODO: it shouldn't be stretched
"<data type='positiveInteger'/>" +
"</element>" +
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"</interleave>" +
@ -241,11 +196,8 @@ Attack.prototype.Schema =
"<optional>" +
"<element name='Slaughter' a:help='A special attack to kill domestic animals'>" +
"<interleave>" +
"<element name='Damage'>" +
BuildDamageTypesSchema("damage strength") +
"</element>" +
Attacking.BuildAttackEffectsSchema() +
"<element name='MaxRange'><ref name='nonNegativeDecimal'/></element>" + // TODO: how do these work?
Attack.prototype.bonusesSchema +
Attack.prototype.preferredClassesSchema +
Attack.prototype.restrictedClassesSchema +
"</interleave>" +
@ -387,6 +339,14 @@ Attack.prototype.GetFullAttackRange = function()
return ret;
};
Attack.prototype.GetAttackEffectsData = function(type, splash)
{
let tp = this.template[type];
if (splash)
tp = tp.Splash;
return Attacking.GetAttackEffectsData("Attack/" + type + splash ? "/Splash" : "", tp, this.entity);
};
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture)
{
let cmpFormation = Engine.QueryInterface(target, IID_Formation);
@ -448,40 +408,16 @@ Attack.prototype.GetTimers = function(type)
return { "prepare": prepare, "repeat": repeat };
};
Attack.prototype.GetAttackStrengths = function(type)
{
// Work out the attack values with technology effects
let template = this.template[type];
let splash = "";
if (!template)
{
template = this.template[type.split(".")[0]].Splash;
splash = "/Splash";
}
let applyMods = damageType =>
ApplyValueModificationsToEntity("Attack/" + type + splash + "/Damage/" + damageType, +(template.Damage[damageType] || 0), this.entity);
if (type == "Capture")
return { "value": ApplyValueModificationsToEntity("Attack/Capture/Value", +(template.Value || 0), this.entity) };
let ret = {};
for (let damageType in template.Damage)
ret[damageType] = applyMods(damageType);
return ret;
};
Attack.prototype.GetSplashDamage = function(type)
{
if (!this.template[type].Splash)
return false;
let splash = {};
splash.damage = this.GetAttackStrengths(type + ".Splash");
splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
splash.shape = this.template[type].Splash.Shape;
return splash;
return {
"attackData": this.GetAttackEffectsData(type, true),
"friendlyFire": this.template[type].Splash.FriendlyFire != "false",
"shape": this.template[type].Splash.Shape,
};
};
Attack.prototype.GetRange = function(type)
@ -498,15 +434,6 @@ Attack.prototype.GetRange = function(type)
return { "max": max, "min": min, "elevationBonus": elevationBonus };
};
Attack.prototype.GetBonusTemplate = function(type)
{
let template = this.template[type];
if (!template)
template = this.template[type.split(".")[0]].Splash;
return template.Bonuses || null;
};
/**
* 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
@ -515,7 +442,6 @@ Attack.prototype.GetBonusTemplate = function(type)
Attack.prototype.PerformAttack = function(type, target)
{
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner();
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
// If this is a ranged attack, then launch a projectile
if (type == "Ranged")
@ -528,7 +454,7 @@ Attack.prototype.PerformAttack = function(type, target)
let horizSpeed = +this.template[type].Projectile.Speed;
let gravity = +this.template[type].Projectile.Gravity;
//horizSpeed /= 2; gravity /= 2; // slow it down for testing
// horizSpeed /= 2; gravity /= 2; // slow it down for testing
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
@ -575,7 +501,7 @@ Attack.prototype.PerformAttack = function(type, target)
// TODO: Use unit rotation to implement x/z offsets.
let deltaLaunchPoint = new Vector3D(0, this.template[type].Projectile.LaunchPoint["@y"], 0.0);
let launchPoint = Vector3D.add(selfPosition, deltaLaunchPoint);
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
@ -598,66 +524,26 @@ Attack.prototype.PerformAttack = function(type, target)
let data = {
"type": type,
"attacker": this.entity,
"attackData": this.GetAttackEffectsData(type),
"target": target,
"strengths": this.GetAttackStrengths(type),
"attacker": this.entity,
"attackerOwner": attackerOwner,
"position": realTargetPosition,
"direction": missileDirection,
"projectileId": id,
"bonus": this.GetBonusTemplate(type),
"isSplash": false,
"attackerOwner": attackerOwner,
"attackImpactSound": attackImpactSound,
"statusEffects": this.template[type].StatusEffects
"attackImpactSound": attackImpactSound
};
if (this.template[type].Splash)
{
data.friendlyFire = this.template[type].Splash.FriendlyFire != "false";
data.radius = +this.template[type].Splash.Range;
data.shape = this.template[type].Splash.Shape;
data.isSplash = true;
data.splashStrengths = this.GetAttackStrengths(type + ".Splash");
data.splashBonus = this.GetBonusTemplate(type + ".Splash");
}
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +this.template[type].Delay, data);
}
else if (type == "Capture")
{
if (attackerOwner == INVALID_PLAYER)
return;
let multiplier = GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type));
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.GetHitpoints() == 0)
return;
multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
let cmpCapturable = Engine.QueryInterface(target, IID_Capturable);
if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner))
return;
let strength = this.GetAttackStrengths("Capture").value * multiplier;
if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target))
Engine.PostMessage(target, MT_Attacked, {
"attacker": this.entity,
"target": target,
"type": type,
"damage": strength,
"attackerOwner": attackerOwner
});
data.splash = {
"friendlyFire": this.template[type].Splash.FriendlyFire != "false",
"radius": +this.template[type].Splash.Range,
"shape": this.template[type].Splash.Shape,
"attackData": this.GetAttackEffectsData(type, true),
};
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data);
}
else
{
// Melee attack - hurt the target immediately
cmpDamage.CauseDamage({
"strengths": this.GetAttackStrengths(type),
"target": target,
"attacker": this.entity,
"multiplier": GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)),
"type": type,
"attackerOwner": attackerOwner
});
}
Attacking.HandleAttackEffects(type, this.GetAttackEffectsData(type), target, this.entity, attackerOwner);
};
/**

View File

@ -131,7 +131,7 @@ BuildingAI.prototype.SetupRangeQuery = function()
var range = cmpAttack.GetRange(attackType);
this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
this.entity, range.min, range.max, range.elevationBonus,
enemies, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
};

View File

@ -48,6 +48,24 @@ Capturable.prototype.SetCapturePoints = function(capturePointsArray)
this.cp = capturePointsArray;
};
Capturable.prototype.Capture = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
let cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
if (attackerOwner == INVALID_PLAYER || !this.CanCapture(attackerOwner) ||
!cmpHealth || cmpHealth.GetHitpoints() == 0)
return {};
bonusMultiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints());
let total = Attacking.GetTotalAttackEffects({ "Capture": effectData }, "Capture") * bonusMultiplier;
let change = this.Reduce(total, attackerOwner);
// TODO: implement loot
return { "captureChange": change };
};
/**
* Reduces the amount of capture points of an entity,
* in favour of the player of the source

View File

@ -1,312 +0,0 @@
function Damage() {}
Damage.prototype.Schema =
"<a:component type='system'/><empty/>";
Damage.prototype.Init = function()
{
};
/**
* Gives the position of the given entity, taking the lateness into account.
* @param {number} ent - Entity id of the entity we are finding the location for.
* @param {number} lateness - The time passed since the expected time to fire the function.
* @return {Vector3D} The location of the entity.
*/
Damage.prototype.InterpolatedLocation = function(ent, lateness)
{
let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
return undefined;
let curPos = cmpTargetPosition.GetPosition();
let prevPos = cmpTargetPosition.GetPreviousPosition();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength();
return new Vector3D(
(curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
0,
(curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength);
};
/**
* Test if a point is inside of an entity's footprint.
* @param {number} ent - Id of the entity we are checking with.
* @param {Vector3D} point - The point we are checking with.
* @param {number} lateness - The time passed since the expected time to fire the function.
* @return {boolean} True if the point is inside of the entity's footprint.
*/
Damage.prototype.TestCollision = function(ent, point, lateness)
{
let targetPosition = this.InterpolatedLocation(ent, lateness);
if (!targetPosition)
return false;
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (!cmpFootprint)
return false;
let targetShape = cmpFootprint.GetShape();
if (!targetShape)
return false;
if (targetShape.type == "circle")
return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;
if (targetShape.type == "square")
{
let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
}
warn("TestCollision called with an invalid footprint shape");
return false;
};
/**
* Get the list of players affected by the damage.
* @param {number} attackerOwner - The player id of the attacker.
* @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
* @return {number[]} The ids of players need to be damaged.
*/
Damage.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
{
if (!friendlyFire)
return QueryPlayerIDInterface(attackerOwner).GetEnemies();
return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
};
/**
* Handles hit logic after the projectile travel time has passed.
* @param {Object} data - The data sent by the caller.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.target - The entity id of the target.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
* @param {string} data.type - The type of damage.
* @param {number} data.attackerOwner - The player id of the owner of the attacker.
* @param {boolean} data.isSplash - A flag indicating if it's splash damage.
* @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 {Object} data.bonus - The attack bonus template from the attacker.
* @param {string} data.attackImpactSound - The name of the sound emited on impact.
* @param {Object} data.statusEffects - Status effects eg. poisoning, burning etc.
* ***When splash damage***
* @param {boolean} data.friendlyFire - A flag indicating if allied entities are also damaged.
* @param {number} data.radius - The radius of the splash damage.
* @param {string} data.shape - The shape of the splash range.
* @param {Object} data.splashBonus - The attack bonus template from the attacker.
* @param {Object} data.splashStrengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
*/
Damage.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.isSplash)
{
this.CauseDamageOverArea({
"attacker": data.attacker,
"origin": Vector2D.from3D(data.position),
"radius": data.radius,
"shape": data.shape,
"strengths": data.splashStrengths,
"splashBonus": data.splashBonus,
"direction": data.direction,
"playersToDamage": this.GetPlayersToDamage(data.attackerOwner, data.friendlyFire),
"type": data.type,
"attackerOwner": data.attackerOwner
});
}
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager);
// Deal direct damage if we hit the main target
// and if the target has DamageReceiver (not the case for a mirage for example)
let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
if (cmpDamageReceiver && this.TestCollision(data.target, data.position, lateness))
{
data.multiplier = GetDamageBonus(data.attacker, data.target, data.type, data.bonus);
this.CauseDamage(data);
cmpProjectileManager.RemoveProjectile(data.projectileId);
let cmpStatusReceiver = Engine.QueryInterface(data.target, IID_StatusEffectsReceiver);
if (cmpStatusReceiver && data.statusEffects)
cmpStatusReceiver.InflictEffects(data.statusEffects);
return;
}
let targetPosition = this.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 = this.EntitiesNearPoint(Vector2D.from3D(data.position), targetPosition.horizDistanceTo(data.position) * 2, cmpPlayer.GetEnemies());
for (let ent of ents)
{
if (!this.TestCollision(ent, data.position, lateness))
continue;
this.CauseDamage({
"strengths": data.strengths,
"target": ent,
"attacker": data.attacker,
"multiplier": GetDamageBonus(data.attacker, ent, data.type, data.bonus),
"type": data.type,
"attackerOwner": data.attackerOwner
});
cmpProjectileManager.RemoveProjectile(data.projectileId);
break;
}
};
/**
* Damages units around a given origin.
* @param {Object} data - The data sent by the caller.
* @param {number} data.attacker - The entity id of the attacker.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {number} data.radius - The radius of the splash damage.
* @param {string} data.shape - The shape of the radius.
* @param {Object} data.strengths - Data of the form { 'hack': number, 'pierce': number, 'crush': number }.
* @param {string} data.type - The type of damage.
* @param {number} data.attackerOwner - The player id of the attacker.
* @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
* @param {Object} data.splashBonus - The attack bonus template from the attacker.
* @param {number[]} data.playersToDamage - The array of player id's to damage.
*/
Damage.prototype.CauseDamageOverArea = function(data)
{
// Get nearby entities and define variables
let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
let damageMultiplier = 1;
// Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
for (let ent of nearEnts)
{
let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius);
else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
{
// Get position of entity relative to splash origin.
let relativePos = entityPosition.sub(data.origin);
// Get the position relative to the missile direction.
let direction = Vector2D.from3D(data.direction);
let parallelPos = relativePos.dot(direction);
let perpPos = relativePos.cross(direction);
// The width of linear splash is one fifth of the normal splash radius.
let width = data.radius / 5;
// Check that the unit is within the distance splash width of the line starting at the missile's
// landing point which extends in the direction of the missile for length splash radius.
if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
(1 - perpPos * perpPos / (width * width));
else
damageMultiplier = 0;
}
else // In case someone calls this function with an invalid shape.
{
warn("The " + data.shape + " splash damage shape is not implemented!");
}
if (data.splashBonus)
damageMultiplier *= GetDamageBonus(data.attacker, ent, data.type, data.splashBonus);
// Call CauseDamage which reduces the hitpoints, posts network command, plays sounds....
this.CauseDamage({
"strengths": data.strengths,
"target": ent,
"attacker": data.attacker,
"multiplier": damageMultiplier,
"type": data.type + ".Splash",
"attackerOwner": data.attackerOwner
});
}
};
/**
* Causes damage on a given unit.
* @param {Object} data - The data passed by the caller.
* @param {Object} data.strengths - Data in the form of { 'hack': number, 'pierce': number, 'crush': number }.
* @param {number} data.target - The entity id of the target.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.multiplier - The damage multiplier.
* @param {string} data.type - The type of damage.
* @param {number} data.attackerOwner - The player id of the attacker.
*/
Damage.prototype.CauseDamage = function(data)
{
let cmpDamageReceiver = Engine.QueryInterface(data.target, IID_DamageReceiver);
if (!cmpDamageReceiver)
return;
let targetState = cmpDamageReceiver.TakeDamage(data.strengths, data.multiplier);
let cmpPromotion = Engine.QueryInterface(data.attacker, IID_Promotion);
let cmpLoot = Engine.QueryInterface(data.target, IID_Loot);
let cmpHealth = Engine.QueryInterface(data.target, IID_Health);
if (cmpPromotion && cmpLoot && cmpLoot.GetXp() > 0)
cmpPromotion.IncreaseXp(cmpLoot.GetXp() * -targetState.change / cmpHealth.GetMaxHitpoints());
if (targetState.killed)
this.TargetKilled(data.attacker, data.target, data.attackerOwner);
Engine.PostMessage(data.target, MT_Attacked, { "attacker": data.attacker, "target": data.target, "type": data.type, "damage": -targetState.change, "attackerOwner": data.attackerOwner });
};
/**
* Gets entities near a give point for given players.
* @param {Vector2D} origin - The point to check around.
* @param {number} radius - The radius around the point to check.
* @param {number[]} players - The players of which we need to check entities.
* @return {number[]} The id's of the entities in range of the given point.
*/
Damage.prototype.EntitiesNearPoint = function(origin, radius, players)
{
// If there is insufficient data return an empty array.
if (!origin || !radius || !players)
return [];
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_DamageReceiver);
};
/**
* Called when a unit kills something (another unit, building, animal etc).
* @param {number} attacker - The entity id of the killer.
* @param {number} target - The entity id of the target.
* @param {number} attackerOwner - The player id of the attacker.
*/
Damage.prototype.TargetKilled = function(attacker, target, attackerOwner)
{
let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
// Add to killer statistics.
let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker);
if (cmpKillerPlayerStatisticsTracker)
cmpKillerPlayerStatisticsTracker.KilledEntity(target);
// Add to loser statistics.
let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker);
if (cmpTargetPlayerStatisticsTracker)
cmpTargetPlayerStatisticsTracker.LostEntity(target);
// If killer can collect loot, let's try to collect it.
let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
if (cmpLooter)
cmpLooter.Collect(target);
};
Engine.RegisterSystemComponentType(IID_Damage, "Damage", Damage);

View File

@ -33,10 +33,7 @@ DeathDamage.prototype.Schema =
"<element name='Shape' a:help='Shape of the splash damage, can be circular'><text/></element>" +
"<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
"<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
"<element name='Damage'>" +
BuildDamageTypesSchema("damage strength") +
"</element>" +
DeathDamage.prototype.bonusesSchema;
Attacking.BuildAttackEffectsSchema();
DeathDamage.prototype.Init = function()
{
@ -44,22 +41,9 @@ DeathDamage.prototype.Init = function()
DeathDamage.prototype.Serialize = null; // we have no dynamic state to save
DeathDamage.prototype.GetDeathDamageStrengths = function()
DeathDamage.prototype.GetDeathDamageEffects = function()
{
// Work out the damage values with technology effects
let applyMods = damageType =>
ApplyValueModificationsToEntity("DeathDamage/Damage/" + damageType, +(this.template.Damage[damageType] || 0), this.entity);
let ret = {};
for (let damageType in this.template.Damage)
ret[damageType] = applyMods(damageType);
return ret;
};
DeathDamage.prototype.GetBonusTemplate = function()
{
return this.template.Bonuses || null;
return Attacking.GetAttackEffectsData("DeathDamage", this.template, this.entity);
};
DeathDamage.prototype.CauseDeathDamage = function()
@ -74,21 +58,19 @@ DeathDamage.prototype.CauseDeathDamage = function()
if (owner == INVALID_PLAYER)
warn("Unit causing death damage does not have any owner.");
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire);
let playersToDamage = Attacking.GetPlayersToDamage(owner, this.template.FriendlyFire);
let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity);
cmpDamage.CauseDamageOverArea({
Attacking.CauseDamageOverArea({
"type": "Death",
"attackData": this.GetDeathDamageEffects(),
"attacker": this.entity,
"attackerOwner": owner,
"origin": pos,
"radius": radius,
"shape": this.template.Shape,
"strengths": this.GetDeathDamageStrengths(),
"splashBonus": this.GetBonusTemplate(),
"playersToDamage": playersToDamage,
"type": "Death",
"attackerOwner": owner
});
};

View File

@ -0,0 +1,85 @@
function DelayedDamage() {}
DelayedDamage.prototype.Schema =
"<a:component type='system'/><empty/>";
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.
* ***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,
"playersToDamage": Attacking.GetPlayersToDamage(data.attackerOwner, 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());
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);

View File

@ -389,12 +389,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
for (let type of types)
{
ret.attack[type] = {};
if (type == "Capture")
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
else
ret.attack[type].damage = cmpAttack.GetAttackStrengths(type);
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].splash = cmpAttack.GetSplashDamage(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
@ -432,9 +432,9 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
let cmpArmour = Engine.QueryInterface(ent, IID_Resistance);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
ret.armour = cmpArmour.GetArmourStrengths("Damage");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)

View File

@ -177,9 +177,34 @@ Health.prototype.Kill = function()
this.Reduce(this.hitpoints);
};
/**
* Take damage according to the entity's resistance.
* @param {Object} strengths - { "hack": number, "pierce": number, "crush": number } or something like that.
* @param {number} bonusMultiplier - the damage multiplier.
* Returns object of the form { "killed": false, "change": -12 }.
*/
Health.prototype.TakeDamage = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
if (cmpResistance && cmpResistance.IsInvulnerable())
return { "killed": false };
let total = Attacking.GetTotalAttackEffects(effectData, "Damage", cmpResistance) * bonusMultiplier;
// Reduce health
let change = this.Reduce(total);
let cmpLoot = Engine.QueryInterface(this.entity, IID_Loot);
if (cmpLoot && cmpLoot.GetXp() > 0 && change.HPchange < 0)
change.xp = cmpLoot.GetXp() * -change.HPchange / this.GetMaxHitpoints();
return change;
};
/**
* @param {number} amount - The amount of hitpoints to substract. Kills the entity if required.
* @return {{killed:boolean, change:number}} - Number of health points lost and whether the entity was killed.
* @return {{killed:boolean, HPchange:number}} - Number of health points lost and whether the entity was killed.
*/
Health.prototype.Reduce = function(amount)
{
@ -188,7 +213,7 @@ Health.prototype.Reduce = function(amount)
// might get called multiple times)
// Likewise if the amount is 0.
if (!amount || !this.hitpoints)
return { "killed": false, "change": 0 };
return { "killed": false, "HPchange": 0 };
// Before changing the value, activate Fogging if necessary to hide changes
let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
@ -202,7 +227,7 @@ Health.prototype.Reduce = function(amount)
this.hitpoints = 0;
this.RegisterHealthChanged(oldHitpoints);
this.HandleDeath();
return { "killed": true, "change": -oldHitpoints };
return { "killed": true, "HPchange": -oldHitpoints };
}
// If we are not marked as injured, do it now
@ -215,7 +240,7 @@ Health.prototype.Reduce = function(amount)
this.hitpoints -= amount;
this.RegisterHealthChanged(oldHitpoints);
return { "killed": false, "change": this.hitpoints - oldHitpoints };
return { "killed": false, "HPchange": this.hitpoints - oldHitpoints };
};
/**

View File

@ -5,13 +5,18 @@ StatusEffectsReceiver.prototype.Init = function()
this.activeStatusEffects = {};
};
StatusEffectsReceiver.prototype.InflictEffects = function(statusEffects)
// Called by attacking effects.
StatusEffectsReceiver.prototype.GiveStatus = function(effectData, attacker, attackerOwner, bonusMultiplier)
{
for (let effect in statusEffects)
this.InflictEffect(effect, statusEffects[effect]);
for (let effect in effectData)
this.AddStatus(effect, effectData[effect]);
// TODO: implement loot / resistance.
return { "inflictedStatuses": Object.keys(effectData) };
};
StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data)
StatusEffectsReceiver.prototype.AddStatus = function(statusName, data)
{
if (this.activeStatusEffects[statusName])
return;
@ -28,7 +33,7 @@ StatusEffectsReceiver.prototype.InflictEffect = function(statusName, data)
status.timer = cmpTimer.SetInterval(this.entity, IID_StatusEffectsReceiver, "ExecuteEffect", 0, +status.interval, statusName);
};
StatusEffectsReceiver.prototype.RemoveEffect = function(statusName) {
StatusEffectsReceiver.prototype.RemoveStatus = function(statusName) {
if (!this.activeStatusEffects[statusName])
return;
@ -51,19 +56,10 @@ StatusEffectsReceiver.prototype.ExecuteEffect = function(statusName, lateness)
else
status.timeElapsed += status.interval + lateness;
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage);
cmpDamage.CauseDamage({
"strengths": { [statusName]: status.damage },
"target": this.entity,
"attacker": -1,
"multiplier": 1,
"type": statusName,
"attackerOwner": -1
});
Attacking.HandleAttackEffects(statusName, { "Damage": { [statusName]: status.damage } }, this.entity, -1, -1);
if (status.timeElapsed >= status.duration)
this.RemoveEffect(statusName);
this.RemoveStatus(statusName);
};
Engine.RegisterComponentType(IID_StatusEffectsReceiver, "StatusEffectsReceiver", StatusEffectsReceiver);

View File

@ -2958,8 +2958,8 @@ UnitAI.prototype.UnitFsmSpec = {
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
cmpResistance.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(2800, 2800);
return false;
@ -2968,8 +2968,8 @@ UnitAI.prototype.UnitFsmSpec = {
"leave": function() {
this.StopTimer();
this.ResetAnimation();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
var cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance);
cmpResistance.SetInvulnerability(false);
},
"Timer": function(msg) {
@ -3466,7 +3466,7 @@ UnitAI.prototype.SetupRangeQuery = function(enable = true)
var players = cmpPlayer.GetEnemies();
var range = this.GetQueryRange(IID_Attack);
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, cmpRangeManager.GetEntityFlagMask("normal"));
this.losRangeQuery = cmpRangeManager.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
if (enable)
cmpRangeManager.EnableActiveQuery(this.losRangeQuery);

View File

@ -1 +0,0 @@
Engine.RegisterInterface("Damage");

View File

@ -0,0 +1 @@
Engine.RegisterInterface("DelayedDamage");

View File

@ -1,4 +1,4 @@
Engine.RegisterInterface("DamageReceiver");
Engine.RegisterInterface("Resistance");
/**
* Message of the form { "entity": entity, "invulnerability": true/false }

View File

@ -1,5 +1,5 @@
Engine.LoadHelperScript("DamageBonus.js");
Engine.LoadHelperScript("DamageTypes.js");
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Attack.js");
@ -104,7 +104,7 @@ function attackComponentTest(defenderClass, isEnemy, test_function)
}
},
"Capture": {
"Value": 8,
"Capture": 8,
"MaxRange": 10,
},
"Slaughter": {}
@ -150,18 +150,28 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => {
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.GetAttackStrengths("Capture"), { "value": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Capture"), { "Capture": 8 });
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged"), {
"Hack": 0,
"Pierce": 10,
"Crush": 0
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackEffectsData("Ranged"), {
"Damage": {
"Hack": 0,
"Pierce": 10,
"Crush": 0
}
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged.Splash"), {
"Hack": 0.0,
"Pierce": 15.0,
"Crush": 35.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"), {
@ -175,10 +185,18 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => {
});
TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), {
"damage": {
"Hack": 0,
"Pierce": 15,
"Crush": 35,
"attackData": {
"Damage": {
"Hack": 0,
"Pierce": 15,
"Crush": 35,
},
"Bonuses": {
"BonusCav": {
"Classes": "Cavalry",
"Multiplier": 3
}
}
},
"friendlyFire": false,
"shape": "Circular"
@ -188,14 +206,14 @@ attackComponentTest(undefined, true, (attacker, cmpAttack, defender) => {
for (let className of ["Infantry", "Cavalry"])
attackComponentTest(className, true, (attacker, cmpAttack, defender) => {
TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2);
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Melee").Bonuses.BonusCav.Multiplier, 2);
TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null);
TS_ASSERT_EQUALS(cmpAttack.GetAttackEffectsData("Capture").Bonuses || null, null);
let getAttackBonus = (s, t, e) => GetDamageBonus(s, e, t, cmpAttack.GetBonusTemplate(t));
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.Splash", defender), className == "Cavalry" ? 3 : 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);
});

View File

@ -1,13 +1,13 @@
Engine.LoadHelperScript("DamageBonus.js");
Engine.LoadHelperScript("DamageTypes.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/AuraManager.js");
Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.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");
@ -16,22 +16,24 @@ Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("Attack.js");
Engine.LoadComponentScript("Damage.js");
Engine.LoadComponentScript("DelayedDamage.js");
Engine.LoadComponentScript("Timer.js");
function Test_Generic()
{
ResetState();
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer");
cmpTimer.OnUpdate({ turnLength: 1 });
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,
@ -51,19 +53,19 @@ function Test_Generic()
let type = "Melee";
let damageTaken = false;
cmpAttack.GetAttackStrengths = attackType => ({ "hack": 0, "pierce": 0, "crush": damage });
cmpAttack.GetAttackStrengths = attackType => ({ "Hack": 0, "Pierce": 0, "Crush": damage });
let data = {
"attacker": attacker,
"target": target,
"type": "Melee",
"strengths": { "hack": 0, "pierce": 0, "crush": damage },
"multiplier": 1.0,
"attackData": {
"Damage": { "Hack": 0, "Pierce": 0, "Crush": damage },
},
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"position": targetPos,
"isSplash": false,
"projectileId": 9,
"direction": new Vector3D(1,0,0)
"direction": new Vector3D(1, 0, 0)
};
AddMock(atkPlayerEntity, IID_Player, {
@ -87,15 +89,22 @@ function Test_Generic()
"IsInWorld": () => true,
});
AddMock(target, IID_Health, {});
AddMock(target, IID_Health, {
"TakeDamage": (effectData, __, ___, bonusMultiplier) => {
damageTaken = true;
return { "killed": false, "HPchange": -bonusMultiplier * effectData.Crush };
},
});
AddMock(target, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => { damageTaken = true; return { "killed": false, "change": -multiplier * strengths.crush }; },
AddMock(SYSTEM_ENTITY, IID_DelayedDamage, {
"MissileHit": () => {
damageTaken = true;
},
});
Engine.PostMessage = function(ent, iid, message)
{
TS_ASSERT_UNEVAL_EQUALS({ "attacker": attacker, "target": target, "type": type, "damage": damage, "attackerOwner": attackerOwner }, message);
TS_ASSERT_UNEVAL_EQUALS({ "type": type, "target": target, "attacker": attacker, "attackerOwner": attackerOwner, "damage": damage, "capture": 0, "statusEffects": [] }, message);
};
AddMock(target, IID_Footprint, {
@ -114,16 +123,17 @@ function Test_Generic()
function TestDamage()
{
cmpTimer.OnUpdate({ turnLength: 1 });
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT(damageTaken);
damageTaken = false;
}
cmpDamage.CauseDamage(data);
Attacking.HandleAttackEffects(data.type, data.attackData, data.target, data.attacker, data.attackerOwner);
TestDamage();
type = data.type = "Ranged";
cmpDamage.CauseDamage(data);
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
@ -135,8 +145,8 @@ function Test_Generic()
AddMock(atkPlayerEntity, IID_Player, {
"GetEnemies": () => [2, 3]
});
TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, true), [0, 1, 2, 3, 4]);
TS_ASSERT_UNEVAL_EQUALS(cmpDamage.GetPlayersToDamage(atkPlayerEntity, false), [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();
@ -152,26 +162,24 @@ function TestLinearSplashDamage()
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",
"strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"direction": new Vector3D(1, 747, 0),
"playersToDamage": [2],
"type": "Ranged",
"attackerOwner": attackerOwner
};
let fallOff = function(x,y)
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();
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62],
});
@ -188,31 +196,31 @@ function TestLinearSplashDamage()
"GetPosition2D": () => new Vector2D(5, 2),
});
AddMock(60, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(60, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(2.2, -0.4));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0, 0));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(62, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(62);
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 0);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
cmpDamage.CauseDamageOverArea(data);
Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
@ -220,15 +228,15 @@ function TestLinearSplashDamage()
data.direction = new Vector3D(0.6, 747, 0.8);
AddMock(60, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(60, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1, 2));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
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) };
}
});
cmpDamage.CauseDamageOverArea(data);
Attacking.CauseDamageOverArea(data);
TS_ASSERT(hitEnts.has(60));
TS_ASSERT(hitEnts.has(61));
TS_ASSERT(hitEnts.has(62));
@ -249,8 +257,6 @@ function TestCircularSplashDamage()
return 1 - r * r / (radius * radius);
};
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
"ExecuteQueryAroundPos": () => [60, 61, 62, 64],
});
@ -276,49 +282,49 @@ function TestCircularSplashDamage()
"GetPosition2D": () => new Vector2D(9, -4),
});
AddMock(60, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(0));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(5));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100 * fallOff(1));
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(63, IID_Resistance, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT(false);
}
});
AddMock(64, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 0);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
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) };
}
});
cmpDamage.CauseDamageOverArea({
Attacking.CauseDamageOverArea({
"type": "Ranged",
"attackData": { "Damage": { "Hack": 100, "Pierce": 0, "Crush": 0 } },
"attacker": 50,
"attackerOwner": 1,
"origin": new Vector2D(3, 4),
"radius": radius,
"shape": "Circular",
"strengths": { "hack" : 100, "pierce" : 0, "crush": 0 },
"playersToDamage": [2],
"type": "Ranged",
"attackerOwner": 1
});
}
@ -329,7 +335,7 @@ function Test_MissileHit()
ResetState();
Engine.PostMessage = (ent, iid, message) => {};
let cmpDamage = ConstructComponent(SYSTEM_ENTITY, "Damage");
let cmpDelayedDamage = ConstructComponent(SYSTEM_ENTITY, "DelayedDamage");
let target = 60;
let targetOwner = 1;
@ -344,15 +350,13 @@ function Test_MissileHit()
let data = {
"type": "Ranged",
"attacker": 70,
"attackData": { "Damage": { "Hack": 0, "Pierce": 100, "Crush": 0 } },
"target": 60,
"strengths": { "hack": 0, "pierce": 100, "crush": 0 },
"attacker": 70,
"attackerOwner": 1,
"position": targetPos,
"direction": new Vector3D(1, 0, 0),
"projectileId": 9,
"bonus": undefined,
"isSplash": false,
"attackerOwner": 1
};
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
@ -372,13 +376,11 @@ function Test_MissileHit()
"IsInWorld": () => true,
});
AddMock(60, IID_Health, {});
AddMock(60, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(60, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(60);
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
@ -400,7 +402,7 @@ function Test_MissileHit()
"GetEnemies": () => [2]
});
cmpDamage.MissileHit(data, 0);
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(60));
hitEnts.clear();
@ -413,10 +415,10 @@ function Test_MissileHit()
"IsInWorld": () => true,
});
AddMock(60, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
AddMock(60, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
TS_ASSERT_EQUALS(false);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
@ -431,13 +433,11 @@ function Test_MissileHit()
"IsInWorld": () => true,
});
AddMock(61, IID_Health, {});
AddMock(61, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 100);
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 100);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
@ -445,28 +445,27 @@ function Test_MissileHit()
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
cmpDamage.MissileHit(data, 0);
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
hitEnts.clear();
// Add a splash damage.
data.friendlyFire = false;
data.radius = 10;
data.shape = "Circular";
data.isSplash = true;
data.splashStrengths = { "hack": 0, "pierce": 0, "crush": 200 };
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_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
dealtDamage += multiplier * (strengths.hack + strengths.pierce + strengths.crush);
AddMock(61, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(61);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
dealtDamage += mult * (effectData.Hack + effectData.Pierce + effectData.Crush);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
@ -477,13 +476,11 @@ function Test_MissileHit()
"IsInWorld": () => true,
});
AddMock(62, IID_Health, {});
AddMock(62, IID_DamageReceiver, {
"TakeDamage": (strengths, multiplier) => {
TS_ASSERT_EQUALS(multiplier * (strengths.hack + strengths.pierce + strengths.crush), 200 * 0.75);
AddMock(62, IID_Health, {
"TakeDamage": (effectData, __, ___, mult) => {
hitEnts.add(62);
return { "killed": false, "change": -multiplier * (strengths.hack + strengths.pierce + strengths.crush) };
TS_ASSERT_EQUALS(mult * (effectData.Hack + effectData.Pierce + effectData.Crush), 200 * 0.75);
return { "killed": false, "change": -mult * (effectData.Hack + effectData.Pierce + effectData.Crush) };
}
});
@ -491,7 +488,7 @@ function Test_MissileHit()
"GetShape": () => ({ "type": "circle", "radius": 20 }),
});
cmpDamage.MissileHit(data, 0);
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 200);
dealtDamage = 0;
@ -512,36 +509,36 @@ function Test_MissileHit()
"HasClass": cl => cl == "Cavalry"
});
data.bonus = bonus;
cmpDamage.MissileHit(data, 0);
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.splashBonus = splashBonus;
cmpDamage.MissileHit(data, 0);
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.bonus = undefined;
cmpDamage.MissileHit(data, 0);
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.bonus = null;
cmpDamage.MissileHit(data, 0);
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.bonus = {};
cmpDamage.MissileHit(data, 0);
data.attackData.Bonuses = {};
cmpDelayedDamage.MissileHit(data, 0);
TS_ASSERT(hitEnts.has(61));
TS_ASSERT_EQUALS(dealtDamage, 100 + 10000 * 200);
dealtDamage = 0;

View File

@ -1,8 +1,7 @@
Engine.LoadHelperScript("DamageBonus.js");
Engine.LoadHelperScript("DamageTypes.js");
Engine.LoadHelperScript("Attacking.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("DeathDamage.js");
@ -28,10 +27,12 @@ let template = {
}
};
let modifiedDamage = {
"Hack": 0.0,
"Pierce": 215.0,
"Crush": 35.0
let effects = {
"Damage": {
"Hack": 0.0,
"Pierce": 215.0,
"Crush": 35.0
}
};
let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
@ -40,21 +41,18 @@ let playersToDamage = [2, 3, 7];
let pos = new Vector2D(3, 4.2);
let result = {
"type": "Death",
"attackData": effects,
"attacker": deadEnt,
"attackerOwner": player,
"origin": pos,
"radius": template.Range,
"shape": template.Shape,
"strengths": modifiedDamage,
"splashBonus": null,
"playersToDamage": playersToDamage,
"type": "Death",
"attackerOwner": player
"playersToDamage": playersToDamage
};
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamageOverArea": data => TS_ASSERT_UNEVAL_EQUALS(data, result),
"GetPlayersToDamage": (owner, friendlyFire) => playersToDamage
});
Attacking.CauseDamageOverArea = data => TS_ASSERT_UNEVAL_EQUALS(data, result);
Attacking.GetPlayersToDamage = () => playersToDamage;
AddMock(deadEnt, IID_Position, {
"GetPosition2D": () => pos,
@ -65,13 +63,13 @@ AddMock(deadEnt, IID_Ownership, {
"GetOwner": () => player
});
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();
// Test splash damage bonus
let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
template.Bonuses = splashBonus;
effects.Bonuses = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } };
template.Bonuses = effects.Bonuses;
cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template);
result.splashBonus = splashBonus;
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage);
result.attackData.Bonuses = effects.Bonuses;
TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageEffects(), effects);
cmpDeathDamage.CauseDeathDamage();

View File

@ -6,7 +6,7 @@ Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");

View File

@ -64,7 +64,7 @@ var change = cmpHealth.Reduce(25);
TS_ASSERT_EQUALS(injured_flag, true);
TS_ASSERT_EQUALS(change.killed, false);
TS_ASSERT_EQUALS(change.change, -25);
TS_ASSERT_EQUALS(change.HPchange, -25);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 25);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), true);
@ -107,7 +107,7 @@ TS_ASSERT_EQUALS(corpse_entity, "corpse|test");
TS_ASSERT_EQUALS(injured_flag, false);
TS_ASSERT_EQUALS(change.killed, true);
TS_ASSERT_EQUALS(change.change, -50);
TS_ASSERT_EQUALS(change.HPchange, -50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
@ -122,7 +122,7 @@ TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
// Check that we can't die twice.
change = cmpHealth.Reduce(50);
TS_ASSERT_EQUALS(change.killed, false);
TS_ASSERT_EQUALS(change.change, 0);
TS_ASSERT_EQUALS(change.HPchange, 0);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);
@ -132,7 +132,7 @@ cmpHealth = setEntityUp();
// Check that we still die with > Max HP of damage.
change = cmpHealth.Reduce(60);
TS_ASSERT_EQUALS(change.killed, true);
TS_ASSERT_EQUALS(change.change, -50);
TS_ASSERT_EQUALS(change.HPchange, -50);
TS_ASSERT_EQUALS(cmpHealth.GetHitpoints(), 0);
TS_ASSERT_EQUALS(cmpHealth.GetMaxHitpoints(), 50);
TS_ASSERT_EQUALS(cmpHealth.IsInjured(), false);

View File

@ -1,4 +1,3 @@
Engine.LoadComponentScript("interfaces/Damage.js");
Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("StatusEffectsReceiver.js");
@ -20,17 +19,16 @@ function testInflictEffects()
{
setup();
let statusName = "Burn";
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
});
let Attacking = {
"HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; }
};
Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 10, 20 sec
cmpStatusReceiver.InflictEffects({
[statusName]: {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
}
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
});
cmpTimer.OnUpdate({ "turnLength": 1 });
@ -54,15 +52,16 @@ testInflictEffects();
function testMultipleEffects()
{
setup();
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => {
if (data.strengths.Burn) dealtDamage += data.strengths.Burn;
if (data.strengths.Poison) dealtDamage += data.strengths.Poison;
}
});
let Attacking = {
"HandleAttackEffects": (_, attackData) => {
if (attackData.Damage.Burn) dealtDamage += attackData.Damage.Burn;
if (attackData.Damage.Poison) dealtDamage += attackData.Damage.Poison;
},
};
Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 1, 2, 10 sec
cmpStatusReceiver.InflictEffects({
cmpStatusReceiver.GiveStatus({
"Burn": {
"Duration": 20000,
"Interval": 10000,
@ -90,30 +89,29 @@ function testMultipleEffects()
testMultipleEffects();
function testRemoveEffect()
function testRemoveStatus()
{
setup();
let statusName = "Poison";
AddMock(SYSTEM_ENTITY, IID_Damage, {
"CauseDamage": (data) => { dealtDamage += data.strengths[statusName]; }
});
let Attacking = {
"HandleAttackEffects": (_, attackData) => { dealtDamage += attackData.Damage[statusName]; }
};
Engine.RegisterGlobal("Attacking", Attacking);
// damage scheduled: 0, 10, 20 sec
cmpStatusReceiver.InflictEffects({
[statusName]: {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
}
cmpStatusReceiver.AddStatus(statusName, {
"Duration": 20000,
"Interval": 10000,
"Damage": 1
});
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 1 sec
cmpStatusReceiver.RemoveEffect(statusName);
cmpStatusReceiver.RemoveStatus(statusName);
cmpTimer.OnUpdate({ "turnLength": 10 });
TS_ASSERT_EQUALS(dealtDamage, 1); // 11 sec
}
testRemoveEffect();
testRemoveStatus();

View File

@ -5,7 +5,7 @@ Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/DamageReceiver.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/Formation.js");
Engine.LoadComponentScript("interfaces/Heal.js");
Engine.LoadComponentScript("interfaces/Health.js");

View File

@ -0,0 +1,308 @@
/**
* Provides attack and damage-related helpers under the Attacking umbrella (to avoid name ambiguity with the component).
*/
function Attacking() {}
/**
* Builds a RelaxRNG schema of possible attack effects.
* Currently harcoded to "Damage", "Capture" and "StatusEffects".
* Attacks may also have a "Bonuses" element.
*
* @return {string} - RelaxNG schema string
*/
Attacking.prototype.BuildAttackEffectsSchema = function()
{
return "" +
"<oneOrMore>" +
"<choice>" +
"<element name='Damage'>" +
"<oneOrMore>" +
"<element a:help='One or more elements describing damage types'>" +
"<anyName>" +
// Armour requires Foundation to not be a damage type.
"<except><name>Foundation</name></except>" +
"</anyName>" +
"<ref name='nonNegativeDecimal' />" +
"</element>" +
"</oneOrMore>" +
"</element>" +
"<element name='Capture' a:help='Capture points value'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='StatusEffects' a:help='Effects like poisoning or burning a unit.'>" +
"<oneOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Interval' a:help='Interval between the occurances of the effect.'><ref name='nonNegativeDecimal'/></element>" +
"<element name='Damage' a:help='Damage caused by the effect.'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
"</element>" +
"</choice>" +
"</oneOrMore>" +
"<optional>" +
"<element name='Bonuses'>" +
"<zeroOrMore>" +
"<element>" +
"<anyName/>" +
"<interleave>" +
"<optional>" +
"<element name='Civ' a:help='If an entity has this civ then the bonus is applied'><text/></element>" +
"</optional>" +
"<element name='Classes' a:help='If an entity has all these classes then the bonus is applied'><text/></element>" +
"<element name='Multiplier' a:help='The effect strength is multiplied by this'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</zeroOrMore>" +
"</element>" +
"</optional>";
};
/**
* Returns a template-like object of attack effects.
*/
Attacking.prototype.GetAttackEffectsData = function(valueModifRoot, template, entity)
{
let ret = {};
if (template.Damage)
{
ret.Damage = {};
let applyMods = damageType =>
ApplyValueModificationsToEntity(valueModifRoot + "/Damage/" + damageType, +(template.Damage[damageType] || 0), entity);
for (let damageType in template.Damage)
ret.Damage[damageType] = applyMods(damageType);
}
if (template.Capture)
ret.Capture = ApplyValueModificationsToEntity(valueModifRoot + "/Capture", +(template.Capture || 0), entity);
if (template.StatusEffects)
ret.StatusEffects = template.StatusEffects;
if (template.Bonuses)
ret.Bonuses = template.Bonuses;
return ret;
};
Attacking.prototype.GetTotalAttackEffects = function(effectData, effectType, cmpResistance)
{
let total = 0;
let armourStrengths = cmpResistance ? cmpResistance.GetArmourStrengths(effectType) : {};
for (let type in effectData)
total += effectData[type] * Math.pow(0.9, armourStrengths[type] || 0);
return total;
};
/**
* Gives the position of the given entity, taking the lateness into account.
* @param {number} ent - Entity id of the entity we are finding the location for.
* @param {number} lateness - The time passed since the expected time to fire the function.
* @return {Vector3D} The location of the entity.
*/
Attacking.prototype.InterpolatedLocation = function(ent, lateness)
{
let cmpTargetPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) // TODO: handle dead target properly
return undefined;
let curPos = cmpTargetPosition.GetPosition();
let prevPos = cmpTargetPosition.GetPreviousPosition();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
let turnLength = cmpTimer.GetLatestTurnLength();
return new Vector3D(
(curPos.x * (turnLength - lateness) + prevPos.x * lateness) / turnLength,
0,
(curPos.z * (turnLength - lateness) + prevPos.z * lateness) / turnLength
);
};
/**
* Test if a point is inside of an entity's footprint.
* @param {number} ent - Id of the entity we are checking with.
* @param {Vector3D} point - The point we are checking with.
* @param {number} lateness - The time passed since the expected time to fire the function.
* @return {boolean} True if the point is inside of the entity's footprint.
*/
Attacking.prototype.TestCollision = function(ent, point, lateness)
{
let targetPosition = this.InterpolatedLocation(ent, lateness);
if (!targetPosition)
return false;
let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
if (!cmpFootprint)
return false;
let targetShape = cmpFootprint.GetShape();
if (!targetShape)
return false;
if (targetShape.type == "circle")
return targetPosition.horizDistanceToSquared(point) < targetShape.radius * targetShape.radius;
if (targetShape.type == "square")
{
let angle = Engine.QueryInterface(ent, IID_Position).GetRotation().y;
let distance = Vector2D.from3D(Vector3D.sub(point, targetPosition)).rotate(-angle);
return Math.abs(distance.x) < targetShape.width / 2 && Math.abs(distance.y) < targetShape.depth / 2;
}
warn("TestCollision called with an invalid footprint shape");
return false;
};
/**
* Get the list of players affected by the damage.
* @param {number} attackerOwner - The player id of the attacker.
* @param {boolean} friendlyFire - A flag indicating if allied entities are also damaged.
* @return {number[]} The ids of players need to be damaged.
*/
Attacking.prototype.GetPlayersToDamage = function(attackerOwner, friendlyFire)
{
if (!friendlyFire)
return QueryPlayerIDInterface(attackerOwner).GetEnemies();
return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
};
/**
* Damages units around a given origin.
* @param {Object} data - The data sent by the caller.
* @param {string} data.type - The type of damage.
* @param {Object} data.attackData - The attack data.
* @param {number} data.attacker - The entity id of the attacker.
* @param {number} data.attackerOwner - The player id of the attacker.
* @param {Vector2D} data.origin - The origin of the projectile hit.
* @param {number} data.radius - The radius of the splash damage.
* @param {string} data.shape - The shape of the radius.
* @param {Vector3D} [data.direction] - The unit vector defining the direction. Needed for linear splash damage.
* @param {number[]} data.playersToDamage - The array of player id's to damage.
*/
Attacking.prototype.CauseDamageOverArea = function(data)
{
// Get nearby entities and define variables
let nearEnts = this.EntitiesNearPoint(data.origin, data.radius, data.playersToDamage);
let damageMultiplier = 1;
// Cycle through all the nearby entities and damage it appropriately based on its distance from the origin.
for (let ent of nearEnts)
{
let entityPosition = Engine.QueryInterface(ent, IID_Position).GetPosition2D();
if (data.shape == 'Circular') // circular effect with quadratic falloff in every direction
damageMultiplier = 1 - data.origin.distanceToSquared(entityPosition) / (data.radius * data.radius);
else if (data.shape == 'Linear') // linear effect with quadratic falloff in two directions (only used for certain missiles)
{
// Get position of entity relative to splash origin.
let relativePos = entityPosition.sub(data.origin);
// Get the position relative to the missile direction.
let direction = Vector2D.from3D(data.direction);
let parallelPos = relativePos.dot(direction);
let perpPos = relativePos.cross(direction);
// The width of linear splash is one fifth of the normal splash radius.
let width = data.radius / 5;
// Check that the unit is within the distance splash width of the line starting at the missile's
// landing point which extends in the direction of the missile for length splash radius.
if (parallelPos >= 0 && Math.abs(perpPos) < width) // If in radius, quadratic falloff in both directions
damageMultiplier = (1 - parallelPos * parallelPos / (data.radius * data.radius)) *
(1 - perpPos * perpPos / (width * width));
else
damageMultiplier = 0;
}
else // In case someone calls this function with an invalid shape.
{
warn("The " + data.shape + " splash damage shape is not implemented!");
}
this.HandleAttackEffects(data.type + ".Splash", data.attackData, ent, data.attacker, data.attackerOwner, damageMultiplier);
}
};
Attacking.prototype.HandleAttackEffects = function(attackType, attackData, target, attacker, attackerOwner, bonusMultiplier = 1)
{
let targetState = {};
for (let effectType of g_EffectTypes)
{
if (!attackData[effectType])
continue;
bonusMultiplier *= !attackData.Bonuses ? 1 : GetAttackBonus(attacker, target, attackType, attackData.Bonuses);
let receiver = g_EffectReceiver[effectType];
let cmpReceiver = Engine.QueryInterface(target, global[receiver.IID]);
if (!cmpReceiver)
return;
Object.assign(targetState, cmpReceiver[receiver.method](attackData[effectType], attacker, attackerOwner, bonusMultiplier));
}
let cmpPromotion = Engine.QueryInterface(attacker, IID_Promotion);
if (cmpPromotion && targetState.xp)
cmpPromotion.IncreaseXp(targetState.xp);
if (targetState.killed)
this.TargetKilled(attacker, target, attackerOwner);
Engine.PostMessage(target, MT_Attacked, {
"type": attackType,
"target": target,
"attacker": attacker,
"attackerOwner": attackerOwner,
"damage": -(targetState.HPchange || 0),
"capture": targetState.captureChange || 0,
"statusEffects": targetState.inflictedStatuses || [],
});
};
/**
* Gets entities near a give point for given players.
* @param {Vector2D} origin - The point to check around.
* @param {number} radius - The radius around the point to check.
* @param {number[]} players - The players of which we need to check entities.
* @return {number[]} The id's of the entities in range of the given point.
*/
Attacking.prototype.EntitiesNearPoint = function(origin, radius, players)
{
// If there is insufficient data return an empty array.
if (!origin || !radius || !players)
return [];
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).ExecuteQueryAroundPos(origin, 0, radius, players, IID_Resistance);
};
/**
* Called when a unit kills something (another unit, building, animal etc).
* @param {number} attacker - The entity id of the killer.
* @param {number} target - The entity id of the target.
* @param {number} attackerOwner - The player id of the attacker.
*/
Attacking.prototype.TargetKilled = function(attacker, target, attackerOwner)
{
let cmpAttackerOwnership = Engine.QueryInterface(attacker, IID_Ownership);
let atkOwner = cmpAttackerOwnership && cmpAttackerOwnership.GetOwner() != INVALID_PLAYER ? cmpAttackerOwnership.GetOwner() : attackerOwner;
// Add to killer statistics.
let cmpKillerPlayerStatisticsTracker = QueryPlayerIDInterface(atkOwner, IID_StatisticsTracker);
if (cmpKillerPlayerStatisticsTracker)
cmpKillerPlayerStatisticsTracker.KilledEntity(target);
// Add to loser statistics.
let cmpTargetPlayerStatisticsTracker = QueryOwnerInterface(target, IID_StatisticsTracker);
if (cmpTargetPlayerStatisticsTracker)
cmpTargetPlayerStatisticsTracker.LostEntity(target);
// If killer can collect loot, let's try to collect it.
let cmpLooter = Engine.QueryInterface(attacker, IID_Looter);
if (cmpLooter)
cmpLooter.Collect(target);
};
var AttackingInstance = new Attacking();
Engine.RegisterGlobal("Attacking", AttackingInstance);

View File

@ -6,7 +6,7 @@
* @param {Object} template - The bonus' template.
* @return {number} - The source entity's attack bonus against the specified target.
*/
function GetDamageBonus(source, target, type, template)
function GetAttackBonus(source, target, type, template)
{
let attackBonus = 1;
@ -28,4 +28,4 @@ function GetDamageBonus(source, target, type, template)
return attackBonus;
}
Engine.RegisterGlobal("GetDamageBonus", GetDamageBonus);
Engine.RegisterGlobal("GetAttackBonus", GetAttackBonus);

View File

@ -1,22 +0,0 @@
/**
* Builds a RelaxRNG schema based on currently valid elements.
*
* To prevent validation errors, disabled damage types are included in the schema.
*
* @param {string} helptext - Text displayed as help
* @return {string} - RelaxNG schema string
*/
function BuildDamageTypesSchema(helptext = "")
{
return "<oneOrMore>" +
"<element a:help='" + helptext + "'>" +
"<anyName>" +
// Armour requires Foundation to not be a damage type.
"<except><name>Foundation</name></except>" +
"</anyName>" +
"<ref name='nonNegativeDecimal' />" +
"</element>" +
"</oneOrMore>";
}
Engine.RegisterGlobal("BuildDamageTypesSchema", BuildDamageTypesSchema);

View File

@ -7,7 +7,7 @@
</Armour>
<Attack>
<Capture>
<Value>2</Value>
<Capture>2</Capture>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Field Palisade SiegeWall StoneWall</RestrictedClasses>

View File

@ -2,7 +2,7 @@
<Entity parent="template_unit">
<Attack>
<Capture>
<Value>5</Value>
<Capture>5</Capture>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Field Palisade SiegeWall StoneWall</RestrictedClasses>

View File

@ -7,7 +7,7 @@
</Armour>
<Attack>
<Capture>
<Value>15</Value>
<Capture>15</Capture>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Field Palisade SiegeWall StoneWall</RestrictedClasses>

View File

@ -7,7 +7,7 @@
</Armour>
<Attack>
<Capture>
<Value>2</Value>
<Capture>2</Capture>
<MaxRange>4</MaxRange>
<RepeatTime>1000</RepeatTime>
<RestrictedClasses datatype="tokens">Field Palisade SiegeWall StoneWall</RestrictedClasses>