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 78 Lines • ▼ Show 20 Lines | "<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>" + | ||||
"<RepeatTime>1000</RepeatTime>" + | "<RepeatTime>1000</RepeatTime>" + | ||||
"<MaxRange>4.0</MaxRange>" + | "<MaxRange>4.0</MaxRange>" + | ||||
"</Slaughter>" + | "</Slaughter>" + | ||||
"</a:example>" + | "</a:example>" + | ||||
"<oneOrMore>" + | "<oneOrMore>" + | ||||
Freagarach: Perhaps nice to add the default value here. | |||||
Done Inline Actionssure bb: sure | |||||
"<element>" + | "<element>" + | ||||
"<anyName a:help='Currently one of Melee, Ranged, Capture or Slaughter.'/>" + | "<anyName/>" + | ||||
"<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/>" + | ||||
Show All 32 Lines | "<element>" + | ||||
"<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>" + | ||||
AttackHelper.BuildAttackEffectsSchema() + | AttackHelper.BuildAttackEffectsSchema() + | ||||
"</interleave>" + | "</interleave>" + | ||||
"</element>" + | "</element>" + | ||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='Projectile'>" + | "<element name='Projectile'>" + | ||||
Not Done Inline ActionsIdem. Why is this optional anyway? Freagarach: Idem.
Why is this optional anyway? | |||||
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 | |||||
"<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'/>" + | ||||
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 ) | |||||
Done Inline ActionsBe my guest bb: Be my guest | |||||
"</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>" + | ||||
Show All 21 Lines | Attack.prototype.Schema = | ||||
"</oneOrMore>"; | "</oneOrMore>"; | ||||
Attack.prototype.Init = function() | Attack.prototype.Init = function() | ||||
{ | { | ||||
}; | }; | ||||
Attack.prototype.GetAttackTypes = function(wantedTypes) | Attack.prototype.GetAttackTypes = function(wantedTypes) | ||||
{ | { | ||||
let types = g_AttackTypes.filter(type => !!this.template[type]); | const types = Object.keys(this.template); | ||||
if (!wantedTypes) | if (!wantedTypes) | ||||
return types; | return types; | ||||
let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); | const 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)); | ||||
}; | }; | ||||
Attack.prototype.GetPreferredClasses = function(type) | Attack.prototype.GetPreferredClasses = function(type) | ||||
{ | { | ||||
if (this.template[type] && this.template[type].PreferredClasses && | if (this.template[type] && this.template[type].PreferredClasses && | ||||
this.template[type].PreferredClasses._string) | this.template[type].PreferredClasses._string) | ||||
return this.template[type].PreferredClasses._string.split(/\s+/); | return this.template[type].PreferredClasses._string.split(/\s+/); | ||||
return []; | return []; | ||||
}; | }; | ||||
Attack.prototype.GetRestrictedClasses = function(type) | Attack.prototype.GetRestrictedClasses = function(type) | ||||
{ | { | ||||
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 entityID 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[]} wantedTypes - List of (negated) attacktypes to allow. | |||||
* @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 entityID 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 allow. | |||||
* @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) | |||||
{ | { | ||||
const cmpFormation = Engine.QueryInterface(target, IID_Formation); | const 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; | |||||
} | |||||
const allowedAttackEffects = g_AttackEffects.Codes().filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]); | |||||
if (!allowedAttackEffects.length) | |||||
return []; | |||||
const types = this.GetAttackTypes(wantedTypes); | |||||
if (!types.length) | |||||
return []; | |||||
const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | const cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
const cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | const cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | ||||
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) | if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) | ||||
return false; | return []; | ||||
const cmpResistance = QueryMiragedInterface(target, IID_Resistance); | const cmpResistance = QueryMiragedInterface(target, IID_Resistance); | ||||
if (!cmpResistance) | if (!cmpResistance) | ||||
return false; | return []; | ||||
const cmpIdentity = QueryMiragedInterface(target, IID_Identity); | |||||
if (!cmpIdentity) | |||||
return false; | |||||
const cmpHealth = QueryMiragedInterface(target, IID_Health); | |||||
const targetClasses = cmpIdentity.GetClassesList(); | |||||
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && | |||||
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length || wantedTypes.indexOf("Slaughter") != -1)) | |||||
return true; | |||||
const cmpEntityPlayer = QueryOwnerInterface(this.entity); | const cmpEntityPlayer = QueryOwnerInterface(this.entity); | ||||
const cmpTargetPlayer = QueryOwnerInterface(target); | const cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity); | ||||
if (!cmpTargetPlayer || !cmpEntityPlayer) | if (!cmpEntityPlayer || !cmpTargetIdentity) | ||||
return false; | return []; | ||||
const types = this.GetAttackTypes(wantedTypes); | const targetClasses = cmpTargetIdentity.GetClassesList(); | ||||
const entityOwner = cmpEntityPlayer.GetPlayerID(); | const entityOwner = cmpEntityPlayer.GetPlayerID(); | ||||
const targetOwner = cmpTargetPlayer.GetPlayerID(); | |||||
const cmpCapturable = QueryMiragedInterface(target, IID_Capturable); | |||||
// Check if the relative height difference is larger than the attack range | const cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); | ||||
// If the relative height is bigger, it means they will never be able to | const heightDiff = cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset(); | ||||
// reach each other, no matter how close they come. | |||||
const heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); | |||||
for (const type of types) | return types.filter(type => { | ||||
if (mustBeInRange) | |||||
{ | { | ||||
Not Done Inline ActionsCan't this be moved to the position helper? Freagarach: Can't this be moved to the position helper? | |||||
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. | |||||
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) | const range = this.GetRange(type); | ||||
continue; | // Parabolic range compuation is the same as in and UnitAI's' MoveToTargetAttackRange and CheckTargetAttackRange. | ||||
// h is positive when I'm higher than the target. | |||||
const h = heightDiff + range.elevationBonus; | |||||
// TODO: merge D3249 | |||||
// In case the target is too high compared to us, we are out of range. | |||||
if (h <= -range.max / 2) | |||||
return false; | |||||
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) | if (!cmpObstructionManager.IsInTargetRange( | ||||
continue; | this.entity, | ||||
target, | |||||
range.min, | |||||
Math.sqrt(Math.square(range.max) + 2 * range.max * h), | |||||
false)) | |||||
return false; | |||||
} | |||||
if (heightDiff > this.GetRange(type).max) | const attackEffects = this.GetAttackEffectsData(type, false); | ||||
continue; | const attackEffectsSplash = this.GetAttackEffectsData(type, true); | ||||
const restrictedClasses = this.GetRestrictedClasses(type); | const bonusMultiplier = attackEffects && attackEffects.Bonuses ? | ||||
if (!restrictedClasses.length) | AttackHelper.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses) : | ||||
1; | |||||
const bonusMultiplierSplash = attackEffectsSplash && attackEffectsSplash.Bonuses ? | |||||
AttackHelper.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 => { | |||||
const receiver = g_AttackEffects.GetReceiverFromCode(effectType); | |||||
const cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); | |||||
if (!cmpReceiver) | |||||
return true; | return true; | ||||
if (!MatchesClassList(targetClasses, restrictedClasses)) | return ((!attackEffects[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, bonusMultiplier, entityOwner, allowedAttackEffects)) && | ||||
return true; | (!attackEffectsSplash[effectType] || !cmpReceiver[receiver.getRelativeEffectMethod](attackEffectsSplash, effectType, bonusMultiplierSplash, entityOwner, allowedAttackEffects))); | ||||
} | })) | ||||
return false; | |||||
if ((projectile == "required" && !this.template[type].Projectile) || (projectile == "disallowed" && this.template[type].Projectile)) | |||||
return false; | return false; | ||||
const restrictedClasses = this.GetRestrictedClasses(type); | |||||
return !restrictedClasses.length || !MatchesClassList(targetClasses, restrictedClasses); | |||||
}); | |||||
}; | }; | ||||
/** | /** | ||||
* Returns undefined if we have no preference or the lowest index of a preferred class. | * Returns undefined 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 34 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 AttackHelper.GetAttackEffectsData("Attack/" + type + (splash ? "/Splash" : ""), template, this.entity); | return AttackHelper.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) | ||||
{ | { | ||||
let cmpFormation = Engine.QueryInterface(target, IID_Formation); | // Work out, based on the preferences, which types are potentially possible. | ||||
if (cmpFormation) | 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); | |||||
if (!types.length) | |||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
return undefined; | |||||
//if (types.length == 1) | |||||
// return types[0]; | |||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
// Work out if there is any preference among the possible types with respect to | |||||
// prefTypes and projectile. We already know the given types satisfy the | |||||
// relevant mustBeInRange condition and the best possible ignoreAttackEffects. | |||||
// So we don't recheck on them to improve performance. | |||||
if (prefTypes && prefTypes.length) | |||||
{ | { | ||||
// TODO: Formation against formation needs review | const types2 = this.GetAllowedAttackTypes(target, false, {}, prefTypes.concat(types)) | ||||
let types = this.GetAttackTypes(); | if (types2.length) | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
return g_AttackTypes.find(attack => types.indexOf(attack) != -1); | types = types2; | ||||
} | } | ||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | //if (types.length == 1) | ||||
if (!cmpIdentity) | // return types[0]; | ||||
return undefined; | |||||
// Always slaughter domestic animals instead of using a normal attack | if (projectile) | ||||
if (this.template.Slaughter && cmpIdentity.HasClass("Domestic")) | { | ||||
return "Slaughter"; | const types2 = this.GetAllowedAttackTypes(target, false, {}, types, projectile) | ||||
if (types2.length) | |||||
let types = this.GetAttackTypes().filter(type => this.CanAttack(target, [type])); | types = types2; | ||||
// Check whether the target is capturable and prefer that when it is allowed. | |||||
let captureIndex = types.indexOf("Capture"); | |||||
if (captureIndex != -1) | |||||
{ | |||||
if (allowCapture) | |||||
return "Capture"; | |||||
types.splice(captureIndex, 1); | |||||
} | } | ||||
let targetClasses = cmpIdentity.GetClassesList(); | //if (types.length == 1) | ||||
let isPreferred = attackType => MatchesClassList(targetClasses, this.GetPreferredClasses(attackType)); | // return types[0]; | ||||
return types.sort((a, b) => | const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | ||||
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - | if (!cmpOwnership) | ||||
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); | return undefined; | ||||
}; | |||||
const distance = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).DistanceToTarget(this.entity, target); | |||||
if (distance < 0) | |||||
return undefined; | |||||
const owner = cmpOwnership.GetOwner(); | |||||
const fullRange = this.GetFullAttackRange(); | |||||
const allAttackEffects = g_AttackEffects.Codes(); | |||||
const consideredAttackEffects = allAttackEffects.filter(attackEffect => !ignoreAttackEffects || !ignoreAttackEffects[attackEffect]); | |||||
// Choose the best attack on a DPS/Range. | |||||
let bestType; | |||||
let bestDPSRange = -Infinity; | |||||
let bestAllEffectsDPSRange = -Infinity; | |||||
Done Inline ActionsAlso Spread bb: Also Spread | |||||
const cmpFormation = Engine.QueryInterface(target, IID_Formation); | |||||
if (cmpFormation) | |||||
{ | |||||
const members = cmpFormation.GetMembers(); | |||||
for (const type of types) | |||||
{ | |||||
let DPSRange = 0; | |||||
for (const member of members) | |||||
DPSRange += this.GetDPSRange(type, member, consideredAttackEffects, owner, distance, fullRange); | |||||
if (DPSRange > bestDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestDPSRange = DPSRange; | |||||
bestAllEffectsDPSRange = 0; | |||||
for (const member of members) | |||||
bestAllEffectsDPSRange += this.GetDPSRange(type, member, allAttackEffects, owner, distance, fullRange) | |||||
} | |||||
else if (DPSRange == bestDPSRange) | |||||
{ | |||||
let allEffectsDPSRange = 0; | |||||
for (const member of members) | |||||
allEffectsDPSRange += this.GetDPSRange(type, member, allAttackEffects, owner, distance, fullRange); | |||||
if (allEffectsDPSRange > bestAllEffectsDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestAllEffectsDPSRange = allEffectsDPSRange; | |||||
} | |||||
} | |||||
} | |||||
return bestType; | |||||
} | |||||
Attack.prototype.CompareEntitiesByPreference = function(a, b) | for (const type of types) | ||||
{ | |||||
const DPSRange = this.GetDPSRange(type, target, consideredAttackEffects, owner, distance, fullRange); | |||||
if (DPSRange > bestDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestDPSRange = DPSRange; | |||||
bestAllEffectsDPSRange = this.GetDPSRange(type, target, allAttackEffects, owner, distance, fullRange); | |||||
} | |||||
else if (DPSRange == bestDPSRange) | |||||
{ | |||||
const allEffectsDPSRange = this.GetDPSRange(type, target, allAttackEffects, owner, distance, fullRange); | |||||
if (allEffectsDPSRange > bestAllEffectsDPSRange) | |||||
{ | |||||
bestType = type; | |||||
bestAllEffectsDPSRange = allEffectsDPSRange; | |||||
} | |||||
} | |||||
} | |||||
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 aPreference = this.GetPreference(a); | let DPSRange = 0; | ||||
let bPreference = this.GetPreference(b); | const attackEffects = this.GetAttackEffectsData(type, false); | ||||
const multiplier = AttackHelper.GetAttackBonus(this.entity, target, type, attackEffects.Bonuses || {}); | |||||
for (const effectType of consideredAttackEffects) | |||||
{ | |||||
if (!attackEffects[effectType]) | |||||
continue; | |||||
const receiver = g_AttackEffects.GetReceiverFromCode(effectType); | |||||
const cmpReceiver = QueryMiragedInterface(target, global[receiver.IID]); | |||||
if (!cmpReceiver) | |||||
continue; | |||||
if (aPreference === null && bPreference === null) return 0; | DPSRange += cmpReceiver[receiver.getRelativeEffectMethod](attackEffects, effectType, multiplier, owner, consideredAttackEffects); | ||||
if (aPreference === null) return 1; | } | ||||
if (bPreference === null) return -1; | DPSRange /= this.GetRepeatTime(type); | ||||
return aPreference - bPreference; | |||||
// Apply an exponential dropoff when out of range. | |||||
// TODO elevation? | |||||
const range = this.GetRange(type); | |||||
if (distance < range.min) | |||||
DPSRange *= Math.pow(0.2, (range.min - distance) / fullRange.min); | |||||
else if (distance > range.max) | |||||
DPSRange *= Math.pow(0.2, (distance - range.max) / (fullRange.max || 1)); | |||||
return DPSRange; | |||||
}; | }; | ||||
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"] | ||||
}; | }; | ||||
}; | }; | ||||
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 36 Lines | |||||
* | * | ||||
* @return {boolean} - Whether we started attacking. | * @return {boolean} - Whether we started attacking. | ||||
*/ | */ | ||||
Attack.prototype.StartAttacking = function(target, type, callerIID) | Attack.prototype.StartAttacking = function(target, type, callerIID) | ||||
{ | { | ||||
if (this.target) | if (this.target) | ||||
this.StopAttacking(); | this.StopAttacking(); | ||||
if (!this.CanAttack(target, [type])) | // We should be in range, but requiring that here yields an infinite loop: | ||||
// unitAI might keep trying to attack this entity from the idle state. | |||||
// TODO: figure our why we are not in range in this case. | |||||
Not Done Inline ActionsMaybe a difference between moveToRange and isInRange? Freagarach: Maybe a difference between moveToRange and isInRange? | |||||
if (!this.CanAttack(target, false, {}, [type])) | |||||
return false; | return false; | ||||
const cmpResistance = QueryMiragedInterface(target, IID_Resistance); | const cmpResistance = QueryMiragedInterface(target, IID_Resistance); | ||||
if (!cmpResistance || !cmpResistance.AddAttacker(this.entity)) | if (!cmpResistance || !cmpResistance.AddAttacker(this.entity)) | ||||
return false; | return false; | ||||
let timings = this.GetTimers(type); | let timings = this.GetTimers(type); | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | ||||
▲ Show 20 Lines • Show All 61 Lines • ▼ Show 20 Lines | |||||
/** | /** | ||||
* Attack our target entity. | * Attack our target entity. | ||||
* @param {string} data - The attack type to use. | * @param {string} data - The attack type to use. | ||||
* @param {number} lateness - The offset of the actual call and when it was expected. | * @param {number} lateness - The offset of the actual call and when it was expected. | ||||
*/ | */ | ||||
Attack.prototype.Attack = function(type, lateness) | Attack.prototype.Attack = function(type, lateness) | ||||
{ | { | ||||
if (!this.CanAttack(this.target, [type])) | // We will check the range after rather than before the attack to facilitate chasing. | ||||
if (!this.CanAttack(this.target, false, {}, [type])) | |||||
{ | { | ||||
this.StopAttacking("TargetInvalidated"); | this.StopAttacking("TargetInvalidated"); | ||||
return; | return; | ||||
} | } | ||||
// ToDo: Enable entities to keep facing a target. | // ToDo: Enable entities to keep facing a target. | ||||
Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); | Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | ||||
this.lastAttacked = cmpTimer.GetTime() - lateness; | this.lastAttacked = cmpTimer.GetTime() - lateness; | ||||
// BuildingAI has its own attack routine. | // BuildingAI has its own attack routine. | ||||
if (!Engine.QueryInterface(this.entity, IID_BuildingAI)) | if (!Engine.QueryInterface(this.entity, IID_BuildingAI)) | ||||
this.PerformAttack(type, this.target); | this.PerformAttack(type, this.target); | ||||
if (!this.target) | if (!this.target) | ||||
return; | return; | ||||
// We check the range after the attack to facilitate chasing. | |||||
if (!this.IsTargetInRange(this.target, type)) | if (!this.IsTargetInRange(this.target, type)) | ||||
{ | { | ||||
this.StopAttacking("OutOfRange"); | this.StopAttacking("OutOfRange"); | ||||
return; | return; | ||||
} | } | ||||
if (this.resyncAnimation) | if (this.resyncAnimation) | ||||
{ | { | ||||
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); | let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); | ||||
if (cmpVisual) | if (cmpVisual) | ||||
{ | { | ||||
let repeat = this.GetTimers(type).repeat; | let repeat = this.GetTimers(type).repeat; | ||||
cmpVisual.SetAnimationSyncRepeat(repeat); | cmpVisual.SetAnimationSyncRepeat(repeat); | ||||
cmpVisual.SetAnimationSyncOffset(repeat); | cmpVisual.SetAnimationSyncOffset(repeat); | ||||
} | } | ||||
delete this.resyncAnimation; | delete this.resyncAnimation; | ||||
} | } | ||||
}; | }; | ||||
/** | /** | ||||
* Attack the target entity. This should only be called after a successful range check, | * Attack the target entity. This should only be called after successful range and | ||||
* and should only be called after GetTimers().repeat msec has passed since the last | * possibility check, and should only be called after GetTimers().repeat msec has | ||||
* call to PerformAttack. | * passed since the last call to PerformAttack. | ||||
*/ | */ | ||||
Attack.prototype.PerformAttack = function(type, target) | Attack.prototype.PerformAttack = function(type, target) | ||||
{ | { | ||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
if (!cmpPosition || !cmpPosition.IsInWorld()) | if (!cmpPosition || !cmpPosition.IsInWorld()) | ||||
return; | return; | ||||
let selfPosition = cmpPosition.GetPosition(); | let selfPosition = cmpPosition.GetPosition(); | ||||
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | ||||
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) | if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) | ||||
return; | return; | ||||
let targetPosition = cmpTargetPosition.GetPosition(); | let targetPosition = cmpTargetPosition.GetPosition(); | ||||
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | ||||
if (!cmpOwnership) | if (!cmpOwnership) | ||||
return; | return; | ||||
let attackerOwner = cmpOwnership.GetOwner(); | let attackerOwner = cmpOwnership.GetOwner(); | ||||
let data = { | let data = { | ||||
"type": type, | "type": type, | ||||
"attackData": this.GetAttackEffectsData(type), | "attackData": this.GetAttackEffectsData(type), | ||||
"splash": this.GetSplashData(type), | "splash": this.GetSplashData(type), | ||||
"attacker": this.entity, | "attacker": this.entity, | ||||
"attackerOwner": attackerOwner, | "attackerOwner": attackerOwner, | ||||
"target": target, | "target": target, | ||||
}; | }; | ||||
let delay = +(this.template[type].Delay || 0); | let delay = +(this.template[type].Delay || 0); | ||||
if (this.template[type].Projectile) | if (this.template[type].Projectile) | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
{ | { | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | ||||
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; | ||||
▲ Show 20 Lines • Show All 76 Lines • ▼ Show 20 Lines | if (this.template[type].Projectile) | ||||
let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); | let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); | ||||
data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, data.position, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); | data.projectileId = cmpProjectileManager.LaunchProjectileAtPoint(launchPoint, data.position, horizSpeed, gravity, actorName, impactActorName, impactAnimationLifetime); | ||||
let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); | let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); | ||||
data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : ""; | data.attackImpactSound = cmpSound ? cmpSound.GetSoundGroup("attack_impact_" + type.toLowerCase()) : ""; | ||||
data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true"; | data.friendlyFire = this.template[type].Projectile.FriendlyFire == "true"; | ||||
} | } | ||||
else | else | ||||
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… | |||||
{ | { | ||||
data.position = targetPosition; | data.position = targetPosition; | ||||
data.direction = Vector3D.sub(targetPosition, selfPosition); | data.direction = Vector3D.sub(targetPosition, selfPosition); | ||||
} | } | ||||
if (delay) | if (delay) | ||||
{ | { | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | ||||
cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "Hit", delay, data); | cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_DelayedDamage, "Hit", delay, data); | ||||
} | } | ||||
else | else | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
Engine.QueryInterface(SYSTEM_ENTITY, IID_DelayedDamage).Hit(data, 0); | Engine.QueryInterface(SYSTEM_ENTITY, IID_DelayedDamage).Hit(data, 0); | ||||
}; | }; | ||||
/** | /** | ||||
* @param {number} - The entity ID of the target to check. | * @param {number} - The entity ID of the target to check. | ||||
* @return {boolean} - Whether this entity is in range of its target. | * @return {boolean} - Whether this entity is in range of its target. | ||||
*/ | */ | ||||
Attack.prototype.IsTargetInRange = function(target, type) | Attack.prototype.IsTargetInRange = function(target, type) | ||||
▲ Show 20 Lines • Show All 58 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator
Perhaps nice to add the default value here.