2020-07-20 03:51:14 -07:00
/ * *
* This class holds the functions regarding entities being visible on
* another entity , but tied to their parents location .
* /
class TurretHolder
{
Init ( )
{
this . turretPoints = [ ] ;
2025-05-12 04:10:26 -07:00
const points = this . template . TurretPoints ;
for ( const point in points )
2020-07-20 03:51:14 -07:00
this . turretPoints . push ( {
2020-11-11 11:40:44 -08:00
"name" : point ,
2020-07-20 03:51:14 -07:00
"offset" : {
"x" : + points [ point ] . X ,
"y" : + points [ point ] . Y ,
"z" : + points [ point ] . Z
} ,
2021-04-02 00:15:46 -07:00
"allowedClasses" : points [ point ] . AllowedClasses ? . _string ,
2020-07-20 03:51:14 -07:00
"angle" : points [ point ] . Angle ? + points [ point ] . Angle * Math . PI / 180 : null ,
2021-04-04 22:22:25 -07:00
"entity" : null ,
"template" : points [ point ] . Template ,
"ejectable" : "Ejectable" in points [ point ] ? points [ point ] . Ejectable == "true" : true
2020-07-20 03:51:14 -07:00
} ) ;
}
2021-04-04 22:22:25 -07:00
/ * *
* Add a subunit as specified in the template .
* This function creates an entity and places it on the turret point .
*
* @ param { Object } turretPoint - A turret point to ( re ) create the predefined subunit for .
*
* @ return { boolean } - Whether the turret creation has succeeded .
* /
CreateSubunit ( turretPointName )
{
2023-05-10 08:13:52 -07:00
const turretPoint = this . TurretPointByName ( turretPointName ) ;
2021-04-04 22:22:25 -07:00
if ( ! turretPoint || turretPoint . entity ||
this . initTurrets ? . has ( turretPointName ) ||
this . reservedTurrets ? . has ( turretPointName ) )
return false ;
2023-05-10 08:13:52 -07:00
const cmpOwnership = Engine . QueryInterface ( this . entity , IID _Ownership ) ;
2021-04-04 22:22:25 -07:00
2023-05-10 08:13:52 -07:00
const upgradedTemplate = GetUpgradedTemplate ( cmpOwnership . GetOwner ( ) , turretPoint . template ) ;
const ent = Engine . AddEntity ( upgradedTemplate ) ;
2021-04-04 22:22:25 -07:00
2023-05-10 08:13:52 -07:00
const cmpEntOwnership = Engine . QueryInterface ( ent , IID _Ownership ) ;
cmpEntOwnership ? . SetOwner ( cmpOwnership . GetOwner ( ) ) ;
const cmpTurretable = Engine . QueryInterface ( ent , IID _Turretable ) ;
2021-04-04 22:22:25 -07:00
return cmpTurretable ? . OccupyTurret ( this . entity , turretPoint . name , turretPoint . ejectable ) || Engine . DestroyEntity ( ent ) ;
}
/ * *
* @ param { string } name - The name of a turret point to reserve , e . g . for promotion .
* /
SetReservedTurretPoint ( name )
{
if ( ! this . reservedTurrets )
this . reservedTurrets = new Set ( ) ;
this . reservedTurrets . add ( name ) ;
}
2020-07-20 03:51:14 -07:00
/ * *
* @ return { Object [ ] } - An array of the turret points this entity has .
* /
GetTurretPoints ( )
{
return this . turretPoints ;
}
/ * *
* @ param { number } entity - The entity to check for .
* @ param { Object } turretPoint - The turret point to use .
2026-01-08 04:08:35 -08:00
* @ param { boolean } [ forReplacement = false ] - Whether this check is for replacement
* ( if true , occupied turret points are allowed ) .
2020-07-20 03:51:14 -07:00
*
* @ return { boolean } - Whether the entity is allowed to occupy the specified turret point .
* /
2026-01-08 04:08:35 -08:00
AllowedToOccupyTurretPoint ( entity , turretPoint , forReplacement = false )
2020-07-20 03:51:14 -07:00
{
2026-01-08 04:08:35 -08:00
if ( ! turretPoint || turretPoint . entity && ! forReplacement )
2020-07-20 03:51:14 -07:00
return false ;
if ( ! IsOwnedByMutualAllyOfEntity ( entity , this . entity ) )
return false ;
if ( ! turretPoint . allowedClasses )
return true ;
2025-05-12 04:10:26 -07:00
const cmpIdentity = Engine . QueryInterface ( entity , IID _Identity ) ;
2021-04-02 00:15:46 -07:00
return cmpIdentity && MatchesClassList ( cmpIdentity . GetClassesList ( ) , turretPoint . allowedClasses ) ;
2020-07-20 03:51:14 -07:00
}
2021-03-26 03:18:30 -07:00
/ * *
* @ param { number } entity - The entity to check for .
* @ return { boolean } - Whether the entity is allowed to occupy any turret point .
* /
CanOccupy ( entity )
{
2021-04-04 22:22:25 -07:00
return ! ! this . turretPoints . find ( turretPoint => this . AllowedToOccupyTurretPoint ( entity , turretPoint ) ) ;
2021-03-26 03:18:30 -07:00
}
2020-07-20 03:51:14 -07:00
/ * *
* Occupy a turret point with the given entity .
* @ param { number } entity - The entity to use .
2020-07-20 23:09:19 -07:00
* @ param { Object } requestedTurretPoint - Optionally the specific turret point to occupy .
2020-07-20 03:51:14 -07:00
*
* @ return { boolean } - Whether the occupation was successful .
* /
2021-04-04 22:22:25 -07:00
OccupyTurretPoint ( entity , requestedTurretPoint )
2020-07-20 03:51:14 -07:00
{
2025-05-12 04:10:26 -07:00
const cmpPositionOccupant = Engine . QueryInterface ( entity , IID _Position ) ;
2020-07-20 03:51:14 -07:00
if ( ! cmpPositionOccupant )
return false ;
2025-05-12 04:10:26 -07:00
const cmpPositionSelf = Engine . QueryInterface ( this . entity , IID _Position ) ;
2020-07-20 03:51:14 -07:00
if ( ! cmpPositionSelf )
return false ;
2021-04-04 22:22:25 -07:00
if ( this . OccupiesTurretPoint ( entity ) )
2020-07-20 03:51:14 -07:00
return false ;
let turretPoint ;
if ( requestedTurretPoint )
{
2021-04-04 22:22:25 -07:00
if ( this . AllowedToOccupyTurretPoint ( entity , requestedTurretPoint ) )
2020-07-20 03:51:14 -07:00
turretPoint = requestedTurretPoint ;
}
else
2021-04-04 22:22:25 -07:00
turretPoint = this . turretPoints . find ( turret => ! turret . entity && this . AllowedToOccupyTurretPoint ( entity , turret ) ) ;
2020-07-20 03:51:14 -07:00
if ( ! turretPoint )
return false ;
turretPoint . entity = entity ;
2021-03-26 03:18:30 -07:00
2020-07-20 03:51:14 -07:00
// Angle of turrets:
// Renamed entities (turretPoint != undefined) should keep their angle.
// Otherwise if an angle is given in the turretPoint, use it.
// If no such angle given (usually walls for which outside/inside not well defined), we keep
// the current angle as it was used for garrisoning and thus quite often was from inside to
// outside, except when garrisoning from outWorld where we take as default PI.
if ( ! turretPoint && turretPoint . angle != null )
cmpPositionOccupant . SetYRotation ( cmpPositionSelf . GetRotation ( ) . y + turretPoint . angle ) ;
else if ( ! turretPoint && ! cmpPosition . IsInWorld ( ) )
cmpPositionOccupant . SetYRotation ( cmpPositionSelf . GetRotation ( ) . y + Math . PI ) ;
cmpPositionOccupant . SetTurretParent ( this . entity , turretPoint . offset ) ;
Engine . PostMessage ( this . entity , MT _TurretsChanged , {
"added" : [ entity ] ,
"removed" : [ ]
} ) ;
return true ;
}
2020-11-11 11:40:44 -08:00
/ * *
* @ param { number } entity - The entityID of the entity .
* @ param { String } turretName - The name of the turret point to occupy .
* @ return { boolean } - Whether the occupation has succeeded .
* /
2021-04-04 22:22:25 -07:00
OccupyNamedTurretPoint ( entity , turretName )
2020-11-11 11:40:44 -08:00
{
2021-04-04 22:22:25 -07:00
return this . OccupyTurretPoint ( entity , this . TurretPointByName ( turretName ) ) ;
}
/ * *
* @ param { string } turretPointName - The name of the requested turret point .
* @ return { Object } - The requested turret point .
* /
TurretPointByName ( turretPointName )
{
return this . turretPoints . find ( turret => turret . name == turretPointName ) ;
2020-11-11 11:40:44 -08:00
}
2020-07-20 03:51:14 -07:00
/ * *
* Remove the entity from a turret .
* @ param { number } entity - The specific entity to eject .
2021-04-04 22:22:25 -07:00
* @ param { boolean } forced - Whether ejection is forced ( e . g . due to death or renaming ) .
2020-07-20 03:51:14 -07:00
* @ param { Object } turret - Optionally the turret to abandon .
*
2021-04-04 22:22:25 -07:00
* @ return { boolean } - Whether the entity succesfully left us .
2020-07-20 03:51:14 -07:00
* /
2021-04-04 22:22:25 -07:00
LeaveTurretPoint ( entity , forced , requestedTurretPoint )
2020-07-20 03:51:14 -07:00
{
let turretPoint ;
if ( requestedTurretPoint )
{
if ( requestedTurretPoint . entity == entity )
turretPoint = requestedTurretPoint ;
}
else
2021-04-04 22:22:25 -07:00
turretPoint = this . GetOccupiedTurretPoint ( entity ) ;
2020-07-20 03:51:14 -07:00
2021-04-04 22:22:25 -07:00
if ( ! turretPoint || ( ! turretPoint . ejectable && ! forced ) )
2020-07-20 03:51:14 -07:00
return false ;
turretPoint . entity = null ;
Engine . PostMessage ( this . entity , MT _TurretsChanged , {
"added" : [ ] ,
"removed" : [ entity ]
} ) ;
return true ;
}
/ * *
* @ param { number } entity - The entity ' s id .
* @ param { Object } turret - Optionally the turret to check .
*
* @ return { boolean } - Whether the entity is positioned on a turret of this entity .
* /
2021-04-04 22:22:25 -07:00
OccupiesTurretPoint ( entity , requestedTurretPoint )
2020-07-20 03:51:14 -07:00
{
return requestedTurretPoint ? requestedTurretPoint . entity == entity :
2021-04-04 22:22:25 -07:00
! ! this . GetOccupiedTurretPoint ( entity ) ;
2020-07-20 03:51:14 -07:00
}
/ * *
* @ param { number } entity - The entity ' s id .
* @ return { Object } - The turret this entity is positioned on , if applicable .
* /
2021-04-04 22:22:25 -07:00
GetOccupiedTurretPoint ( entity )
2020-07-20 03:51:14 -07:00
{
return this . turretPoints . find ( turretPoint => turretPoint . entity == entity ) ;
}
2020-11-11 11:40:44 -08:00
/ * *
* @ param { number } entity - The entity ' s id .
* @ return { Object } - The turret this entity is positioned on , if applicable .
* /
2021-04-04 22:22:25 -07:00
GetOccupiedTurretPointName ( entity )
2020-11-11 11:40:44 -08:00
{
2025-05-12 04:10:26 -07:00
const turret = this . GetOccupiedTurretPoint ( entity ) ;
2021-03-26 03:18:30 -07:00
return turret ? turret . name : "" ;
2020-11-11 11:40:44 -08:00
}
2026-05-05 09:32:58 -07:00
/ * *
* Calculate the closest horizontal distance an external entity could ever get
* to the specified turret point . If the holder is passable , returns 0.
* Otherwise returns the perpendicular distance from the turret point to the
* nearest edge of the holder ' s obstruction .
*
* @ param { string | Object } turretPoint - The turret point name or object .
* @ return { number } - The minimum possible horizontal distance .
* /
GetClosestApproachDistanceToTurretPoint ( turretPoint )
{
if ( typeof turretPoint === "string" )
turretPoint = this . TurretPointByName ( turretPoint ) ;
if ( ! turretPoint )
return 0 ;
const cmpObstruction = Engine . QueryInterface ( this . entity , IID _Obstruction ) ;
if ( ! cmpObstruction || ! cmpObstruction . GetBlockMovementFlag ( false ) )
return 0 ;
const dxLocal = turretPoint . offset . x ;
const dzLocal = turretPoint . offset . z ;
const halfSizes = cmpObstruction . GetObstructionHalfSizes ( ) ;
const hw = halfSizes . x ;
const hh = halfSizes . y ;
if ( hw == null || hh == null || hw < 0 || hh < 0 )
return 0 ;
return Math . max ( 0 , Math . min ( hw - Math . abs ( dxLocal ) , hh - Math . abs ( dzLocal ) ) ) ;
}
2020-11-11 11:40:44 -08:00
/ * *
* @ return { number [ ] } - The turretted entityIDs .
* /
GetEntities ( )
{
2025-05-12 04:10:26 -07:00
const entities = [ ] ;
for ( const turretPoint of this . turretPoints )
2020-11-11 11:40:44 -08:00
if ( turretPoint . entity )
entities . push ( turretPoint . entity ) ;
return entities ;
}
2021-03-26 03:18:30 -07:00
/ * *
* @ return { boolean } - Whether all the turret points are occupied .
* /
IsFull ( )
{
return ! ! this . turretPoints . find ( turretPoint => turretPoint . entity == null ) ;
}
/ * *
* @ return { Object } - Max and min ranges at which entities can occupy any turret .
* /
2021-03-30 04:17:46 -07:00
LoadingRange ( )
2021-03-26 03:18:30 -07:00
{
return { "min" : 0 , "max" : + ( this . template . LoadingRange || 2 ) } ;
}
/ * *
* @ param { number } ent - The entity ID of the turret to be potentially picked up .
* @ return { boolean } - Whether this entity can pick the specified entity up .
* /
CanPickup ( ent )
{
if ( ! this . template . Pickup || this . IsFull ( ) )
return false ;
2025-05-12 04:10:26 -07:00
const cmpOwner = Engine . QueryInterface ( this . entity , IID _Ownership ) ;
2021-03-26 03:18:30 -07:00
return ! ! cmpOwner && IsOwnedByPlayer ( cmpOwner . GetOwner ( ) , ent ) ;
}
/ * *
* @ param { number [ ] } entities - The entities to ask to leave or to kill .
* /
EjectOrKill ( entities )
{
2025-05-12 04:10:26 -07:00
const removedEntities = [ ] ;
for ( const entity of entities )
2021-03-26 03:18:30 -07:00
{
2025-05-12 04:10:26 -07:00
const cmpTurretable = Engine . QueryInterface ( entity , IID _Turretable ) ;
2021-03-26 03:18:30 -07:00
if ( ! cmpTurretable || ! cmpTurretable . LeaveTurret ( true ) )
{
2025-05-12 04:10:26 -07:00
const cmpHealth = Engine . QueryInterface ( entity , IID _Health ) ;
2021-03-26 03:18:30 -07:00
if ( cmpHealth )
cmpHealth . Kill ( ) ;
else
Engine . DestroyEntity ( entity ) ;
removedEntities . push ( entity ) ;
}
}
if ( removedEntities . length )
Engine . PostMessage ( this . entity , MT _TurretsChanged , {
"added" : [ ] ,
"removed" : removedEntities
} ) ;
}
2020-11-11 11:40:44 -08:00
/ * *
* Sets an init turret , present from game start . ( E . g . set in Atlas . )
* @ param { String } turretName - The name of the turret point to be used .
* @ param { number } entity - The entity - ID to be placed .
* /
SetInitEntity ( turretName , entity )
{
if ( ! this . initTurrets )
this . initTurrets = new Map ( ) ;
if ( this . initTurrets . has ( turretName ) )
warn ( "The turret position " + turretName + " of entity " +
this . entity + " is already set! Overwriting." ) ;
this . initTurrets . set ( turretName , entity ) ;
}
2021-03-07 23:01:17 -08:00
/ * *
* Update list of turreted entities when a game inits .
* /
OnGlobalSkirmishReplacerReplaced ( msg )
{
if ( ! this . initTurrets )
return ;
if ( msg . entity == this . entity )
{
2025-05-12 04:10:26 -07:00
const cmpTurretHolder = Engine . QueryInterface ( msg . newentity , IID _TurretHolder ) ;
2021-03-07 23:01:17 -08:00
if ( cmpTurretHolder )
cmpTurretHolder . initTurrets = this . initTurrets ;
}
else
{
2025-05-12 04:10:26 -07:00
const entityIndex = this . initTurrets . indexOf ( msg . entity ) ;
2021-03-07 23:01:17 -08:00
if ( entityIndex != - 1 )
this . initTurrets [ entityIndex ] = msg . newentity ;
}
}
2020-11-11 11:40:44 -08:00
/ * *
2021-03-26 03:18:30 -07:00
* Initialise turreted units .
2020-11-11 11:40:44 -08:00
* /
OnGlobalInitGame ( msg )
{
if ( ! this . initTurrets )
return ;
2025-05-12 04:10:26 -07:00
for ( const [ turretPointName , entity ] of this . initTurrets )
2021-04-04 22:22:25 -07:00
{
2025-05-12 04:10:26 -07:00
const cmpTurretable = Engine . QueryInterface ( entity , IID _Turretable ) ;
2021-04-04 22:22:25 -07:00
if ( ! cmpTurretable || ! cmpTurretable . OccupyTurret ( this . entity , turretPointName , this . TurretPointByName ( turretPointName ) . ejectable ) )
2020-11-11 11:40:44 -08:00
warn ( "Entity " + entity + " could not occupy the turret point " +
turretPointName + " of turret holder " + this . entity + "." ) ;
2021-04-04 22:22:25 -07:00
}
2020-11-11 11:40:44 -08:00
delete this . initTurrets ;
}
2021-03-26 03:18:30 -07:00
/ * *
* @ param { Object } msg - { "entity" : number , "newentity" : number } .
* /
OnEntityRenamed ( msg )
{
2025-05-12 04:10:26 -07:00
for ( const entity of this . GetEntities ( ) )
2021-03-26 03:18:30 -07:00
{
2025-05-12 04:10:26 -07:00
const cmpTurretable = Engine . QueryInterface ( entity , IID _Turretable ) ;
2021-03-26 03:18:30 -07:00
if ( ! cmpTurretable )
continue ;
2025-05-12 04:10:26 -07:00
const currentPoint = this . GetOccupiedTurretPointName ( entity ) ;
2021-03-26 03:18:30 -07:00
cmpTurretable . LeaveTurret ( true ) ;
cmpTurretable . OccupyTurret ( msg . newentity , currentPoint ) ;
}
}
/ * *
* @ param { Object } msg - { "entity" : number , "from" : number , "to" : number } .
* /
OnOwnershipChanged ( msg )
{
2021-04-04 22:22:25 -07:00
if ( msg . to === INVALID _PLAYER )
{
this . EjectOrKill ( this . GetEntities ( ) ) ;
2021-03-26 03:18:30 -07:00
return ;
2021-04-04 22:22:25 -07:00
}
2023-05-10 08:13:52 -07:00
2025-05-12 04:10:26 -07:00
for ( const point of this . turretPoints )
2021-04-04 22:22:25 -07:00
{
// If we were created, create any subunits now.
// This has to be done here (instead of on Init)
// for Ownership ought to be initialised.
if ( point . template && msg . from === INVALID _PLAYER )
{
this . CreateSubunit ( point . name ) ;
continue ;
}
if ( ! point . entity )
continue ;
if ( ! point . ejectable )
{
2025-05-12 04:10:26 -07:00
const cmpTurretOwnership = Engine . QueryInterface ( point . entity , IID _Ownership ) ;
2021-04-04 22:22:25 -07:00
if ( cmpTurretOwnership )
cmpTurretOwnership . SetOwner ( msg . to ) ;
}
else if ( ! IsOwnedByMutualAllyOfEntity ( point . entity , this . entity ) )
2021-03-26 03:18:30 -07:00
{
2025-05-12 04:10:26 -07:00
const cmpTurretable = Engine . QueryInterface ( point . entity , IID _Turretable ) ;
2021-03-26 03:18:30 -07:00
if ( cmpTurretable )
cmpTurretable . LeaveTurret ( ) ;
}
2021-04-04 22:22:25 -07:00
}
delete this . reservedTurrets ;
2021-03-26 03:18:30 -07:00
}
2020-07-20 03:51:14 -07:00
}
TurretHolder . prototype . Schema =
"<element name='TurretPoints' a:help='Points that will be used to visibly garrison a unit.'>" +
"<oneOrMore>" +
"<element a:help='Element containing the offset coordinates.'>" +
"<anyName/>" +
"<interleave>" +
"<element name='X'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Y'>" +
"<data type='decimal'/>" +
"</element>" +
"<element name='Z'>" +
"<data type='decimal'/>" +
"</element>" +
2021-04-04 22:22:25 -07:00
"<optional>" +
"<interleave>" +
"<element name='Template'>" +
"<text/>" +
"</element>" +
"<element name='Ejectable' a:help='Whether this template is tied to the turret position (i.e. not allowed to leave the turret point).'>" +
"<data type='boolean'/>" +
"</element>" +
"</interleave>" +
"</optional>" +
2020-07-20 03:51:14 -07:00
"<optional>" +
"<element name='AllowedClasses' a:help='If specified, only entities matching the given classes will be able to use this turret.'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Angle' a:help='Angle in degrees relative to the turretHolder direction.'>" +
"<data type='decimal'/>" +
"</element>" +
"</optional>" +
"</interleave>" +
"</element>" +
"</oneOrMore>" +
2021-03-26 03:18:30 -07:00
"</element>" +
"<optional>" +
"<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to occupy a turret point. Should be about 2.0 for land entities and preferably greater for ships.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
2025-05-03 00:33:38 -07:00
"</optional>" +
2021-03-26 03:18:30 -07:00
"<optional>" +
"<element name='Pickup' a:help='This entity will try to move to pick up units to be turreted.'>" +
"<data type='boolean'/>" +
"</element>" +
"</optional>" ;
2020-07-20 03:51:14 -07:00
Engine . RegisterComponentType ( IID _TurretHolder , "TurretHolder" , TurretHolder ) ;