Changeset View
Changeset View
Standalone View
Standalone View
binaries/data/mods/public/simulation/components/Attack.js
function Attack() {} | function Attack() {} | ||||
var g_AttackTypes = ["Melee", "Ranged", "Capture"]; | |||||
Attack.prototype.preferredClassesSchema = | Attack.prototype.preferredClassesSchema = | ||||
"<optional>" + | "<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'>" + | "<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'>" + | ||||
"<attribute name='datatype'>" + | "<attribute name='datatype'>" + | ||||
"<value>tokens</value>" + | "<value>tokens</value>" + | ||||
"</attribute>" + | "</attribute>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
▲ Show 20 Lines • Show All 76 Lines • ▼ Show 20 Lines | "<a:example>" + | ||||
"</Ranged>" + | "</Ranged>" + | ||||
"<Slaughter>" + | "<Slaughter>" + | ||||
"<Damage>" + | "<Damage>" + | ||||
"<Hack>1000.0</Hack>" + | "<Hack>1000.0</Hack>" + | ||||
"<Pierce>0.0</Pierce>" + | "<Pierce>0.0</Pierce>" + | ||||
"<Crush>0.0</Crush>" + | "<Crush>0.0</Crush>" + | ||||
"</Damage>" + | "</Damage>" + | ||||
"<MaxRange>4.0</MaxRange>" + | "<MaxRange>4.0</MaxRange>" + | ||||
"<RepeatTime>1000</RepeatTime>" + | |||||
"<RestrictedClasses datatype=\"tokens\">!Domestic</RestrictedClasses>" + | |||||
"</Slaughter>" + | "</Slaughter>" + | ||||
"</a:example>" + | "</a:example>" + | ||||
"<optional>" + | |||||
"<element name='Melee'>" + | |||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" + | "<zeroOrMore>" + | ||||
"<optional>" + | "<element>" + | ||||
"<attribute name='context'>" + | "<anyName/>" + | ||||
"<text/>" + | |||||
"</attribute>" + | |||||
"</optional>" + | |||||
"<text/>" + | |||||
"</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's attack animation'>" + | |||||
"<data type='nonNegativeInteger'/>" + | |||||
"</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.preferredClassesSchema + | |||||
Attack.prototype.restrictedClassesSchema + | |||||
"</interleave>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Ranged'>" + | |||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" + | "<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<attribute name='context'>" + | "<attribute name='context'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</attribute>" + | "</attribute>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
Attacking.BuildAttackEffectsSchema() + | Attacking.BuildAttackEffectsSchema() + | ||||
"<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + | "<element name='MaxRange' a:help='Maximum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + | ||||
"<optional>" + | |||||
"<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + | "<element name='MinRange' a:help='Minimum attack range (in metres)'><ref name='nonNegativeDecimal'/></element>" + | ||||
Freagarach: Perhaps nice to add the default value here. | |||||
bbAuthorUnsubmitted Done Inline Actionssure bb: sure | |||||
"</optional>" + | |||||
"<optional>"+ | "<optional>" + | ||||
"<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" + | "<element name='ElevationBonus' a:help='give an elevation advantage (in meters)'><ref name='nonNegativeDecimal'/></element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='RangeOverlay'>" + | "<element name='RangeOverlay'>" + | ||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='LineTexture'><text/></element>" + | "<element name='LineTexture'><text/></element>" + | ||||
"<element name='LineTextureMask'><text/></element>" + | "<element name='LineTextureMask'><text/></element>" + | ||||
"<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" + | "<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" + | ||||
"</interleave>" + | "</interleave>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | |||||
"<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's attack animation'>" + | "<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's attack animation'>" + | ||||
FreagarachUnsubmitted Not Done Inline ActionsIdem. Why is this optional anyway? Freagarach: Idem.
Why is this optional anyway? | |||||
bbAuthorUnsubmitted Done Inline ActionsTo not change every single template, when a obvious default value (0) is available bb: To not change every single template, when a obvious default value (0) is available | |||||
"<data type='nonNegativeInteger'/>" + | "<data type='nonNegativeInteger'/>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | |||||
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + | "<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + | ||||
"<data type='positiveInteger'/>" + | "<data type='positiveInteger'/>" + | ||||
"</element>" + | "</element>" + | ||||
"<optional>" + | |||||
"<element name='Delay' a:help='Delay of the damage in milliseconds'><ref name='nonNegativeDecimal'/></element>" + | "<element name='Delay' a:help='Delay of the damage in milliseconds'><ref name='nonNegativeDecimal'/></element>" + | ||||
"</optional>" + | |||||
"<optional>" + | "<optional>" + | ||||
"<element name='Splash'>" + | "<element name='Splash'>" + | ||||
FreagarachUnsubmitted Not Done Inline Actions(Are we finally going to give elephants splash damage? ;P ) Freagarach: (Are we finally going to give elephants splash damage? ;P ) | |||||
bbAuthorUnsubmitted Done Inline ActionsBe my guest bb: Be my guest | |||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" + | "<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='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='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" + | ||||
Attacking.BuildAttackEffectsSchema() + | Attacking.BuildAttackEffectsSchema() + | ||||
"</interleave>" + | "</interleave>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | |||||
"<element name='Projectile'>" + | "<element name='Projectile'>" + | ||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='Speed' a:help='Speed of projectiles (in meters per second).'>" + | "<element name='Speed' a:help='Speed of projectiles (in meters per second).'>" + | ||||
"<ref name='positiveDecimal'/>" + | "<ref name='positiveDecimal'/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='Spread' a:help='Standard deviation of the bivariate normal distribution of hits at 100 meters. A disk at 100 meters from the attacker with this radius (2x this radius, 3x this radius) is expected to include the landing points of 39.3% (86.5%, 98.9%) of the rounds.'><ref name='nonNegativeDecimal'/></element>" + | "<element name='Spread' a:help='Standard deviation of the bivariate normal distribution of hits at 100 meters. A disk at 100 meters from the attacker with this radius (2x this radius, 3x this radius) is expected to include the landing points of 39.3% (86.5%, 98.9%) of the rounds.'><ref name='nonNegativeDecimal'/></element>" + | ||||
"<element name='Gravity' a:help='The gravity affecting the projectile. This affects the shape of the flight curve.'>" + | "<element name='Gravity' a:help='The gravity affecting the projectile. This affects the shape of the flight curve.'>" + | ||||
"<ref name='nonNegativeDecimal'/>" + | "<ref name='nonNegativeDecimal'/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='FriendlyFire' a:help='Whether stray missiles can hurt non enemy units.'><data type='boolean'/></element>" + | "<element name='FriendlyFire' a:help='Whether stray missiles can hurt non enemy units.'><data type='boolean'/></element>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='LaunchPoint' a:help='Delta from the unit position where to launch the projectile.'>" + | "<element name='LaunchPoint' a:help='Delta from the unit position where to launch the projectile.'>" + | ||||
"<attribute name='y'>" + | "<attribute name='y'>" + | ||||
"<data type='decimal'/>" + | "<data type='decimal'/>" + | ||||
"</attribute>" + | "</attribute>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='ActorName' a:help='actor of the projectile animation.'>" + | "<element name='ActorName' a:help='actor of the projectile animation.'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='ImpactActorName' a:help='actor of the projectile impact animation'>" + | "<element name='ImpactActorName' a:help='actor of the projectile impact animation'>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='ImpactAnimationLifetime' a:help='length of the projectile impact animation.'>" + | "<element name='ImpactAnimationLifetime' a:help='length of the projectile impact animation.'>" + | ||||
"<ref name='positiveDecimal'/>" + | "<ref name='positiveDecimal'/>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"</interleave>" + | "</interleave>" + | ||||
"</element>" + | "</element>" + | ||||
Attack.prototype.preferredClassesSchema + | |||||
Attack.prototype.restrictedClassesSchema + | |||||
"</interleave>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Capture'>" + | |||||
"<interleave>" + | |||||
"<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" + | |||||
"<optional>" + | |||||
"<attribute name='context'>" + | |||||
"<text/>" + | |||||
"</attribute>" + | |||||
"</optional>" + | |||||
"<text/>" + | |||||
"</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.preferredClassesSchema + | |||||
Attack.prototype.restrictedClassesSchema + | |||||
"</interleave>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Slaughter' a:help='A special attack to kill domestic animals'>" + | |||||
"<interleave>" + | |||||
"<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI. Optionally includes a translate context attribute.'>" + | |||||
"<optional>" + | |||||
"<attribute name='context'>" + | |||||
"<text/>" + | |||||
"</attribute>" + | |||||
"</optional>" + | "</optional>" + | ||||
"<text/>" + | |||||
"</element>" + | |||||
Attacking.BuildAttackEffectsSchema() + | |||||
"<element name='MaxRange'><ref name='nonNegativeDecimal'/></element>" + // TODO: how do these work? | |||||
Attack.prototype.preferredClassesSchema + | Attack.prototype.preferredClassesSchema + | ||||
Attack.prototype.restrictedClassesSchema + | Attack.prototype.restrictedClassesSchema + | ||||
"</interleave>" + | "</interleave>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>"; | "</zeroOrMore>" + | ||||
"</interleave>"; | |||||
Attack.prototype.Init = function() | Attack.prototype.Init = function() | ||||
{ | { | ||||
}; | }; | ||||
Attack.prototype.Serialize = null; // we have no dynamic state to save | Attack.prototype.Serialize = null; // we have no dynamic state to save | ||||
Attack.prototype.GetAttackTypes = function(wantedTypes) | Attack.prototype.GetAttackTypes = function(wantedTypes) | ||||
{ | { | ||||
let types = g_AttackTypes.filter(type => !!this.template[type]); | let types = Object.keys(this.template); | ||||
if (!wantedTypes) | if (!wantedTypes) | ||||
return types; | return types; | ||||
let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); | let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); | ||||
return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && | return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && | ||||
(!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); | (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); | ||||
}; | }; | ||||
Show All 10 Lines | |||||
{ | { | ||||
if (this.template[type] && this.template[type].RestrictedClasses && | if (this.template[type] && this.template[type].RestrictedClasses && | ||||
this.template[type].RestrictedClasses._string) | this.template[type].RestrictedClasses._string) | ||||
return this.template[type].RestrictedClasses._string.split(/\s+/); | return this.template[type].RestrictedClasses._string.split(/\s+/); | ||||
return []; | return []; | ||||
}; | }; | ||||
Attack.prototype.CanAttack = function(target, wantedTypes) | /** | ||||
* Figure out whether we can attack a given target, with some attack type. | |||||
* @param {number} target - The entity-ID of the target. | |||||
* @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range. | |||||
* @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore. | |||||
* @param {string[]} preAttackTypes - List of (negated) attacktypes to allowed. | |||||
* @param {string} projectile - Only allow types with(out) projectiles. Use "required" to allow only types with a projectile, use "disallowed" to allow only types without a projectile. | |||||
* @return {boolean} - Whether we can attack the target. | |||||
*/ | |||||
Attack.prototype.CanAttack = function(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile) | |||||
{ | |||||
return this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile).length > 0; | |||||
}; | |||||
/** | |||||
* Find all attack types we can use to attack the target. | |||||
* @param {number} target - The entity-ID of the target. | |||||
* @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range. | |||||
* @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore. | |||||
* @param {string[]} preAttackTypes - List of (negated) attacktypes to allowed. | |||||
* @param {string} projectile - Only allow types with(out) projectiles. Use "required" to allow only types with a projectile, use "disallowed" to allow only types without a projectile. | |||||
* @return {string[]} - The list of allowed attack types. | |||||
*/ | |||||
Attack.prototype.GetAllowedAttackTypes = function(target, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile) | |||||
{ | { | ||||
let cmpFormation = Engine.QueryInterface(target, IID_Formation); | let cmpFormation = Engine.QueryInterface(target, IID_Formation); | ||||
if (cmpFormation) | if (cmpFormation) | ||||
return true; | { | ||||
let types = []; | |||||
for (let member of cmpFormation.GetMembers()) | |||||
types = types.concat(cmpMemberAttack.GetAllowedAttackTypes(member, mustBeInRange, ignoreAttackEffects, wantedTypes, projectile)); | |||||
return types; | |||||
} | |||||
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | let allowedAttackEffects = g_EffectTypes.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]); | ||||
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | if (!allowedAttackEffects.length) | ||||
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) | return []; | ||||
return false; | |||||
let cmpIdentity = QueryMiragedInterface(target, IID_Identity); | let types = this.GetAttackTypes(wantedTypes); | ||||
if (!cmpIdentity) | if (!types.length) | ||||
return false; | return []; | ||||
let cmpHealth = QueryMiragedInterface(target, IID_Health); | let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
let targetClasses = cmpIdentity.GetClassesList(); | let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | ||||
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && | if (!cmpThisPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition || !cmpTargetPosition.IsInWorld()) | ||||
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) | return []; | ||||
return true; | |||||
let cmpEntityPlayer = QueryOwnerInterface(this.entity); | let cmpEntityPlayer = QueryOwnerInterface(this.entity); | ||||
let cmpTargetPlayer = QueryOwnerInterface(target); | let cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity); | ||||
if (!cmpTargetPlayer || !cmpEntityPlayer) | if (!cmpEntityPlayer || !cmpTargetIdentity) | ||||
return false; | return []; | ||||
let types = this.GetAttackTypes(wantedTypes); | let targetClasses = cmpTargetIdentity.GetClassesList(); | ||||
let entityOwner = cmpEntityPlayer.GetPlayerID(); | 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 | let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); | ||||
// 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) | let heightDiff = cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset(); | ||||
return types.filter(type => { | |||||
if (mustBeInRange) | |||||
{ | { | ||||
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) | let range = this.GetRange(type); | ||||
continue; | // Parabolic range compuation is the same as in BuildingAI's FireArrows and UnitAI's' MoveToTargetAttackRange and CheckTargetAttackRange. | ||||
FreagarachUnsubmitted Not Done Inline ActionsCan't this be moved to the position helper? Freagarach: Can't this be moved to the position helper? | |||||
bbAuthorUnsubmitted Done Inline Actionsyea was thinking about the same. Need to think about RangeManager::getParabolicRangeForm too. bb: yea was thinking about the same. Need to think about RangeManager::getParabolicRangeForm too. | |||||
// h is positive when I'm higher than the target. | |||||
let h = heightDiff + range.elevationBonus; | |||||
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) | // In case the target is too high compared to us, we are out of range. | ||||
continue; | if (h <= -range.max / 2) | ||||
return false; | |||||
if (heightDiff > this.GetRange(type).max) | if (!cmpObstructionManager.IsInTargetRange( | ||||
continue; | this.entity, | ||||
target, | |||||
range.min, | |||||
Math.sqrt(Math.square(range.max) + 2 * range.max * h), | |||||
false)) | |||||
return false; | |||||
} | |||||
let restrictedClasses = this.GetRestrictedClasses(type); | let attackEffects = this.GetAttackEffectsData(type, false); | ||||
if (!restrictedClasses.length) | let attackEffectsSplash = this.GetAttackEffectsData(type, true); | ||||
return true; | |||||
if (!MatchesClassList(targetClasses, restrictedClasses)) | let bonusMultiplier = attackEffects && attackEffects.Bonuses ? | ||||
Attacking.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses) : | |||||
1; | |||||
let bonusMultiplierSplash = attackEffectsSplash && attackEffectsSplash.Bonuses ? | |||||
Attacking.GetAttackBonus(this.entity, target, type + "/Splash", attackEffectsSplash.Bonuses) : | |||||
1; | |||||
// We can't use the type if we can't cause any effect. | |||||
if (allowedAttackEffects.every(effectType => { | |||||
let receiver = g_EffectReceiver[effectType]; | |||||
let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); | |||||
if (!cmpReceiver) | |||||
return true; | return true; | ||||
} | |||||
return ((!attackEffects[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, bonusMultiplier, entityOwner)) && | |||||
(!attackEffectsSplash[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffectsSplash, effectType, bonusMultiplierSplash, entityOwner))); | |||||
})) | |||||
return false; | |||||
if ((projectile == "required" && !this.template[type].Projectile) || (projectile == "disallowed" && this.template[type].Projectile)) | |||||
return false; | return false; | ||||
let restrictedClasses = this.GetRestrictedClasses(type); | |||||
return !restrictedClasses.length || !MatchesClassList(targetClasses, restrictedClasses); | |||||
}); | |||||
}; | }; | ||||
/** | /** | ||||
* Returns null if we have no preference or the lowest index of a preferred class. | * Returns null if we have no preference or the lowest index of a preferred class. | ||||
*/ | */ | ||||
Attack.prototype.GetPreference = function(target) | Attack.prototype.GetPreference = function(target) | ||||
{ | { | ||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | ||||
Show All 32 Lines | Attack.prototype.GetFullAttackRange = function() | ||||
} | } | ||||
return ret; | return ret; | ||||
}; | }; | ||||
Attack.prototype.GetAttackEffectsData = function(type, splash) | Attack.prototype.GetAttackEffectsData = function(type, splash) | ||||
{ | { | ||||
let template = this.template[type]; | let template = this.template[type]; | ||||
if (splash) | if (splash) | ||||
{ | |||||
if (!template.Splash) | |||||
return {}; | |||||
template = template.Splash; | template = template.Splash; | ||||
} | |||||
return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); | return Attacking.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); | ||||
}; | }; | ||||
/** | /** | ||||
* Find the best attack against a target. | * Find the best attack against a target. Using a DPS/range algorithm. | ||||
* @param {number} target - The entity-ID of the target. | * @param {number} target - The entity-ID of the target. | ||||
* @param {boolean} allowCapture - Whether capturing is allowed. | * @param {boolean} mustBeInRange - Consider only attack types for which we are currently in range. This will always be honoured. | ||||
* @param {Object} ignoreAttackEffects - Object of the form { "Damage": true, "Capture": false } defining which attackEffects to ignore. | |||||
* @param {string[]} preAttackTypes - List of (negated) attacktypes to prefer. | |||||
* @param {string} projectile - Prefer types with(out) projectiles. Use "required" to prefer types with a projectile, use "disallowed" to prefer types without a projectile. | |||||
* @return {string} - The preferred attack type. | * @return {string} - The preferred attack type. | ||||
*/ | */ | ||||
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) | Attack.prototype.GetBestAttackAgainst = function(target, mustBeInRange, ignoreAttackEffects, prefTypes, projectile) | ||||
{ | { | ||||
// Work out, based on the preferences, which types are potentially possible. | |||||
let types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, prefTypes, projectile); | |||||
if (!types.length && projectile) | |||||
types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects, prefTypes); | |||||
if (!types.length && prefTypes && prefTypes.length) | |||||
types = this.GetAllowedAttackTypes(target, mustBeInRange, ignoreAttackEffects); | |||||
if (!types.length && ignoreAttackEffects && Object.keys(ignoreAttackEffects).filter(effect => ignoreAttackEffects[effect]).length) | |||||
types = this.GetAllowedAttackTypes(target, mustBeInRange); | |||||
// We can't attack at all... | |||||
FreagarachUnsubmitted Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (!types.length) | |||||
return undefined; | |||||
// Boring choosing... | |||||
FreagarachUnsubmitted Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (types.length == 1) | |||||
return types[0]; | |||||
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | |||||
if (!cmpOwnership) | |||||
return undefined; | |||||
let distance = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).DistanceToTarget(this.entity, target); | |||||
// In case the unit or target is not in world, we can't attack them. | |||||
FreagarachUnsubmitted Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (distance < 0) | |||||
return undefined; | |||||
let owner = cmpOwnership.GetOwner(); | |||||
let fullRange = this.GetFullAttackRange(); | |||||
let consideredAttackEffects = g_EffectTypes.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]); | |||||
// Choose the best attack on a DPS/Range. | |||||
let bestType; | |||||
let bestDPSRange = -Infinity; | |||||
let cmpFormation = Engine.QueryInterface(target, IID_Formation); | let cmpFormation = Engine.QueryInterface(target, IID_Formation); | ||||
if (cmpFormation) | if (cmpFormation) | ||||
{ | { | ||||
// TODO: Formation against formation needs review | let members = cmpFormation.GetMembers(); | ||||
let types = this.GetAttackTypes(); | for (let type of types) | ||||
return g_AttackTypes.find(attack => types.indexOf(attack) != -1); | { | ||||
let DPSRange = 0; | |||||
for (let member of members) | |||||
DPSRange += this.GetDPSRange(type, member, consideredAttackEffects, owner, distance, fullRange); | |||||
if (DPSRange > bestDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestDPSRange = DPSRange; | |||||
} | |||||
} | |||||
return bestType; | |||||
} | } | ||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | for (let type of types) | ||||
if (!cmpIdentity) | { | ||||
return undefined; | let DPSRange = this.GetDPSRange(type, target, consideredAttackEffects, owner, distance, fullRange); | ||||
if (DPSRange > bestDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestDPSRange = DPSRange; | |||||
} | |||||
} | |||||
return bestType; | |||||
}; | |||||
/** | |||||
* Compute a DPS range. | |||||
* @param {string} type - The attack type. | |||||
* @param {number} target - Id of the target entity. | |||||
* @param {string[]} - Array of the AttackEffects we should consider. | |||||
* @param {number} attackerOwner - Owner of this entity | |||||
* @param {number} distance - Distance between this.entity and target. | |||||
* @param {Object} fullRange - Range object as returned by this.GetFullAttackRange. | |||||
* @return {number} - The DPS range value. | |||||
*/ | |||||
Attack.prototype.GetDPSRange = function(type, target, consideredAttackEffects, owner, distance, fullRange) | |||||
{ | |||||
let DPSRange = 0; | |||||
let attackEffects = this.GetAttackEffectsData(type, false); | |||||
let multiplier = Attacking.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses || {}); | |||||
for (let effectType of consideredAttackEffects) | |||||
{ | |||||
if (!attackEffects[effectType]) | |||||
continue; | |||||
let receiver = g_EffectReceiver[effectType]; | |||||
let cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); | |||||
if (!cmpReceiver) | |||||
continue; | |||||
// Always slaughter domestic animals instead of using a normal attack | DPSRange += cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, multiplier, owner); | ||||
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); | |||||
} | } | ||||
DPSRange /= this.GetRepeatTime(type); | |||||
let targetClasses = cmpIdentity.GetClassesList(); | // Apply an exponential dropoff when out of range. | ||||
let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType)); | // TODO elevation? | ||||
let range = this.GetRange(type); | |||||
if (distance < range.min) | |||||
DPSRange *= Math.pow(0.2, (range.min - distance) / (fullRange.min || 1)); | |||||
else if (distance > range.max) | |||||
DPSRange *= Math.pow(0.2, (distance - range.max) / (fullRange.max || 1)); | |||||
return types.sort((a, b) => | return DPSRange; | ||||
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - | |||||
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); | |||||
}; | }; | ||||
Attack.prototype.CompareEntitiesByPreference = function(a, b) | Attack.prototype.CompareEntitiesByPreference = function(a, b) | ||||
{ | { | ||||
let aPreference = this.GetPreference(a); | let aPreference = this.GetPreference(a); | ||||
let bPreference = this.GetPreference(b); | let bPreference = this.GetPreference(b); | ||||
if (aPreference === null && bPreference === null) return 0; | if (aPreference === null && bPreference === null) return 0; | ||||
if (aPreference === null) return 1; | if (aPreference === null) return 1; | ||||
if (bPreference === null) return -1; | if (bPreference === null) return -1; | ||||
return aPreference - bPreference; | return aPreference - bPreference; | ||||
}; | }; | ||||
Attack.prototype.GetAttackName = function(type) | Attack.prototype.GetAttackName = function(type) | ||||
{ | { | ||||
return { | return { | ||||
"name": this.template[type].AttackName._string || this.template[type].AttackName, | "name": this.template[type].AttackName._string || this.template[type].AttackName, | ||||
"context": this.template[type].AttackName["@context"] | "context": this.template[type].AttackName["@context"] | ||||
Done Inline ActionsAlso Spread bb: Also Spread | |||||
}; | }; | ||||
}; | }; | ||||
Attack.prototype.GetRepeatTime = function(type) | Attack.prototype.GetRepeatTime = function(type) | ||||
{ | { | ||||
let repeatTime = 1000; | return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", +this.template[type].RepeatTime, this.entity); | ||||
if (this.template[type] && this.template[type].RepeatTime) | |||||
repeatTime = +this.template[type].RepeatTime; | |||||
return ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeatTime, this.entity); | |||||
}; | }; | ||||
Attack.prototype.GetTimers = function(type) | Attack.prototype.GetTimers = function(type) | ||||
{ | { | ||||
return { | return { | ||||
"prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), | "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), | ||||
"repeat": this.GetRepeatTime(type) | "repeat": this.GetRepeatTime(type) | ||||
}; | }; | ||||
Show All 26 Lines | Attack.prototype.GetRange = function(type) | ||||
let elevationBonus = +(this.template[type].ElevationBonus || 0); | let elevationBonus = +(this.template[type].ElevationBonus || 0); | ||||
elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); | elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); | ||||
return { "max": max, "min": min, "elevationBonus": elevationBonus }; | return { "max": max, "min": min, "elevationBonus": elevationBonus }; | ||||
}; | }; | ||||
/** | /** | ||||
* Attack the target entity. This should only be called after a successful range check, | * 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 | * and should only be called after GetTimers().repeat msec has passed since the last | ||||
Not Done Inline ActionsMaybe a difference between moveToRange and isInRange? Freagarach: Maybe a difference between moveToRange and isInRange? | |||||
* call to PerformAttack. | * call to PerformAttack. | ||||
*/ | */ | ||||
Attack.prototype.PerformAttack = function(type, target) | Attack.prototype.PerformAttack = function(type, target) | ||||
{ | { | ||||
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); | // Safety check, TODO find out if it is required. | ||||
if (!this.CanAttack(target, true, {}, [type])) | |||||
return; | |||||
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 cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | |||||
if (!cmpOwnership) | |||||
return; | |||||
let attackerOwner = cmpOwnership.GetOwner(); | |||||
// If this is a ranged attack, then launch a projectile | |||||
if (type == "Ranged") | |||||
{ | |||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | ||||
let data = { | |||||
"type": type, | |||||
"attackData": this.GetAttackEffectsData(type), | |||||
"attacker": this.entity, | |||||
"target": target, | |||||
"attackerOwner": attackerOwner, | |||||
"splash": this.GetSplashData(type) | |||||
}; | |||||
// When we have a projectile, launch it. | |||||
FreagarachUnsubmitted Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (this.template[type].Projectile) | |||||
{ | |||||
let turnLength = cmpTimer.GetLatestTurnLength()/1000; | let turnLength = cmpTimer.GetLatestTurnLength()/1000; | ||||
// In the future this could be extended: | // In the future this could be extended: | ||||
// * Obstacles like trees could reduce the probability of the target being hit | // * Obstacles like trees could reduce the probability of the target being hit | ||||
// * Obstacles like walls should block projectiles entirely | // * Obstacles like walls should block projectiles entirely | ||||
let horizSpeed = +this.template[type].Projectile.Speed; | let horizSpeed = +this.template[type].Projectile.Speed; | ||||
let gravity = +this.template[type].Projectile.Gravity; | 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()) | |||||
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 previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); | ||||
let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); | let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); | ||||
let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); | let timeToTarget = PositionHelper.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); | ||||
let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; | let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; | ||||
// Add inaccuracy based on spread. | // Add inaccuracy based on spread. | ||||
let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template[type].Projectile.Spread, this.entity) * | let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/" + type + "/Spread", +this.template[type].Projectile.Spread, this.entity) * | ||||
predictedPosition.horizDistanceTo(selfPosition) / 100; | predictedPosition.horizDistanceTo(selfPosition) / 100; | ||||
let randNorm = randomNormal2D(); | let randNorm = randomNormal2D(); | ||||
let offsetX = randNorm[0] * distanceModifiedSpread; | let offsetX = randNorm[0] * distanceModifiedSpread; | ||||
let offsetZ = randNorm[1] * distanceModifiedSpread; | let offsetZ = randNorm[1] * distanceModifiedSpread; | ||||
let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); | let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); | ||||
data.position = realTargetPosition; | |||||
// Recalculate when the missile will hit the target position. | // Recalculate when the missile will hit the target position. | ||||
let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); | let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); | ||||
timeToTarget = realHorizDistance / horizSpeed; | timeToTarget = realHorizDistance / horizSpeed; | ||||
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); | data.direction = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); | ||||
// Launch the graphical projectile. | // Launch the graphical projectile. | ||||
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); | let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); | ||||
let actorName = ""; | let actorName = ""; | ||||
let impactActorName = ""; | let impactActorName = ""; | ||||
let impactAnimationLifetime = 0; | let impactAnimationLifetime = 0; | ||||
Show All 13 Lines | if (cmpVisual) | ||||
if (!actorName) | if (!actorName) | ||||
actorName = cmpVisual.GetProjectileActor(); | actorName = cmpVisual.GetProjectileActor(); | ||||
let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint(); | let visualActorLaunchPoint = cmpVisual.GetProjectileLaunchPoint(); | ||||
if (visualActorLaunchPoint.length() > 0) | if (visualActorLaunchPoint.length() > 0) | ||||
launchPoint = visualActorLaunchPoint; | launchPoint = visualActorLaunchPoint; | ||||
} | } | ||||
let id = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); | data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, realTargetPosition, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); | ||||
let attackImpactSound = ""; | |||||
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); | let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); | ||||
if (cmpSound) | data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : ""; | ||||
attackImpactSound = cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()); | |||||
let data = { | data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true"; | ||||
"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", | |||||
}; | |||||
Not Done Inline Actions"immediately after" sounds a bit like an oxymoron. See discussion on D2016, I think rather this should be handled by a status effect. wraitii: "immediately after" sounds a bit like an oxymoron. See discussion on D2016, I think rather this… | |||||
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", +this.template[type].Delay + timeToTarget * 1000, data); | cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "MissileHit", timeToTarget * 1000 + (+this.template[type].Delay || 0), data); | ||||
} | } | ||||
// Close attack, hurt the target immediately after this.template.Delay | |||||
FreagarachUnsubmitted Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
else | else | ||||
Attacking.HandleAttackEffects(target, type, this.GetAttackEffectsData(type), this.entity, attackerOwner); | { | ||||
data.position = targetPosition; | |||||
data.direction = Vector3D.sub(targetPosition, selfPosition); | |||||
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "CauseAttackEffects", +(this.template[type].Delay || 0), data); | |||||
} | |||||
}; | }; | ||||
Attack.prototype.OnValueModification = function(msg) | Attack.prototype.OnValueModification = function(msg) | ||||
{ | { | ||||
if (msg.component != "Attack") | if (msg.component != "Attack") | ||||
return; | return; | ||||
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | ||||
Show All 27 Lines |
Wildfire Games · Phabricator
Perhaps nice to add the default value here.