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.statusEffectsSchema = | Attack.prototype.statusEffectsSchema = | ||||
"<optional>" + | "<optional>" + | ||||
"<element name='StatusEffects' a:help='Effects like poisioning or burning a unit.'>" + | "<element name='StatusEffects' a:help='Effects like poisioning or burning a unit.'>" + | ||||
"<oneOrMore>" + | "<oneOrMore>" + | ||||
"<element>" + | "<element>" + | ||||
"<anyName/>" + | "<anyName/>" + | ||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" + | "<element name='Duration' a:help='The duration of the status while the effect occurs.'><ref name='nonNegativeDecimal'/></element>" + | ||||
▲ Show 20 Lines • Show All 108 Lines • ▼ Show 20 Lines | "<a:example>" + | ||||
"<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>" + | ||||
"</Slaughter>" + | "</Slaughter>" + | ||||
"</a:example>" + | "</a:example>" + | ||||
"<optional>" + | |||||
"<element name='Melee'>" + | |||||
"<interleave>" + | "<interleave>" + | ||||
Freagarach: Perhaps nice to add the default value here. | |||||
Done Inline Actionssure bb: sure | |||||
"<element name='Damage'>" + | "<zeroOrMore>" + | ||||
DamageTypes.BuildSchema("damage strength") + | "<element>" + | ||||
"</element>" + | "<anyName/>" + | ||||
"<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.bonusesSchema + | |||||
Attack.prototype.preferredClassesSchema + | |||||
Attack.prototype.restrictedClassesSchema + | |||||
"</interleave>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Ranged'>" + | |||||
"<interleave>" + | "<interleave>" + | ||||
"<element name='TooltipHeader'>" + | |||||
"<text/>" + | |||||
"</element>" + | |||||
"<element name='Damage'>" + | "<element name='Damage'>" + | ||||
DamageTypes.BuildSchema("damage strength") + | DamageTypes.BuildSchema("damage strength") + | ||||
"</element>" + | "</element>" + | ||||
"<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>" + | ||||
"</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>" + | |||||
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 | |||||
"<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'>" + | ||||
"<data type='nonNegativeInteger'/>" + | "<data type='nonNegativeInteger'/>" + | ||||
"</element>" + | "</element>" + | ||||
"<element name='RepeatTime' a:help='Time between attacks (in milliseconds). The attack animation will be stretched to match this time'>" + | "</optional>" + | ||||
"<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'/>" + | "<data type='positiveInteger'/>" + | ||||
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>" + | ||||
"<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'>" + | ||||
"<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>" + | ||||
"<element name='Damage'>" + | "<element name='Damage'>" + | ||||
DamageTypes.BuildSchema("damage strength") + | DamageTypes.BuildSchema("damage strength") + | ||||
"</element>" + | "</element>" + | ||||
Attack.prototype.bonusesSchema + | Attack.prototype.bonusesSchema + | ||||
"</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>" + | ||||
"<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.statusEffectsSchema + | |||||
Attack.prototype.bonusesSchema + | |||||
Attack.prototype.preferredClassesSchema + | |||||
Attack.prototype.restrictedClassesSchema + | |||||
"</interleave>" + | |||||
"</element>" + | |||||
"</optional>" + | "</optional>" + | ||||
"<optional>" + | Attack.prototype.statusEffectsSchema + | ||||
"<element name='Capture'>" + | |||||
"<interleave>" + | |||||
"<element name='Value' a:help='Capture points value'><ref name='nonNegativeDecimal'/></element>" + | |||||
"<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>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Slaughter' a:help='A special attack to kill domestic animals'>" + | |||||
"<interleave>" + | |||||
"<element name='Damage'>" + | |||||
DamageTypes.BuildSchema("damage strength") + | |||||
"</element>" + | |||||
"<element name='MaxRange'><ref name='nonNegativeDecimal'/></element>" + // TODO: how do these work? | |||||
Attack.prototype.bonusesSchema + | Attack.prototype.bonusesSchema + | ||||
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 12 Lines | 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) | Attack.prototype.CanAttack = function(target, wantedTypes) | ||||
{ | { | ||||
return this.GetAllowedAttackTypes(target, wantedTypes).length > 0; | |||||
}; | |||||
Attack.prototype.GetAllowedAttackTypes = function(target, wantedTypes) | |||||
{ | |||||
let types = this.GetAttackTypes(wantedTypes); | |||||
let cmpFormation = Engine.QueryInterface(target, IID_Formation); | let cmpFormation = Engine.QueryInterface(target, IID_Formation); | ||||
if (cmpFormation) | if (cmpFormation) | ||||
return true; | return types; | ||||
let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | ||||
if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) | if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) | ||||
return false; | return []; | ||||
let isTurret = cmpThisPosition.GetTurretParent() != INVALID_ENTITY | |||||
let cmpIdentity = QueryMiragedInterface(target, IID_Identity); | |||||
if (!cmpIdentity) | |||||
return false; | |||||
let cmpHealth = QueryMiragedInterface(target, IID_Health); | let cmpTargetIdentity = QueryMiragedInterface(target, IID_Identity); | ||||
let targetClasses = cmpIdentity.GetClassesList(); | if (!cmpTargetIdentity) | ||||
if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && cmpHealth && cmpHealth.GetHitpoints() && | return []; | ||||
(!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) | let targetClasses = cmpTargetIdentity.GetClassesList(); | ||||
return true; | |||||
let cmpEntityPlayer = QueryOwnerInterface(this.entity); | let cmpEntityPlayer = QueryOwnerInterface(this.entity); | ||||
let cmpTargetPlayer = QueryOwnerInterface(target); | if (!cmpEntityPlayer) | ||||
if (!cmpTargetPlayer || !cmpEntityPlayer) | return []; | ||||
return false; | |||||
let types = this.GetAttackTypes(wantedTypes); | |||||
let entityOwner = cmpEntityPlayer.GetPlayerID(); | let entityOwner = cmpEntityPlayer.GetPlayerID(); | ||||
let targetOwner = cmpTargetPlayer.GetPlayerID(); | |||||
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); | let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); | ||||
// Check if the relative height difference is larger than the attack range | // Check if the relative height difference is larger than the attack range | ||||
// If the relative height is bigger, it means they will never be able to | // If the relative height is bigger, it means they will never be able to | ||||
// reach each other, no matter how close they come. | // reach each other, no matter how close they come. | ||||
let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); | let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); | ||||
for (let type of types) | return types.filter(type => { | ||||
{ | //TODO splash? | ||||
if (type != "Capture" && (!cmpEntityPlayer.IsEnemy(targetOwner) || !cmpHealth || !cmpHealth.GetHitpoints())) | if (Object.keys(this.template[type].Damage).every(damageType => { | ||||
continue; | if (!this.template[type].Damage[damageType]) | ||||
return true; | |||||
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. | |||||
let cmp = QueryMiragedInterface(target, DamageTypes.GetComponents()[damageType]); | |||||
return !cmp || !cmp.CanAttack(entityOwner); | |||||
})) | |||||
return false; | |||||
if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) | // If we are visisble garrisoned always do attacks with projectiles. | ||||
continue; | if (isTurret && this.GetRange(type).max < cmpObstructionManager.DistanceToTarget(this.entity, target)) | ||||
return false; | |||||
if (heightDiff > this.GetRange(type).max) | if (heightDiff > this.GetRange(type).max) | ||||
continue; | return false; | ||||
let restrictedClasses = this.GetRestrictedClasses(type); | let restrictedClasses = this.GetRestrictedClasses(type); | ||||
if (!restrictedClasses.length) | if (!restrictedClasses.length) | ||||
return true; | return true; | ||||
if (!MatchesClassList(targetClasses, restrictedClasses)) | if (!MatchesClassList(targetClasses, restrictedClasses)) | ||||
return true; | return true; | ||||
} | }); | ||||
return false; | |||||
}; | }; | ||||
/** | /** | ||||
* 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 28 Lines | Attack.prototype.GetFullAttackRange = function() | ||||
{ | { | ||||
let range = this.GetRange(type); | let range = this.GetRange(type); | ||||
ret.min = Math.min(ret.min, range.min); | ret.min = Math.min(ret.min, range.min); | ||||
ret.max = Math.max(ret.max, range.max); | ret.max = Math.max(ret.max, range.max); | ||||
} | } | ||||
return ret; | return ret; | ||||
}; | }; | ||||
Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) | Attack.prototype.GetBestAttackAgainst = function(target, prefAttackTypes) | ||||
{ | { | ||||
let cmpFormation = Engine.QueryInterface(target, IID_Formation); | let cmpFormation = Engine.QueryInterface(target, IID_Formation); | ||||
if (cmpFormation) | if (cmpFormation) | ||||
{ | { | ||||
// TODO: Formation against formation needs review | // TODO: Formation against formation needs review | ||||
let types = this.GetAttackTypes(); | let types = this.GetAttackTypes(prefAttackTypes); | ||||
return g_AttackTypes.find(attack => types.indexOf(attack) != -1); | return types.length ? types[0] : undefined; | ||||
} | } | ||||
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | |||||
if (!cmpPosition || !cmpPosition.IsInWorld()) | |||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
return undefined; | |||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | ||||
if (!cmpIdentity) | if (!cmpIdentity) | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
return undefined; | return undefined; | ||||
let targetClasses = cmpIdentity.GetClassesList(); | let targetClasses = cmpIdentity.GetClassesList(); | ||||
let isTargetClass = className => targetClasses.indexOf(className) != -1; | let isTargetClass = className => targetClasses.indexOf(className) != -1; | ||||
// Always slaughter domestic animals instead of using a normal attack | // Try find an allowed attack preferred by prefAttackTypes, if none exists try all attack types in the template | ||||
if (isTargetClass("Domestic") && this.template.Slaughter) | let types = this.GetAllowedAttackTypes(target, prefAttackTypes); | ||||
return "Slaughter"; | if (!types.length) | ||||
types = this.GetAllowedAttackTypes(target); | |||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (!types.length) | |||||
return undefined; | |||||
let types = this.GetAttackTypes().filter(type => !this.GetRestrictedClasses(type).some(isTargetClass)); | // For performance: don't worry calculating the best when there is only one | ||||
if (types.length == 1) | |||||
return types[0]; | |||||
// check if the target is capturable | let cmpDamageReceiver = QueryMiragedInterface(target, IID_DamageReceiver); | ||||
let captureIndex = types.indexOf("Capture"); | if (!cmpDamageReceiver) | ||||
if (captureIndex != -1) | return undefined; | ||||
{ | |||||
let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); | |||||
let cmpPlayer = QueryOwnerInterface(this.entity); | let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); | ||||
if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) | let distance = cmpObstructionManager.DistanceToTarget(this.entity, target); | ||||
return "Capture"; | let fullRange = this.GetFullAttackRange(); | ||||
// not capturable, so remove this attack | // Choose the best attack on a DPS/Range | ||||
types.splice(captureIndex, 1); | let bestType; | ||||
let bestDPSRange = -Infinity; | |||||
for (let type of types) | |||||
{ | |||||
let DPSRange = 0; | |||||
let damage = cmpDamageReceiver.GetDamage(this.GetAttackStrengths(type), GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type))); | |||||
for (let cmp in damage) | |||||
{ | |||||
let cmpReciever = QueryMiragedInterface(target, +cmp); | |||||
if (!cmpReciever) | |||||
continue; | |||||
DPSRange += damage[cmp] / cmpReciever.GetMax() / this.GetTimers(type).repeat; | |||||
} | } | ||||
let isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass); | // TODO elevation? | ||||
bbAuthorUnsubmitted Done Inline ActionsAlso Spread bb: Also Spread | |||||
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) => | if (DPSRange > bestDPSRange) | ||||
(types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - | { | ||||
(types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); | bestType = type; | ||||
bestDPSRange = DPSRange; | |||||
} | |||||
} | |||||
return bestType | |||||
}; | }; | ||||
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.GetTimers = function(type) | Attack.prototype.GetTimers = function(type) | ||||
{ | { | ||||
let prepare = +(this.template[type].PrepareTime || 0); | return { | ||||
prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); | "prepare": ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", +(this.template[type].PrepareTime || 0), this.entity), | ||||
"repeat": ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", +this.template[type].RepeatTime, this.entity) | |||||
let repeat = +(this.template[type].RepeatTime || 1000); | }; | ||||
repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); | |||||
return { "prepare": prepare, "repeat": repeat }; | |||||
}; | }; | ||||
Attack.prototype.GetAttackStrengths = function(type) | Attack.prototype.GetAttackStrengths = function(type) | ||||
{ | { | ||||
// Work out the attack values with technology effects | // Work out the attack values with technology effects | ||||
let template = this.template[type]; | let template = this.template[type]; | ||||
let splash = ""; | let splash = ""; | ||||
if (!template) | if (!template) | ||||
{ | { | ||||
template = this.template[type.split(".")[0]].Splash; | template = this.template[type.split(".")[0]].Splash; | ||||
splash = "/Splash"; | splash = "/Splash"; | ||||
} | } | ||||
let applyMods = damageType => | let applyMods = damageType => | ||||
ApplyValueModificationsToEntity("Attack/" + type + splash + "/Damage/" + damageType, +(template.Damage[damageType] || 0), this.entity); | 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 = {}; | let ret = {}; | ||||
for (let damageType of DamageTypes.GetTypes()) | for (let damageType of DamageTypes.GetTypes()) | ||||
ret[damageType] = applyMods(damageType); | ret[damageType] = applyMods(damageType); | ||||
return ret; | return ret; | ||||
}; | }; | ||||
Attack.prototype.GetSplashDamage = function(type) | Attack.prototype.GetSplashDamage = function(type) | ||||
Show All 20 Lines | Attack.prototype.GetRange = function(type) | ||||
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.prototype.GetBonusTemplate = function(type) | Attack.prototype.GetBonusTemplate = function(type) | ||||
{ | { | ||||
let template = this.template[type]; | let template = this.template[type]; | ||||
if (!template) | if (!template) | ||||
Not Done Inline ActionsMaybe a difference between moveToRange and isInRange? Freagarach: Maybe a difference between moveToRange and isInRange? | |||||
template = this.template[type.split(".")[0]].Splash; | template = this.template[type.split(".")[0]].Splash; | ||||
return template.Bonuses || null; | return template.Bonuses || null; | ||||
}; | }; | ||||
Attack.prototype.GetTooltipHeader = function(type) | |||||
{ | |||||
return this.template[type] ? this.template[type].TooltipHeader : ""; | |||||
}; | |||||
Attack.prototype.HasProjectile = function(type) | |||||
{ | |||||
return !!(this.template[type] && this.template[type].Projectile); | |||||
}; | |||||
/** | /** | ||||
* 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 | ||||
* call to PerformAttack. | * call to PerformAttack. | ||||
*/ | */ | ||||
Attack.prototype.PerformAttack = function(type, target) | Attack.prototype.PerformAttack = function(type, target) | ||||
{ | { | ||||
// Safety check, TODO find out if it is required | |||||
if (!this.CanAttack(target, [type])) | |||||
return; | |||||
let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); | let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); | ||||
let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); | let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | |||||
let data = { | |||||
"type": type, | |||||
"attacker": this.entity, | |||||
"target": target, | |||||
"strengths": this.GetAttackStrengths(type), | |||||
"isSplash": false, | |||||
"attackerOwner": attackerOwner, | |||||
"statusEffects": this.template[type].StatusEffects | |||||
}; | |||||
// If this is a ranged attack, then launch a projectile | if (this.template[type].Splash) | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
if (type == "Ranged") | |||||
{ | { | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | data.friendlyFire = this.template[type].Splash.FriendlyFire != "false"; | ||||
data.isSplash = true; | |||||
data.radius = +this.template[type].Splash.Range; | |||||
data.shape = this.template[type].Splash.Shape; | |||||
data.splashStrengths = this.GetAttackStrengths(type + ".Splash"); | |||||
data.splashBonus = this.GetBonusTemplate(type + ".Splash"); | |||||
} | |||||
// When we have a projectile, launch it. | |||||
if (this.template[type].Projectile) | |||||
{ | |||||
data.bonus = this.GetBonusTemplate(type); | |||||
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 | ||||
Show All 9 Lines | if (this.template[type].Projectile) | ||||
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 = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); | let timeToTarget = this.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); | ||||
data.direction = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); | |||||
timeToTarget = realHorizDistance / horizSpeed; | timeToTarget = realHorizDistance / horizSpeed; | ||||
let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); | let missileDirection = 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 = ""; | ||||
Show All 16 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 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 = { | cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000 + +(this.template[type].Delay || 0), data); | ||||
"type": type, | |||||
"attacker": this.entity, | |||||
"target": target, | |||||
"strengths": this.GetAttackStrengths(type), | |||||
"position": realTargetPosition, | |||||
"direction": missileDirection, | |||||
"projectileId": id, | |||||
"bonus": this.GetBonusTemplate(type), | |||||
"isSplash": false, | |||||
"attackerOwner": attackerOwner, | |||||
"attackImpactSound": attackImpactSound, | |||||
"statusEffects": this.template[type].StatusEffects | |||||
}; | |||||
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") | // Close attack, hurt the target immediately after this.template.Delay | ||||
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… | |||||
{ | { | ||||
if (attackerOwner == INVALID_PLAYER) | data.multiplier = GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)); | ||||
return; | |||||
let multiplier = GetDamageBonus(this.entity, target, type, this.GetBonusTemplate(type)); | if (this.template[type].Splash) | ||||
let cmpHealth = Engine.QueryInterface(target, IID_Health); | { | ||||
if (!cmpHealth || cmpHealth.GetHitpoints() == 0) | let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); | ||||
if (!cmpPosition || !cmpPosition.IsInWorld()) | |||||
return; | return; | ||||
multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); | let selfPosition = cmpPosition.GetPosition(); | ||||
let cmpCapturable = Engine.QueryInterface(target, IID_Capturable); | let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); | ||||
if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner)) | if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) | ||||
return; | return; | ||||
let targetPosition = cmpTargetPosition.GetPosition(); | |||||
let strength = this.GetAttackStrengths("Capture").value * multiplier; | data.position = targetPosition; | ||||
if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target)) | data.direction = new Vector3D.sub(targetPosition, selfPosition); | ||||
Engine.PostMessage(target, MT_Attacked, { | |||||
"attacker": this.entity, | |||||
"target": target, | |||||
"type": type, | |||||
"damage": strength, | |||||
"attackerOwner": attackerOwner | |||||
}); | |||||
} | } | ||||
else | |||||
{ | cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "CauseDamage", +(this.template[type].Delay || 0), data); | ||||
Not Done Inline ActionsNuke. Freagarach: Nuke. | |||||
// 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 | |||||
}); | |||||
} | } | ||||
}; | }; | ||||
/** | /** | ||||
* Get the predicted time of collision between a projectile (or a chaser) | * Get the predicted time of collision between a projectile (or a chaser) | ||||
* and its target, assuming they both move in straight line at a constant speed. | * and its target, assuming they both move in straight line at a constant speed. | ||||
* Vertical component of movement is ignored. | * Vertical component of movement is ignored. | ||||
* @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser). | * @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser). | ||||
▲ Show 20 Lines • Show All 58 Lines • Show Last 20 Lines |
Wildfire Games · Phabricator
Perhaps nice to add the default value here.