2010-02-05 14:00:39 -08:00
function Attack ( ) { }
2017-05-07 12:32:59 -07:00
var g _AttackTypes = [ "Melee" , "Ranged" , "Capture" ] ;
2014-10-14 13:44:17 -07:00
Attack . prototype . preferredClassesSchema =
2012-05-01 15:20:08 -07:00
"<optional>" +
2025-04-10 21:24:32 -07:00
"<element name='PreferredClasses' a:help='Space delimited list of classes preferred for attacking. If an entity has any of these classes, it is preferred. The classes are in descending order of preference'>" +
2012-05-01 15:20:08 -07:00
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>" ;
2014-10-14 13:44:17 -07:00
Attack . prototype . restrictedClassesSchema =
2012-05-01 15:20:08 -07:00
"<optional>" +
"<element name='RestrictedClasses' a:help='Space delimited list of classes that cannot be attacked by this entity. If target entity has any of these classes, it cannot be attacked'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>" ;
2010-04-09 12:02:39 -07:00
Attack . prototype . Schema =
2010-04-23 09:09:03 -07:00
"<a:help>Controls the attack abilities and strengths of the unit.</a:help>" +
"<a:example>" +
2010-05-15 14:07:52 -07:00
"<Melee>" +
2020-11-18 13:34:33 -08:00
"<AttackName>Spear</AttackName>" +
2019-07-05 11:15:42 -07:00
"<Damage>" +
"<Hack>10.0</Hack>" +
"<Pierce>0.0</Pierce>" +
"<Crush>5.0</Crush>" +
"</Damage>" +
2010-05-15 14:07:52 -07:00
"<MaxRange>4.0</MaxRange>" +
"<RepeatTime>1000</RepeatTime>" +
2011-12-23 19:49:30 -08:00
"<Bonuses>" +
"<Bonus1>" +
2026-04-19 17:17:38 -07:00
"<Civ>achae</Civ>" +
2011-12-23 19:49:30 -08:00
"<Classes>Infantry</Classes>" +
"<Multiplier>1.5</Multiplier>" +
"</Bonus1>" +
"<BonusCavMelee>" +
"<Classes>Cavalry Melee</Classes>" +
"<Multiplier>1.5</Multiplier>" +
"</BonusCavMelee>" +
"</Bonuses>" +
2012-05-01 15:20:08 -07:00
"<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
"<PreferredClasses datatype=\"tokens\">Cavalry Infantry</PreferredClasses>" +
2010-05-15 14:07:52 -07:00
"</Melee>" +
"<Ranged>" +
2020-11-18 13:34:33 -08:00
"<AttackName>Bow</AttackName>" +
2019-07-05 11:15:42 -07:00
"<Damage>" +
"<Hack>0.0</Hack>" +
"<Pierce>10.0</Pierce>" +
"<Crush>0.0</Crush>" +
"</Damage>" +
2010-05-15 14:07:52 -07:00
"<MaxRange>44.0</MaxRange>" +
"<MinRange>20.0</MinRange>" +
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
"<Origin>" +
"<X>0</X>" +
"<Y>10.0</Y>" +
"<Z>0</Z>" +
"</Origin>" +
2010-05-15 14:07:52 -07:00
"<PrepareTime>800</PrepareTime>" +
"<RepeatTime>1600</RepeatTime>" +
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
"<EffectDelay>1000</EffectDelay>" +
2011-12-23 19:49:30 -08:00
"<Bonuses>" +
"<Bonus1>" +
"<Classes>Cavalry</Classes>" +
"<Multiplier>2</Multiplier>" +
"</Bonus1>" +
"</Bonuses>" +
2017-12-23 01:27:19 -08:00
"<Projectile>" +
2019-04-13 02:27:14 -07:00
"<Speed>50.0</Speed>" +
"<Spread>2.5</Spread>" +
2017-12-23 01:27:19 -08:00
"<ActorName>props/units/weapons/rock_flaming.xml</ActorName>" +
"<ImpactActorName>props/units/weapons/rock_explosion.xml</ImpactActorName>" +
"<ImpactAnimationLifetime>0.1</ImpactAnimationLifetime>" +
2020-03-07 02:39:05 -08:00
"<FriendlyFire>false</FriendlyFire>" +
2017-12-23 01:27:19 -08:00
"</Projectile>" +
2012-05-01 15:20:08 -07:00
"<RestrictedClasses datatype=\"tokens\">Champion</RestrictedClasses>" +
2012-05-19 16:07:41 -07:00
"<Splash>" +
"<Shape>Circular</Shape>" +
"<Range>20</Range>" +
"<FriendlyFire>false</FriendlyFire>" +
2019-07-05 11:15:42 -07:00
"<Damage>" +
"<Hack>0.0</Hack>" +
"<Pierce>10.0</Pierce>" +
"<Crush>0.0</Crush>" +
"</Damage>" +
2012-05-19 16:07:41 -07:00
"</Splash>" +
2010-05-15 14:07:52 -07:00
"</Ranged>" +
2012-09-24 12:31:31 -07:00
"<Slaughter>" +
2019-07-05 11:15:42 -07:00
"<Damage>" +
"<Hack>1000.0</Hack>" +
"<Pierce>0.0</Pierce>" +
"<Crush>0.0</Crush>" +
"</Damage>" +
2021-04-11 23:17:13 -07:00
"<RepeatTime>1000</RepeatTime>" +
2012-09-24 12:31:31 -07:00
"<MaxRange>4.0</MaxRange>" +
"</Slaughter>" +
2010-04-23 09:09:03 -07:00
"</a:example>" +
2021-04-11 23:17:13 -07:00
"<oneOrMore>" +
"<element>" +
"<anyName a:help='Currently one of Melee, Ranged, Capture or Slaughter.'/>" +
2010-05-15 14:07:52 -07:00
"<interleave>" +
2025-11-16 01:34:53 -08:00
"<element name='AttackName' a:help='Name of the attack, to be displayed in the GUI.'>" +
2020-11-18 13:34:33 -08:00
"<optional>" +
"<attribute name='context'>" +
"<text/>" +
"</attribute>" +
"</optional>" +
2025-11-16 01:34:53 -08:00
"<optional>" +
"<attribute name='comment'>" +
"<text/>" +
"</attribute>" +
"</optional>" +
2020-11-18 13:34:33 -08:00
"<text/>" +
"</element>" +
2021-04-15 23:55:23 -07:00
AttackHelper . BuildAttackEffectsSchema ( ) +
2025-04-10 21:24:32 -07:00
"<element name='MaxRange' a:help='Maximum attack range (in meters)'><ref name='nonNegativeDecimal'/></element>" +
2021-04-11 23:17:13 -07:00
"<optional>" +
2025-04-10 21:24:32 -07:00
"<element name='MinRange' a:help='Minimum attack range (in meters). Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
2021-04-11 23:17:13 -07:00
"</optional>" +
2013-08-03 12:20:20 -07:00
"<optional>" +
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
"<element name='Origin' a:help='The offset from which the attack occurs, relative to the entity position. Defaults to {0,0,0}.'>" +
"<interleave>" +
"<element name='X'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Y'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Z'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</interleave>" +
"</element>" +
2013-08-03 12:20:20 -07:00
"</optional>" +
2017-12-08 05:40:41 -08:00
"<optional>" +
"<element name='RangeOverlay'>" +
"<interleave>" +
"<element name='LineTexture'><text/></element>" +
"<element name='LineTextureMask'><text/></element>" +
"<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</optional>" +
2021-04-11 23:17:13 -07:00
"<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. Defaults to 0.'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</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
2010-05-15 14:07:52 -07:00
"<data type='positiveInteger'/>" +
"</element>" +
2021-04-11 23:17:13 -07:00
"<optional>" +
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
"<element name='EffectDelay' a:help='Delay of applying the effects, in milliseconds after the attack has landed. Defaults to 0.'><ref name='nonNegativeDecimal'/></element>" +
2021-04-11 23:17:13 -07:00
"</optional>" +
2012-05-19 16:07:41 -07:00
"<optional>" +
"<element name='Splash'>" +
"<interleave>" +
"<element name='Shape' a:help='Shape of the splash damage, can be circular or linear'><text/></element>" +
"<element name='Range' a:help='Size of the area affected by the splash'><ref name='nonNegativeDecimal'/></element>" +
"<element name='FriendlyFire' a:help='Whether the splash damage can hurt non enemy units'><data type='boolean'/></element>" +
2021-04-15 23:55:23 -07:00
AttackHelper . BuildAttackEffectsSchema ( ) +
2012-05-19 16:07:41 -07:00
"</interleave>" +
"</element>" +
"</optional>" +
2021-04-11 23:17:13 -07:00
"<optional>" +
"<element name='Projectile'>" +
"<interleave>" +
"<element name='Speed' a:help='Speed of projectiles (in meters per second).'>" +
2019-04-13 02:27:14 -07:00
"<ref name='positiveDecimal'/>" +
"</element>" +
2021-04-11 23:17:13 -07:00
"<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.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='FriendlyFire' a:help='Whether stray missiles can hurt non enemy units.'><data type='boolean'/></element>" +
2026-05-18 06:39:24 -07:00
"<optional>" +
"<element name='Count' a:help='Number of projectiles to fire at once.'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"</optional>" +
2021-04-11 23:17:13 -07:00
"<optional>" +
"<element name='LaunchPoint' a:help='Delta from the unit position where to launch the projectile.'>" +
"<attribute name='y'>" +
"<data type='decimal'/>" +
"</attribute>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='ActorName' a:help='actor of the projectile animation.'>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='ImpactActorName' a:help='actor of the projectile impact animation'>" +
"<text/>" +
"</element>" +
"<element name='ImpactAnimationLifetime' a:help='length of the projectile impact animation.'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</optional>" +
2014-10-14 13:44:17 -07:00
Attack . prototype . preferredClassesSchema +
Attack . prototype . restrictedClassesSchema +
2012-09-24 12:31:31 -07:00
"</interleave>" +
"</element>" +
2021-04-11 23:17:13 -07:00
"</oneOrMore>" ;
2010-04-09 12:02:39 -07:00
2011-03-05 14:30:32 -08:00
Attack . prototype . Init = function ( )
{
} ;
2017-05-07 12:32:59 -07:00
Attack . prototype . GetAttackTypes = function ( wantedTypes )
2012-05-01 15:20:08 -07:00
{
2025-05-11 01:39:08 -07:00
const types = g _AttackTypes . filter ( type => ! ! this . template [ type ] ) ;
2017-05-07 12:32:59 -07:00
if ( ! wantedTypes )
return types ;
2025-05-11 01:39:08 -07:00
const wantedTypesReal = wantedTypes . filter ( wtype => wtype . indexOf ( "!" ) != 0 ) ;
2017-05-07 12:32:59 -07:00
return types . filter ( type => wantedTypes . indexOf ( "!" + type ) == - 1 &&
( ! wantedTypesReal || ! wantedTypesReal . length || wantedTypesReal . indexOf ( type ) != - 1 ) ) ;
2012-05-01 15:20:08 -07:00
} ;
2026-05-22 06:07:37 -07:00
Attack . prototype . GetProjectileCount = function ( type )
{
if ( ! this . template [ type ] || ! this . template [ type ] . Projectile )
return 1 ;
let count = this . template [ type ] . Projectile . Count ? + this . template [ type ] . Projectile . Count : 1 ;
count = ApplyValueModificationsToEntity ( "Attack/" + type + "/Projectile/Count" , count , this . entity ) ;
return Math . max ( 1 , Math . floor ( count ) ) ;
} ;
2012-05-01 15:20:08 -07:00
Attack . prototype . GetPreferredClasses = function ( type )
{
2015-12-18 17:48:11 -08:00
if ( this . template [ type ] && this . template [ type ] . PreferredClasses &&
2015-12-18 18:03:30 -08:00
this . template [ type ] . PreferredClasses . _string )
2012-05-01 15:20:08 -07:00
return this . template [ type ] . PreferredClasses . _string . split ( /\s+/ ) ;
2016-07-01 08:05:09 -07:00
2012-05-01 15:20:08 -07:00
return [ ] ;
} ;
Attack . prototype . GetRestrictedClasses = function ( type )
{
2015-12-18 17:48:11 -08:00
if ( this . template [ type ] && this . template [ type ] . RestrictedClasses &&
2015-12-18 18:03:30 -08:00
this . template [ type ] . RestrictedClasses . _string )
2012-05-01 15:20:08 -07:00
return this . template [ type ] . RestrictedClasses . _string . split ( /\s+/ ) ;
2016-07-01 08:05:09 -07:00
2012-05-01 15:20:08 -07:00
return [ ] ;
} ;
2017-05-07 12:32:59 -07:00
Attack . prototype . CanAttack = function ( target , wantedTypes )
2012-05-01 15:20:08 -07:00
{
2021-05-19 05:36:22 -07:00
const cmpFormation = Engine . QueryInterface ( target , IID _Formation ) ;
2014-01-21 08:50:58 -08:00
if ( cmpFormation )
return true ;
2021-05-19 05:36:22 -07:00
const cmpThisPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
const cmpTargetPosition = Engine . QueryInterface ( target , IID _Position ) ;
2014-05-30 07:46:06 -07:00
if ( ! cmpThisPosition || ! cmpTargetPosition || ! cmpThisPosition . IsInWorld ( ) || ! cmpTargetPosition . IsInWorld ( ) )
return false ;
2021-05-19 05:36:22 -07:00
const cmpResistance = QueryMiragedInterface ( target , IID _Resistance ) ;
if ( ! cmpResistance )
return false ;
const cmpIdentity = QueryMiragedInterface ( target , IID _Identity ) ;
2017-05-07 12:32:59 -07:00
if ( ! cmpIdentity )
return false ;
2021-05-19 05:36:22 -07:00
const cmpHealth = QueryMiragedInterface ( target , IID _Health ) ;
const targetClasses = cmpIdentity . GetClassesList ( ) ;
2017-12-23 07:53:00 -08:00
if ( targetClasses . indexOf ( "Domestic" ) != - 1 && this . template . Slaughter && cmpHealth && cmpHealth . GetHitpoints ( ) &&
2021-04-14 22:45:13 -07:00
( ! wantedTypes || ! wantedTypes . filter ( wType => wType . indexOf ( "!" ) != 0 ) . length || wantedTypes . indexOf ( "Slaughter" ) != - 1 ) )
2017-05-07 12:32:59 -07:00
return true ;
2021-05-19 05:36:22 -07:00
const cmpEntityPlayer = QueryOwnerInterface ( this . entity ) ;
const cmpTargetPlayer = QueryOwnerInterface ( target ) ;
2017-05-07 12:32:59 -07:00
if ( ! cmpTargetPlayer || ! cmpEntityPlayer )
return false ;
2026-05-18 05:54:26 -07:00
// Must be visible or miraged / with retainInFog flag, not completely hidden
const cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
if ( cmpRangeManager )
{
const visibility = cmpRangeManager . GetLosVisibility ( target , cmpEntityPlayer . GetPlayerID ( ) ) ;
if ( visibility == "hidden" )
return false ;
}
2021-05-19 05:36:22 -07:00
const types = this . GetAttackTypes ( wantedTypes ) ;
const entityOwner = cmpEntityPlayer . GetPlayerID ( ) ;
const targetOwner = cmpTargetPlayer . GetPlayerID ( ) ;
const cmpCapturable = QueryMiragedInterface ( target , IID _Capturable ) ;
2023-06-18 23:33:33 -07:00
const cmpDiplomacy = QueryPlayerIDInterface ( entityOwner , IID _Diplomacy ) ;
2017-05-07 12:32:59 -07:00
2021-05-19 05:36:22 -07:00
for ( const type of types )
2012-05-01 15:20:08 -07:00
{
2023-06-18 23:33:33 -07:00
if ( type != "Capture" && ( ! cmpDiplomacy ? . IsEnemy ( targetOwner ) || ! cmpHealth || ! cmpHealth . GetHitpoints ( ) ) )
2017-05-07 12:32:59 -07:00
continue ;
if ( type == "Capture" && ( ! cmpCapturable || ! cmpCapturable . CanCapture ( entityOwner ) ) )
2015-11-13 09:47:31 -08:00
continue ;
2015-12-18 17:48:11 -08:00
2026-05-05 09:32:58 -07:00
// Check if the target is currently in range, or could ever be reached
if ( ! this . IsTargetInRange ( target , type ) && ! this . CanEverReachTarget ( target , type ) )
2014-05-30 07:46:06 -07:00
continue ;
2021-05-19 05:36:22 -07:00
const restrictedClasses = this . GetRestrictedClasses ( type ) ;
2015-11-13 09:47:31 -08:00
if ( ! restrictedClasses . length )
return true ;
2012-05-01 15:20:08 -07:00
2016-09-21 05:51:14 -07:00
if ( ! MatchesClassList ( targetClasses , restrictedClasses ) )
2016-07-11 10:25:19 -07:00
return true ;
2012-05-01 15:20:08 -07:00
}
return false ;
} ;
2026-05-05 09:32:58 -07:00
/ * *
* Check if the target could potentially ever be reached with the given attack type ,
* as an optimistic estimate . This assumes the attacker can move to the closest
* possible position to the target — ignoring obstructions and terrain features
* ( e . g . , hills ) that might help or hinder .
*
* This is a best - effort guess :
* - It may return true even when the target is actually unreachable ( e . g . , turreted
* units on walls with a height offset too large for the projectile to overcome ) .
* - It may return false even when the target is reachable ( e . g . , a nearby hill could
* provide enough elevation to hit a "too high" target , but we don ' t check for that ) .
*
* Currently these checks are mostly useful to determine if we can reach turreted units
* ( e . g . on a wall , outpost ... ) .
*
* @ param { number } targetId - The target entity ID .
* @ param { string } type - The attack type .
* @ return { boolean } - Whether the target is estimated to be reachable ( see caveats above ) .
* /
Attack . prototype . CanEverReachTarget = function ( targetId , type )
{
const cmpThisPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
const cmpTargetPosition = Engine . QueryInterface ( targetId , IID _Position ) ;
const thisHeightOffset = cmpThisPosition . GetHeightOffset ( ) ;
const targetHeightOffset = cmpTargetPosition . GetHeightOffset ( ) ;
const range = this . GetRange ( type ) ;
// Find the closest horizontal distance we could ever get to the target.
// We first determine the closest horizontal distance we could ever get to the target,
// accounting for turreted units inside buildings:
// - If the building blocks movement, we can only reach its exterior edge.
// - If the building is passable, we can walk right up to the turret point.
const cmpTurretable = Engine . QueryInterface ( targetId , IID _Turretable ) ;
const holderId = cmpTurretable ? . HolderID ( ) ;
let closestDistance = 0 ;
if ( holderId && holderId != INVALID _ENTITY )
{
const cmpTurretHolder = Engine . QueryInterface ( holderId , IID _TurretHolder ) ;
if ( cmpTurretHolder )
{
const turretPoint = cmpTurretHolder . GetOccupiedTurretPoint ( targetId ) ;
closestDistance = cmpTurretHolder . GetClosestApproachDistanceToTurretPoint ( turretPoint ) ;
}
}
if ( ! range . parabolic )
{
// For non-parabolic attacks (e.g., "Melee" attack type), we check if the height offset
// is within max range at the closest possible horizontal distance (simple 3D distance check).
const heightDiff = Math . abs ( targetHeightOffset - thisHeightOffset ) ;
return Math . sqrt ( closestDistance * closestDistance + heightDiff * heightDiff ) <= range . max ;
}
// For parabolic attacks (generally "Ranged" attack type), we use the parabolic formula
// to determine if the height offset is surmountable at the closest possible distance.
// Typical scenario: units on walls/towers may be unreachable if the attacker's
// projectiles can't arc high enough, even at point-blank range.
const cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
if ( ! cmpRangeManager )
return true ;
const yOrigin = this . GetAttackYOrigin ( type ) ;
const maxReachableHeightDiff = cmpRangeManager . GetMaxReachableParabolicHeight (
range . max , yOrigin , closestDistance ) ;
return targetHeightOffset - thisHeightOffset <= maxReachableHeightDiff ;
} ;
2012-05-01 15:20:08 -07:00
/ * *
2021-03-22 06:27:33 -07:00
* Returns undefined if we have no preference or the lowest index of a preferred class .
2012-05-01 15:20:08 -07:00
* /
Attack . prototype . GetPreference = function ( target )
{
2025-05-11 01:39:08 -07:00
const cmpIdentity = Engine . QueryInterface ( target , IID _Identity ) ;
2016-05-21 09:20:27 -07:00
if ( ! cmpIdentity )
2012-05-01 15:20:08 -07:00
return undefined ;
2025-05-11 01:39:08 -07:00
const targetClasses = cmpIdentity . GetClassesList ( ) ;
2013-11-26 10:33:42 -08:00
2021-03-22 06:27:33 -07:00
let minPref ;
2025-05-11 01:39:08 -07:00
for ( const type of this . GetAttackTypes ( ) )
2012-05-01 15:20:08 -07:00
{
2025-05-11 01:39:08 -07:00
const preferredClasses = this . GetPreferredClasses ( type ) ;
2021-01-21 06:34:08 -08:00
for ( let pref = 0 ; pref < preferredClasses . length ; ++ pref )
2012-05-01 15:20:08 -07:00
{
2021-01-21 06:34:08 -08:00
if ( MatchesClassList ( targetClasses , preferredClasses [ pref ] ) )
{
if ( pref === 0 )
return pref ;
2021-03-22 06:27:33 -07:00
if ( ( minPref === undefined || minPref > pref ) )
2021-01-21 06:34:08 -08:00
minPref = pref ;
}
2012-05-01 15:20:08 -07:00
}
}
return minPref ;
} ;
2015-10-08 13:48:05 -07:00
/ * *
* Get the full range of attack using all available attack types .
* /
Attack . prototype . GetFullAttackRange = function ( )
{
2026-05-01 05:42:14 -07:00
const ret = { "min" : Infinity , "max" : 0 , "parabolic" : false } ;
2025-05-11 01:39:08 -07:00
for ( const type of this . GetAttackTypes ( ) )
2015-10-08 13:48:05 -07:00
{
2025-05-11 01:39:08 -07:00
const range = this . GetRange ( type ) ;
2016-07-01 08:05:09 -07:00
ret . min = Math . min ( ret . min , range . min ) ;
ret . max = Math . max ( ret . max , range . max ) ;
2026-05-01 05:42:14 -07:00
if ( range . parabolic )
ret . parabolic = true ;
2015-10-08 13:48:05 -07:00
}
return ret ;
} ;
2019-08-22 11:00:33 -07:00
Attack . prototype . GetAttackEffectsData = function ( type , splash )
{
2019-08-25 11:03:32 -07:00
let template = this . template [ type ] ;
2022-10-09 22:36:27 -07:00
if ( ! template )
return undefined ;
2019-08-22 11:00:33 -07:00
if ( splash )
2019-08-25 11:03:32 -07:00
template = template . Splash ;
2021-04-15 23:55:23 -07:00
return AttackHelper . GetAttackEffectsData ( "Attack/" + type + ( splash ? "/Splash" : "" ) , template , this . entity ) ;
2019-08-22 11:00:33 -07:00
} ;
2019-08-24 01:52:15 -07:00
/ * *
* Find the best attack against a target .
* @ param { number } target - The entity - ID of the target .
* @ param { boolean } allowCapture - Whether capturing is allowed .
* @ return { string } - The preferred attack type .
* /
2015-04-21 06:21:19 -07:00
Attack . prototype . GetBestAttackAgainst = function ( target , allowCapture )
2012-05-01 15:20:08 -07:00
{
2025-05-11 01:39:08 -07:00
const types = this . GetAttackTypes ( ) ;
2023-02-09 23:14:39 -08:00
if ( Engine . QueryInterface ( target , IID _Formation ) )
2015-10-08 13:48:05 -07:00
// TODO: Formation against formation needs review
2017-05-07 12:32:59 -07:00
return g _AttackTypes . find ( attack => types . indexOf ( attack ) != - 1 ) ;
2014-01-21 08:50:58 -08:00
2023-02-09 23:14:39 -08:00
const cmpIdentity = Engine . QueryInterface ( target , IID _Identity ) ;
2016-05-21 09:20:27 -07:00
if ( ! cmpIdentity )
2010-05-15 14:07:52 -07:00
return undefined ;
2012-05-01 15:20:08 -07:00
2012-09-24 12:31:31 -07:00
// Always slaughter domestic animals instead of using a normal attack
2019-08-24 01:52:15 -07:00
if ( this . template . Slaughter && cmpIdentity . HasClass ( "Domestic" ) )
2012-09-24 12:31:31 -07:00
return "Slaughter" ;
2012-05-01 15:20:08 -07:00
2023-02-09 23:14:39 -08:00
const targetClasses = cmpIdentity . GetClassesList ( ) ;
2025-12-30 00:57:37 -08:00
const getPreferrence = attackType =>
{
2023-02-09 23:14:39 -08:00
let pref = 0 ;
if ( MatchesClassList ( targetClasses , this . GetPreferredClasses ( attackType ) ) )
pref += 2 ;
if ( allowCapture ? attackType === "Capture" : attackType !== "Capture" )
pref ++ ;
return pref ;
} ;
2015-04-20 00:45:45 -07:00
2025-12-30 00:57:37 -08:00
return types . filter ( type => this . CanAttack ( target , [ type ] ) ) . sort ( ( a , b ) =>
{
2023-02-09 23:14:39 -08:00
const prefA = getPreferrence ( a ) ;
const prefB = getPreferrence ( b ) ;
return ( types . indexOf ( a ) + ( prefA > 0 ? prefA + types . length : 0 ) ) -
2025-05-03 05:12:49 -07:00
( types . indexOf ( b ) + ( prefB > 0 ? prefB + types . length : 0 ) ) ;
2023-02-09 23:14:39 -08:00
} ) . pop ( ) ;
2012-05-01 15:20:08 -07:00
} ;
Attack . prototype . CompareEntitiesByPreference = function ( a , b )
{
2025-05-11 01:39:08 -07:00
const aPreference = this . GetPreference ( a ) ;
const bPreference = this . GetPreference ( b ) ;
2012-05-01 15:20:08 -07:00
if ( aPreference === null && bPreference === null ) return 0 ;
if ( aPreference === null ) return 1 ;
if ( bPreference === null ) return - 1 ;
return aPreference - bPreference ;
2010-02-05 14:00:39 -08:00
} ;
2020-11-18 13:34:33 -08:00
Attack . prototype . GetAttackName = function ( type )
{
return {
"name" : this . template [ type ] . AttackName . _string || this . template [ type ] . AttackName ,
"context" : this . template [ type ] . AttackName [ "@context" ]
} ;
} ;
2020-04-21 14:44:05 -07:00
Attack . prototype . GetRepeatTime = function ( type )
2010-02-05 14:00:39 -08:00
{
2020-04-21 14:44:05 -07:00
let repeatTime = 1000 ;
if ( this . template [ type ] && this . template [ type ] . RepeatTime )
repeatTime = + this . template [ type ] . RepeatTime ;
2015-12-18 18:03:30 -08:00
2020-04-21 14:44:05 -07:00
return ApplyValueModificationsToEntity ( "Attack/" + type + "/RepeatTime" , repeatTime , this . entity ) ;
} ;
2012-05-04 15:51:14 -07:00
2020-04-21 14:44:05 -07:00
Attack . prototype . GetTimers = function ( type )
{
return {
"prepare" : ApplyValueModificationsToEntity ( "Attack/" + type + "/PrepareTime" , + ( this . template [ type ] . PrepareTime || 0 ) , this . entity ) ,
"repeat" : this . GetRepeatTime ( type )
} ;
2010-02-05 14:00:39 -08:00
} ;
2020-01-12 05:37:14 -08:00
Attack . prototype . GetSplashData = function ( type )
2016-07-02 21:08:52 -07:00
{
if ( ! this . template [ type ] . Splash )
2020-04-01 11:17:48 -07:00
return undefined ;
2016-07-02 21:08:52 -07:00
2019-08-22 11:00:33 -07:00
return {
"attackData" : this . GetAttackEffectsData ( type , true ) ,
2020-03-07 02:39:05 -08:00
"friendlyFire" : this . template [ type ] . Splash . FriendlyFire == "true" ,
2020-01-12 05:37:14 -08:00
"radius" : ApplyValueModificationsToEntity ( "Attack/" + type + "/Splash/Range" , + this . template [ type ] . Splash . Range , this . entity ) ,
2019-08-22 11:00:33 -07:00
"shape" : this . template [ type ] . Splash . Shape ,
} ;
2016-07-02 21:08:52 -07:00
} ;
2010-05-15 14:07:52 -07:00
Attack . prototype . GetRange = function ( type )
2010-02-05 14:00:39 -08:00
{
2026-01-15 20:54:46 -08:00
if ( ! type || ! this . template [ type ] )
2020-01-30 13:05:59 -08:00
return this . GetFullAttackRange ( ) ;
2015-12-18 17:48:11 -08:00
let max = + this . template [ type ] . MaxRange ;
2013-10-15 03:05:08 -07:00
max = ApplyValueModificationsToEntity ( "Attack/" + type + "/MaxRange" , max , this . entity ) ;
2015-12-18 18:03:30 -08:00
2015-12-18 17:48:11 -08:00
let min = + ( this . template [ type ] . MinRange || 0 ) ;
2013-10-15 03:05:08 -07:00
min = ApplyValueModificationsToEntity ( "Attack/" + type + "/MinRange" , min , this . entity ) ;
2013-08-03 12:20:20 -07:00
2026-05-01 05:42:14 -07:00
return {
"max" : max ,
"min" : min ,
"parabolic" : type === "Ranged"
} ;
} ;
/ * *
* Get the effective range for attacking a specific target , accounting
* for elevation and projectile physics where applicable .
* @ param { number } target - The target entity ID .
* @ param { string } type - The attack type .
* @ return { { min : number , max : number } } - The min and max effective range .
* /
Attack . prototype . GetEffectiveAttackRange = function ( target , type )
{
const range = this . GetRange ( type ) ;
// Only Parabolic attacks get parabolic elevation adjustment
if ( ! range . parabolic )
return range ;
const cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
if ( ! cmpRangeManager )
return range ;
const effectiveMax = cmpRangeManager . GetEffectiveParabolicRange (
this . entity , target , range . max , this . GetAttackYOrigin ( type ) ) ;
if ( effectiveMax < 0 )
return { "min" : Infinity , "max" : 0 } ; // Out of range
return { "min" : range . min , "max" : effectiveMax } ;
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
} ;
2015-12-18 18:03:30 -08:00
Rename "ElevationBonus" and "Delay" to "Origin" and "EffectDelay", respectively.
`ElevationBonus` is vague, as discussions proved. Therefore it is
renamed to `Origin`, which, describes better what the value stands for.
`Delay` is also quite vague, so renamed to `EffectDelay`.
Differential revision: https://code.wildfiregames.com/D2016
Comments by: @bb, @nani, @Nescio, @Silier, @Stan, @wraitii
This was SVN commit r26074.
2021-12-14 23:42:06 -08:00
Attack . prototype . GetAttackYOrigin = function ( type )
{
if ( ! this . template [ type ] . Origin )
return 0 ;
return ApplyValueModificationsToEntity ( "Attack/" + type + "/Origin/Y" , + this . template [ type ] . Origin . Y , this . entity ) ;
2011-12-23 19:49:30 -08:00
} ;
2025-12-30 00:57:37 -08:00
Attack . prototype . RepeatRangeCheck = function ( type )
{
2024-11-03 09:24:04 -08:00
if ( ! this . IsTargetInRange ( this . target , type ) )
this . StopAttacking ( "OutOfRange" ) ;
} ;
2021-04-11 23:51:39 -07:00
/ * *
* @ param { number } target - The target to attack .
* @ param { string } type - The type of attack to use .
* @ param { number } callerIID - The IID to notify on specific events .
*
* @ return { boolean } - Whether we started attacking .
* /
2025-08-17 05:55:25 -07:00
Attack . prototype . StartAttacking = function ( target , type , callerIID , force )
2021-04-11 23:51:39 -07:00
{
if ( this . target )
this . StopAttacking ( ) ;
2021-04-14 22:45:13 -07:00
if ( ! this . CanAttack ( target , [ type ] ) )
2021-04-11 23:51:39 -07:00
return false ;
2021-09-10 21:47:25 -07:00
const cmpResistance = QueryMiragedInterface ( target , IID _Resistance ) ;
2021-05-03 22:18:11 -07:00
if ( ! cmpResistance || ! cmpResistance . AddAttacker ( this . entity ) )
return false ;
2025-05-11 01:39:08 -07:00
const timings = this . GetTimers ( type ) ;
const cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
2021-04-11 23:51:39 -07:00
// If the repeat time since the last attack hasn't elapsed,
// delay the action to avoid attacking too fast.
let prepare = timings . prepare ;
if ( this . lastAttacked )
{
2025-05-11 01:39:08 -07:00
const repeatLeft = this . lastAttacked + timings . repeat - cmpTimer . GetTime ( ) ;
2021-04-11 23:51:39 -07:00
prepare = Math . max ( prepare , repeatLeft ) ;
}
2025-05-11 01:39:08 -07:00
const cmpVisual = Engine . QueryInterface ( this . entity , IID _Visual ) ;
2021-04-11 23:51:39 -07:00
if ( cmpVisual )
{
cmpVisual . SelectAnimation ( "attack_" + type . toLowerCase ( ) , false , 1.0 ) ;
cmpVisual . SetAnimationSyncRepeat ( timings . repeat ) ;
cmpVisual . SetAnimationSyncOffset ( prepare ) ;
}
2024-11-03 09:24:04 -08:00
// Find the number of range checks needed during repeat time.
const numCheck = Math . ceil ( timings . repeat / 1000 ) ;
// Calculate the timing offset to be half the check repeat time.
const repeatPerCheck = timings . repeat / numCheck ;
const offset = repeatPerCheck / 2 ;
// Set the startpoint to be the prepare time plus any remaining time not evenly divisible by the offset.
let checkStart = timings . prepare + ( ( prepare - timings . prepare ) % offset ) ;
// Add an offset to the start time if we are not already offset.
if ( ( prepare - checkStart ) % repeatPerCheck === 0 )
checkStart += offset ;
2021-04-11 23:51:39 -07:00
// If using a non-default prepare time, re-sync the animation when the timer runs.
this . resyncAnimation = prepare != timings . prepare ;
this . target = target ;
this . callerIID = callerIID ;
2025-08-17 05:55:25 -07:00
this . force = force ;
2021-04-11 23:51:39 -07:00
this . timer = cmpTimer . SetInterval ( this . entity , IID _Attack , "Attack" , prepare , timings . repeat , type ) ;
2024-11-03 09:24:04 -08:00
this . checkTimer = cmpTimer . SetInterval ( this . entity , IID _Attack , "RepeatRangeCheck" , checkStart , repeatPerCheck , type ) ;
2021-04-11 23:51:39 -07:00
return true ;
} ;
/ * *
* @ param { string } reason - The reason why we stopped attacking .
* /
Attack . prototype . StopAttacking = function ( reason )
{
if ( ! this . target )
return ;
2025-05-11 01:39:08 -07:00
const cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
2021-04-11 23:51:39 -07:00
cmpTimer . CancelTimer ( this . timer ) ;
2024-11-03 09:24:04 -08:00
cmpTimer . CancelTimer ( this . checkTimer ) ;
2021-04-11 23:51:39 -07:00
delete this . timer ;
2024-11-03 09:24:04 -08:00
delete this . checkTimer ;
2021-04-11 23:51:39 -07:00
2021-09-10 21:47:25 -07:00
const cmpResistance = QueryMiragedInterface ( this . target , IID _Resistance ) ;
2021-05-03 22:18:11 -07:00
if ( cmpResistance )
cmpResistance . RemoveAttacker ( this . entity ) ;
2021-04-11 23:51:39 -07:00
delete this . target ;
2025-05-11 01:39:08 -07:00
const cmpVisual = Engine . QueryInterface ( this . entity , IID _Visual ) ;
2021-04-11 23:51:39 -07:00
if ( cmpVisual )
cmpVisual . SelectAnimation ( "idle" , false , 1.0 ) ;
// The callerIID component may start again,
// replacing the callerIID, hence save that.
2025-05-11 01:39:08 -07:00
const callerIID = this . callerIID ;
2021-04-11 23:51:39 -07:00
delete this . callerIID ;
if ( reason && callerIID )
{
2025-05-11 01:39:08 -07:00
const component = Engine . QueryInterface ( this . entity , callerIID ) ;
2021-04-11 23:51:39 -07:00
if ( component )
component . ProcessMessage ( reason , null ) ;
}
} ;
/ * *
* Attack our target entity .
* @ param { string } data - The attack type to use .
* @ param { number } lateness - The offset of the actual call and when it was expected .
* /
Attack . prototype . Attack = function ( type , lateness )
{
2021-04-14 22:45:13 -07:00
if ( ! this . CanAttack ( this . target , [ type ] ) )
2021-04-11 23:51:39 -07:00
{
this . StopAttacking ( "TargetInvalidated" ) ;
return ;
}
// ToDo: Enable entities to keep facing a target.
Engine . QueryInterface ( this . entity , IID _UnitAI ) ? . FaceTowardsTarget ( this . target ) ;
2025-05-11 01:39:08 -07:00
const cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
2021-04-11 23:51:39 -07:00
this . lastAttacked = cmpTimer . GetTime ( ) - lateness ;
// BuildingAI has its own attack routine.
if ( ! Engine . QueryInterface ( this . entity , IID _BuildingAI ) )
this . PerformAttack ( type , this . target ) ;
if ( ! this . target )
return ;
// We check the range after the attack to facilitate chasing.
if ( ! this . IsTargetInRange ( this . target , type ) )
{
this . StopAttacking ( "OutOfRange" ) ;
return ;
}
2025-08-17 05:55:25 -07:00
// If a low preference unit is attacked without player direction, check for higher preference.
if ( this . GetPreference ( this . target ) === undefined && ! this . force )
{
this . StopAttacking ( "TargetInvalidated" ) ;
return ;
}
2021-04-11 23:51:39 -07:00
if ( this . resyncAnimation )
{
2025-05-11 01:39:08 -07:00
const cmpVisual = Engine . QueryInterface ( this . entity , IID _Visual ) ;
2021-04-11 23:51:39 -07:00
if ( cmpVisual )
{
2025-05-11 01:39:08 -07:00
const repeat = this . GetTimers ( type ) . repeat ;
2021-04-11 23:51:39 -07:00
cmpVisual . SetAnimationSyncRepeat ( repeat ) ;
cmpVisual . SetAnimationSyncOffset ( repeat ) ;
}
delete this . resyncAnimation ;
}
} ;
2010-02-05 14:00:39 -08:00
/ * *
2010-02-12 14:46:53 -08:00
* Attack the target entity . This should only be called after a successful range check ,
2010-02-05 14:00:39 -08:00
* and should only be called after GetTimers ( ) . repeat msec has passed since the last
* call to PerformAttack .
* /
2010-05-15 14:07:52 -07:00
Attack . prototype . PerformAttack = function ( type , target )
2010-03-07 13:38:39 -08:00
{
2025-05-11 01:39:08 -07:00
const cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
2021-04-11 23:17:13 -07:00
if ( ! cmpPosition || ! cmpPosition . IsInWorld ( ) )
return ;
2025-05-11 01:39:08 -07:00
const selfPosition = cmpPosition . GetPosition ( ) ;
2021-04-11 23:17:13 -07:00
2025-05-11 01:39:08 -07:00
const cmpTargetPosition = Engine . QueryInterface ( target , IID _Position ) ;
2021-04-11 23:17:13 -07:00
if ( ! cmpTargetPosition || ! cmpTargetPosition . IsInWorld ( ) )
return ;
2025-05-11 01:39:08 -07:00
const targetPosition = cmpTargetPosition . GetPosition ( ) ;
2021-04-11 23:17:13 -07:00
2025-05-11 01:39:08 -07:00
const cmpOwnership = Engine . QueryInterface ( this . entity , IID _Ownership ) ;
2021-04-11 23:17:13 -07:00
if ( ! cmpOwnership )
return ;
2025-05-11 01:39:08 -07:00
const attackerOwner = cmpOwnership . GetOwner ( ) ;
2016-11-23 05:02:58 -08:00
2025-05-11 01:39:08 -07:00
const data = {
2021-03-04 22:32:44 -08:00
"type" : type ,
"attackData" : this . GetAttackEffectsData ( type ) ,
2021-04-11 23:17:13 -07:00
"splash" : this . GetSplashData ( type ) ,
2021-03-04 22:32:44 -08:00
"attacker" : this . entity ,
"attackerOwner" : attackerOwner ,
2021-04-11 23:17:13 -07:00
"target" : target ,
2021-03-04 22:32:44 -08:00
} ;
2026-05-18 06:39:24 -07:00
const delay = + ( this . template [ type ] . EffectDelay || 0 ) ;
2021-04-11 23:17:13 -07:00
if ( this . template [ type ] . Projectile )
2010-03-07 13:38:39 -08:00
{
2025-05-11 01:39:08 -07:00
const cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
const turnLength = cmpTimer . GetLatestTurnLength ( ) / 1000 ;
2012-05-19 16:07:41 -07:00
// In the future this could be extended:
2010-03-07 13:38:39 -08:00
// * Obstacles like trees could reduce the probability of the target being hit
// * Obstacles like walls should block projectiles entirely
2025-05-11 01:39:08 -07:00
const horizSpeed = + this . template [ type ] . Projectile . Speed ;
const gravity = + this . template [ type ] . Projectile . Gravity ;
2019-08-22 11:00:33 -07:00
// horizSpeed /= 2; gravity /= 2; // slow it down for testing
2013-11-26 10:33:42 -08:00
2021-01-19 07:59:03 -08:00
// We will try to estimate the position of the target, where we can hit it.
// We first estimate the time-till-hit by extrapolating linearly the movement
// of the last turn. We compute the time till an arrow will intersect the target.
2025-05-11 01:39:08 -07:00
const targetVelocity = Vector3D . sub ( targetPosition , cmpTargetPosition . GetPreviousPosition ( ) ) . div ( turnLength ) ;
2013-11-26 10:33:42 -08:00
2026-05-18 06:39:24 -07:00
const timeToTarget = PositionHelper . PredictTimeToTarget ( selfPosition , horizSpeed , targetPosition , targetVelocity ) ;
2021-01-19 07:59:03 -08:00
// 'Cheat' and use UnitMotion to predict the position in the near-future.
// This avoids 'dancing' issues with units zigzagging over very short distances.
// However, this could fail if the player gives several short move orders, so
// occasionally fall back to basic interpolation.
let predictedPosition = targetPosition ;
if ( timeToTarget !== false )
{
// Don't predict too far in the future, but avoid threshold effects.
// After 1 second, always use the 'dumb' interpolated past-motion prediction.
2025-05-11 01:39:08 -07:00
const useUnitMotion = randBool ( Math . max ( 0 , 0.75 - timeToTarget / 1.333 ) ) ;
2021-01-19 07:59:03 -08:00
if ( useUnitMotion )
{
2025-05-11 01:39:08 -07:00
const cmpTargetUnitMotion = Engine . QueryInterface ( target , IID _UnitMotion ) ;
const cmpTargetUnitAI = Engine . QueryInterface ( target , IID _UnitAI ) ;
2021-01-19 07:59:03 -08:00
if ( cmpTargetUnitMotion && ( ! cmpTargetUnitAI || ! cmpTargetUnitAI . IsFormationMember ( ) ) )
{
2025-05-11 01:39:08 -07:00
const pos2D = cmpTargetUnitMotion . EstimateFuturePosition ( timeToTarget ) ;
2021-01-19 07:59:03 -08:00
predictedPosition . x = pos2D . x ;
predictedPosition . z = pos2D . y ;
}
else
predictedPosition = Vector3D . mult ( targetVelocity , timeToTarget ) . add ( targetPosition ) ;
}
else
predictedPosition = Vector3D . mult ( targetVelocity , timeToTarget ) . add ( targetPosition ) ;
}
2013-11-26 10:33:42 -08:00
2025-05-11 01:39:08 -07:00
const predictedHeight = cmpTargetPosition . GetHeightAt ( predictedPosition . x , predictedPosition . z ) ;
2021-01-22 10:16:13 -08:00
2017-04-01 00:16:46 -07:00
// Add inaccuracy based on spread.
2024-12-28 13:37:58 -08:00
const distanceModifiedSpread = ApplyValueModificationsToEntity ( "Attack/" + type + "/Projectile/Spread" , + this . template [ type ] . Projectile . Spread , this . entity ) *
2017-08-20 08:46:54 -07:00
predictedPosition . horizDistanceTo ( selfPosition ) / 100 ;
2013-12-29 00:09:44 -08:00
2021-04-11 23:17:13 -07:00
let actorName = this . template [ type ] . Projectile . ActorName || "" ;
2025-05-11 01:39:08 -07:00
const impactActorName = this . template [ type ] . Projectile . ImpactActorName || "" ;
const impactAnimationLifetime = this . template [ type ] . Projectile . ImpactAnimationLifetime || 0 ;
2019-04-13 02:27:14 -07:00
// TODO: Use unit rotation to implement x/z offsets.
2025-05-11 01:39:08 -07:00
const deltaLaunchPoint = new Vector3D ( 0 , + this . template [ type ] . Projectile . LaunchPoint [ "@y" ] , 0 ) ;
2019-04-13 02:27:14 -07:00
let launchPoint = Vector3D . add ( selfPosition , deltaLaunchPoint ) ;
2019-08-22 11:00:33 -07:00
2025-05-11 01:39:08 -07:00
const cmpVisual = Engine . QueryInterface ( this . entity , IID _Visual ) ;
2017-12-23 01:27:19 -08:00
if ( cmpVisual )
{
// if the projectile definition is missing from the template
// then fallback to the projectile name and launchpoint in the visual actor
if ( ! actorName )
actorName = cmpVisual . GetProjectileActor ( ) ;
2025-05-11 01:39:08 -07:00
const visualActorLaunchPoint = cmpVisual . GetProjectileLaunchPoint ( ) ;
2017-12-23 01:27:19 -08:00
if ( visualActorLaunchPoint . length ( ) > 0 )
launchPoint = visualActorLaunchPoint ;
}
2025-05-11 01:39:08 -07:00
const cmpProjectileManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _ProjectileManager ) ;
const cmpSound = Engine . QueryInterface ( this . entity , IID _Sound ) ;
2026-05-18 06:39:24 -07:00
const attackImpactSound = cmpSound ? cmpSound . GetSoundGroup ( "attack_impact_" + type . toLowerCase ( ) ) : "" ;
const friendlyFire = this . template [ type ] . Projectile . FriendlyFire == "true" ;
const count = Math . max ( 1 , Math . floor (
ApplyValueModificationsToEntity (
"Attack/" + type + "/Projectile/Count" ,
this . template [ type ] . Projectile . Count ? + this . template [ type ] . Projectile . Count : 1 ,
this . entity
)
) ) ;
for ( let i = 0 ; i < count ; ++ i )
{
const randNorm = randomNormal2D ( ) ;
const offsetX = randNorm [ 0 ] * distanceModifiedSpread ;
const offsetZ = randNorm [ 1 ] * distanceModifiedSpread ;
const projectileData = {
"type" : type ,
"attackData" : this . GetAttackEffectsData ( type ) ,
"splash" : this . GetSplashData ( type ) ,
"attacker" : this . entity ,
"attackerOwner" : attackerOwner ,
"target" : target ,
"position" : new Vector3D ( predictedPosition . x + offsetX , predictedHeight , predictedPosition . z + offsetZ ) ,
"friendlyFire" : friendlyFire ,
"attackImpactSound" : attackImpactSound
} ;
const realHorizDistance = projectileData . position . horizDistanceTo ( selfPosition ) ;
const projectileTimeToTarget = realHorizDistance / horizSpeed ;
const projectileDelay = delay + projectileTimeToTarget * 1000 ;
projectileData . direction = Vector3D . sub ( projectileData . position , selfPosition ) . div ( realHorizDistance ) ;
projectileData . projectileId = cmpProjectileManager . LaunchProjectileAtPoint ( launchPoint , projectileData . position , horizSpeed , gravity , actorName , impactActorName , impactAnimationLifetime ) ;
if ( projectileDelay )
cmpTimer . SetTimeout ( SYSTEM _ENTITY , IID _DelayedDamage , "Hit" , projectileDelay , projectileData ) ;
else
Engine . QueryInterface ( SYSTEM _ENTITY , IID _DelayedDamage ) . Hit ( projectileData , 0 ) ;
}
return ;
2021-04-11 23:17:13 -07:00
}
2026-05-18 06:39:24 -07:00
data . position = targetPosition ;
data . direction = Vector3D . sub ( targetPosition , selfPosition ) ;
2021-04-11 23:17:13 -07:00
if ( delay )
{
2025-05-11 01:39:08 -07:00
const cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
2021-04-11 23:17:13 -07:00
cmpTimer . SetTimeout ( SYSTEM _ENTITY , IID _DelayedDamage , "Hit" , delay , data ) ;
2015-04-20 00:45:45 -07:00
}
2010-03-07 13:38:39 -08:00
else
2021-04-11 23:17:13 -07:00
Engine . QueryInterface ( SYSTEM _ENTITY , IID _DelayedDamage ) . Hit ( data , 0 ) ;
2012-05-19 16:07:41 -07:00
} ;
2021-04-11 23:51:39 -07:00
/ * *
* @ param { number } - The entity ID of the target to check .
* @ return { boolean } - Whether this entity is in range of its target .
* /
Attack . prototype . IsTargetInRange = function ( target , type )
{
2026-05-01 05:42:14 -07:00
const range = this . GetEffectiveAttackRange ( target , type ) ;
return Engine . QueryInterface ( SYSTEM _ENTITY , IID _ObstructionManager ) . IsInTargetRange (
this . entity , target , range . min , range . max , false ) ;
2021-04-11 23:51:39 -07:00
} ;
2015-09-27 05:23:40 -07:00
Attack . prototype . OnValueModification = function ( msg )
{
if ( msg . component != "Attack" )
return ;
2025-05-11 01:39:08 -07:00
const cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
2015-09-27 05:23:40 -07:00
if ( ! cmpUnitAI )
return ;
2016-07-01 08:18:09 -07:00
if ( this . GetAttackTypes ( ) . some ( type =>
2025-05-30 12:33:49 -07:00
msg . valueNames . indexOf ( "Attack/" + type + "/MaxRange" ) != - 1 ) )
2016-07-01 08:05:09 -07:00
cmpUnitAI . UpdateRangeQueries ( ) ;
2015-09-27 05:23:40 -07:00
} ;
2021-04-11 23:17:13 -07:00
Attack . prototype . GetRangeOverlays = function ( type = "Ranged" )
2017-12-08 05:40:41 -08:00
{
2021-04-11 23:17:13 -07:00
if ( ! this . template [ type ] || ! this . template [ type ] . RangeOverlay )
2017-12-08 05:40:41 -08:00
return [ ] ;
2025-05-11 01:39:08 -07:00
const range = this . GetRange ( type ) ;
const rangeOverlays = [ ] ;
for ( const i in range )
2017-12-08 05:40:41 -08:00
if ( ( i == "min" || i == "max" ) && range [ i ] )
rangeOverlays . push ( {
"radius" : range [ i ] ,
2021-04-11 23:17:13 -07:00
"texture" : this . template [ type ] . RangeOverlay . LineTexture ,
"textureMask" : this . template [ type ] . RangeOverlay . LineTextureMask ,
"thickness" : + this . template [ type ] . RangeOverlay . LineThickness ,
2017-12-08 05:40:41 -08:00
} ) ;
return rangeOverlays ;
} ;
2010-02-05 14:00:39 -08:00
Engine . RegisterComponentType ( IID _Attack , "Attack" , Attack ) ;