2010-09-03 11:55:14 +02:00
function Formation ( ) { }
Formation . prototype . Schema =
2014-01-05 19:50:31 +01:00
"<element name='FormationName' a:help='Name of the formation'>" +
2014-01-05 11:09:42 +01:00
"<text/>" +
2014-01-05 18:13:22 +01:00
"</element>" +
2014-01-08 09:02:15 +01:00
"<element name='RequiredMemberCount' a:help='Minimum number of entities the formation should contain'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<element name='DisabledTooltip' a:help='Tooltip shown when the formation is disabled'>" +
"<text/>" +
"</element>" +
2014-01-05 19:50:31 +01:00
"<element name='SpeedMultiplier' a:help='The speed of the formation is determined by the minimum speed of all members, multiplied with this number.'>" +
2014-01-05 18:13:22 +01:00
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
2014-01-06 10:10:46 +01:00
"<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
2014-01-05 18:13:22 +01:00
"<text/>" +
"</element>" +
2014-01-05 19:50:31 +01:00
"<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows'>" +
2014-01-05 18:13:22 +01:00
"<text/>" +
"</element>" +
2014-01-06 14:32:48 +01:00
"<element name='SortingClasses' a:help='Classes will be added to the formation in this order. Where the classes will be added first depends on the formation'>" +
"<text/>" +
"</element>" +
"<optional>" +
"<element name='SortingOrder' a:help='The order of sorting. This defaults to an order where the formation is filled from the first row to the last, and the center of each row to the sides. Other possible sort orders are \"fillFromTheSides\", where the most important units are on the sides of each row, and \"fillToTheCenter\", where the most vulerable units are right in the center of the formation. '>" +
"<text/>" +
"</element>" +
"</optional>" +
2014-01-05 19:50:31 +01:00
"<element name='WidthDepthRatio' a:help='Average width/depth, counted in number of units.'>" +
2014-01-05 18:13:22 +01:00
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<optional>" +
2014-01-05 19:50:31 +01:00
"<element name='MinColumns' a:help='When possible, this number of colums will be created. Overriding the wanted width depth ratio'>" +
2014-01-05 18:13:22 +01:00
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
2014-01-05 19:50:31 +01:00
"<element name='MaxColumns' a:help='When possible within the number of units, and the maximum number of rows, this will be the maximum number of columns.'>" +
2014-01-05 18:13:22 +01:00
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
2014-01-05 19:50:31 +01:00
"<element name='MaxRows' a:help='The maximum number of rows in the formation'>" +
2014-01-05 18:13:22 +01:00
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
2014-01-05 19:50:31 +01:00
"<optional>" +
"<element name='CenterGap' a:help='The size of the central gap, expressed in number of units wide'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</optional>" +
"<element name='UnitSeparationWidthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
2014-01-05 18:13:22 +01:00
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
2014-01-05 19:50:31 +01:00
"<element name='UnitSeparationDepthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
2014-01-05 18:13:22 +01:00
"<ref name='nonNegativeDecimal'/>" +
2014-01-07 21:21:55 +01:00
"</element>" +
"<element name='Animations' a:help='Give a list of animations to use for the particular formation members, based on their positions'>" +
"<zeroOrMore>" +
"<element a:help='The name of the default animation (walk, idle, attack_ranged...) that will be transformed in the formation-specific ResetMoveAnimation'>" +
"<anyName/>" +
"<text a:help='example text: \"1..1,1..-1:animation1;2..2,1..-1;animation2\", this will set animation1 for the first row, and animation2 for the second row. The first part of the numbers (1..1 and 2..2) means the row range. Every row between (and including) those values will switch animations. The second part of the numbers (1..-1) denote the columns inside those rows that will be affected. Note that in both cases, you can use -1 for the last row/column, -2 for the second to last, etc.'/>" +
"</element>" +
"</zeroOrMore>" +
2014-01-05 11:09:42 +01:00
"</element>" ;
2010-09-03 11:55:14 +02:00
2012-03-03 21:47:49 +01:00
var g _ColumnDistanceThreshold = 128 ; // distance at which we'll switch between column/box formations
2010-10-02 21:40:30 +02:00
2010-09-03 11:55:14 +02:00
Formation . prototype . Init = function ( )
{
2014-01-05 18:13:22 +01:00
this . formationShape = this . template . FormationShape ;
2014-01-06 14:32:48 +01:00
this . sortingClasses = this . template . SortingClasses . split ( /\s+/g ) ;
this . sortingOrder = this . template . SortingOrder ;
2014-01-05 18:13:22 +01:00
this . shiftRows = this . template . ShiftRows == "true" ;
this . separationMultiplier = {
"width" : + this . template . UnitSeparationWidthMultiplier ,
"depth" : + this . template . UnitSeparationDepthMultiplier
} ;
this . widthDepthRatio = + this . template . WidthDepthRatio ;
this . minColumns = + ( this . template . MinColumns || 0 ) ;
this . maxColumns = + ( this . template . MaxColumns || 0 ) ;
this . maxRows = + ( this . template . MaxRows || 0 ) ;
2014-01-05 19:50:31 +01:00
this . centerGap = + ( this . template . CenterGap || 0 ) ;
2014-01-05 18:13:22 +01:00
2014-01-07 21:21:55 +01:00
var animations = this . template . Animations ;
this . animations = { }
for ( var animationName in animations )
{
var differentAnimations = animations [ animationName ] . split ( /\s*;\s*/ ) ;
this . animations [ animationName ] = [ ] ;
// loop over the different rectangulars that will map to different animations
for each ( var rectAnimation in differentAnimations )
{
var rect , replacementAnimationName ;
[ rect , replacementAnimationName ] = rectAnimation . split ( /\s*:\s*/ ) ;
var rows , columns ;
[ rows , columns ] = rect . split ( /\s*,\s*/ ) ;
var minRow , maxRow , minColumn , maxColumn ;
[ minRow , maxRow ] = rows . split ( /\s*\.\.\s*/ ) ;
[ minColumn , maxColumn ] = columns . split ( /\s*\.\.\s*/ ) ;
this . animations [ animationName ] . push ( {
"minRow" : + minRow ,
"maxRow" : + maxRow ,
"minColumn" : + minColumn ,
"maxColumn" : + maxColumn ,
"animation" : replacementAnimationName
} ) ;
}
}
2010-10-02 21:40:30 +02:00
this . members = [ ] ; // entity IDs currently belonging to this formation
2014-01-07 21:21:55 +01:00
this . memberPositions = { } ;
this . maxRowsUsed = 0 ;
this . maxColumnsUsed = [ ] ;
2012-08-31 10:20:36 +02:00
this . inPosition = [ ] ; // entities that have reached their final position
2010-10-02 21:40:30 +02:00
this . columnar = false ; // whether we're travelling in column (vs box) formation
2012-06-04 01:00:36 +02:00
this . rearrange = true ; // whether we should rearrange all formation members
2013-11-27 17:30:14 +01:00
this . formationMembersWithAura = [ ] ; // Members with a formation aura
2013-12-05 20:26:55 +01:00
this . width = 0 ;
this . depth = 0 ;
2013-12-06 11:21:07 +01:00
this . oldOrientation = { "sin" : 0 , "cos" : 0 } ;
2014-01-02 21:04:50 +01:00
this . twinFormations = [ ] ;
// distance from which two twin formations will merge into one.
this . formationSeparation = 0 ;
Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer )
. SetInterval ( this . entity , IID _Formation , "ShapeUpdate" , 1000 , 1000 , null ) ;
} ;
/ * *
* Set the value from which two twin formations will become one .
* /
Formation . prototype . SetFormationSeparation = function ( value )
{
this . formationSeparation = value ;
2013-12-05 20:26:55 +01:00
} ;
Formation . prototype . GetSize = function ( )
{
return { "width" : this . width , "depth" : this . depth } ;
2010-09-03 11:55:14 +02:00
} ;
2014-01-05 18:13:22 +01:00
Formation . prototype . GetSpeedMultiplier = function ( )
{
return + this . template . SpeedMultiplier ;
} ;
2010-09-03 11:55:14 +02:00
Formation . prototype . GetMemberCount = function ( )
{
return this . members . length ;
} ;
2010-10-03 19:58:49 +02:00
/ * *
* Returns the 'primary' member of this formation ( typically the most
* important unit type ) , for e . g . playing a representative sound .
* Returns undefined if no members .
* TODO : actually implement something like that ; currently this just returns
* the arbitrary first one .
* /
Formation . prototype . GetPrimaryMember = function ( )
{
return this . members [ 0 ] ;
} ;
2014-01-07 21:21:55 +01:00
/ * *
* Get the formation animation for a certain member of this formation
* @ param entity The entity ID to get the animation for
* @ param defaultAnimation The name of the default wanted animation for the entity
* E . g . "walk" , "idle" ...
* @ return The name of the transformed animation as defined in the template
* E . g . "walk_testudo_row1"
* /
Formation . prototype . GetFormationAnimation = function ( entity , defaultAnimation )
{
var animationGroup = this . animations [ defaultAnimation ] ;
if ( ! animationGroup || this . columnar )
return defaultAnimation ;
var row = this . memberPositions [ entity ] . row ;
var column = this . memberPositions [ entity ] . column ;
for ( var i = 0 ; i < animationGroup . length ; ++ i )
{
var minRow = animationGroup [ i ] . minRow ;
if ( minRow < 0 )
minRow += this . maxRowsUsed + 1 ;
if ( row < minRow )
continue ;
var maxRow = animationGroup [ i ] . maxRow ;
if ( maxRow < 0 )
maxRow += this . maxRowsUsed + 1 ;
if ( row > maxRow )
continue ;
var minColumn = animationGroup [ i ] . minColumn ;
if ( minColumn < 0 )
minColumn += this . maxColumnsUsed [ row ] + 1 ;
if ( column < minColumn )
continue ;
var maxColumn = animationGroup [ i ] . maxColumn ;
if ( maxColumn < 0 )
maxColumn += this . maxColumnsUsed [ row ] + 1 ;
if ( column > maxColumn )
continue ;
return animationGroup [ i ] . animation ;
}
return defaultAnimation ;
} ;
2012-08-31 10:20:36 +02:00
/ * *
2014-01-08 10:47:27 +01:00
* Permits formation members to register that they ' ve reached their destination .
2012-08-31 10:20:36 +02:00
* /
Formation . prototype . SetInPosition = function ( ent )
{
if ( this . inPosition . indexOf ( ent ) != - 1 )
return ;
this . inPosition . push ( ent ) ;
} ;
2012-09-05 01:27:06 +02:00
/ * *
* Called by formation members upon entering non - walking states .
* /
Formation . prototype . UnsetInPosition = function ( ent )
{
var ind = this . inPosition . indexOf ( ent ) ;
if ( ind != - 1 )
this . inPosition . splice ( ind , 1 ) ;
}
2012-06-04 01:00:36 +02:00
/ * *
* Set whether we should rearrange formation members if
* units are removed from the formation .
* /
Formation . prototype . SetRearrange = function ( rearrange )
{
this . rearrange = rearrange ;
} ;
2010-09-03 11:55:14 +02:00
/ * *
* Initialise the members of this formation .
* Must only be called once .
* All members must implement UnitAI .
* /
Formation . prototype . SetMembers = function ( ents )
{
this . members = ents ;
2014-01-07 14:33:08 +01:00
var cmpTemplateManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _TemplateManager ) ;
var templateName = cmpTemplateManager . GetCurrentTemplateName ( this . entity ) ;
2010-09-03 11:55:14 +02:00
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
cmpUnitAI . SetFormationController ( this . entity ) ;
2014-01-07 14:33:08 +01:00
cmpUnitAI . SetLastFormationTemplate ( templateName ) ;
2013-10-14 17:51:21 +02:00
var cmpAuras = Engine . QueryInterface ( ent , IID _Auras ) ;
if ( cmpAuras && cmpAuras . HasFormationAura ( ) )
{
2013-11-27 17:30:14 +01:00
this . formationMembersWithAura . push ( ent ) ;
2013-10-14 17:51:21 +02:00
cmpAuras . ApplyFormationBonus ( ents ) ;
}
2010-09-03 11:55:14 +02:00
}
2013-12-06 11:21:07 +01:00
this . offsets = undefined ;
2010-09-03 11:55:14 +02:00
// Locate this formation controller in the middle of its members
this . MoveToMembersCenter ( ) ;
this . ComputeMotionParameters ( ) ;
} ;
/ * *
* Remove the given list of entities .
* The entities must already be members of this formation .
* /
Formation . prototype . RemoveMembers = function ( ents )
{
2013-12-08 20:57:34 +01:00
this . offsets = undefined ;
2010-09-03 11:55:14 +02:00
this . members = this . members . filter ( function ( e ) { return ents . indexOf ( e ) == - 1 ; } ) ;
2012-08-31 10:20:36 +02:00
this . inPosition = this . inPosition . filter ( function ( e ) { return ents . indexOf ( e ) == - 1 ; } ) ;
2010-09-03 11:55:14 +02:00
for each ( var ent in ents )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
2013-11-01 23:00:06 +01:00
cmpUnitAI . UpdateWorkOrders ( ) ;
2010-09-03 11:55:14 +02:00
cmpUnitAI . SetFormationController ( INVALID _ENTITY ) ;
}
2013-11-27 17:30:14 +01:00
for each ( var ent in this . formationMembersWithAura )
2013-10-14 17:51:21 +02:00
{
var cmpAuras = Engine . QueryInterface ( ent , IID _Auras ) ;
cmpAuras . RemoveFormationBonus ( ents ) ;
// the unit with the aura is also removed from the formation
if ( ents . indexOf ( ent ) !== - 1 )
cmpAuras . RemoveFormationBonus ( this . members ) ;
}
2013-11-27 17:30:14 +01:00
this . formationMembersWithAura = this . formationMembersWithAura . filter ( function ( e ) { return ents . indexOf ( e ) == - 1 ; } ) ;
2013-10-14 17:51:21 +02:00
2010-09-03 11:55:14 +02:00
// If there's nobody left, destroy the formation
if ( this . members . length == 0 )
{
Engine . DestroyEntity ( this . entity ) ;
return ;
}
2012-06-04 01:00:36 +02:00
if ( ! this . rearrange )
return ;
2010-09-03 11:55:14 +02:00
this . ComputeMotionParameters ( ) ;
// Rearrange the remaining members
2012-12-02 02:52:27 +01:00
this . MoveMembersIntoFormation ( true , true ) ;
2010-09-03 11:55:14 +02:00
} ;
2014-01-02 21:04:50 +01:00
Formation . prototype . AddMembers = function ( ents )
{
this . offsets = undefined ;
this . inPosition = [ ] ;
for each ( var ent in this . formationMembersWithAura )
{
var cmpAuras = Engine . QueryInterface ( ent , IID _Auras ) ;
cmpAuras . RemoveFormationBonus ( ents ) ;
// the unit with the aura is also removed from the formation
if ( ents . indexOf ( ent ) !== - 1 )
cmpAuras . RemoveFormationBonus ( this . members ) ;
}
this . members = this . members . concat ( ents ) ;
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
cmpUnitAI . SetFormationController ( this . entity ) ;
var cmpAuras = Engine . QueryInterface ( ent , IID _Auras ) ;
if ( cmpAuras && cmpAuras . HasFormationAura ( ) )
{
this . formationMembersWithAura . push ( ent ) ;
cmpAuras . ApplyFormationBonus ( ents ) ;
}
}
this . MoveMembersIntoFormation ( true , true ) ;
} ;
2012-09-04 05:57:22 +02:00
/ * *
* Called when the formation stops moving in order to detect
* units that have already reached their final positions .
* /
Formation . prototype . FindInPosition = function ( )
{
for ( var i = 0 ; i < this . members . length ; ++ i )
{
var cmpUnitMotion = Engine . QueryInterface ( this . members [ i ] , IID _UnitMotion ) ;
if ( ! cmpUnitMotion . IsMoving ( ) )
2012-09-05 01:27:06 +02:00
{
// Verify that members are stopped in FORMATIONMEMBER.WALKING
var cmpUnitAI = Engine . QueryInterface ( this . members [ i ] , IID _UnitAI ) ;
if ( cmpUnitAI . IsWalking ( ) )
this . SetInPosition ( this . members [ i ] ) ;
}
2012-09-04 05:57:22 +02:00
}
}
2010-09-03 11:55:14 +02:00
/ * *
* Remove all members and destroy the formation .
* /
Formation . prototype . Disband = function ( )
{
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
cmpUnitAI . SetFormationController ( INVALID _ENTITY ) ;
}
2013-11-27 17:30:14 +01:00
for each ( var ent in this . formationMembersWithAura )
2013-10-14 17:51:21 +02:00
{
var cmpAuras = Engine . QueryInterface ( ent , IID _Auras ) ;
cmpAuras . RemoveFormationBonus ( this . members ) ;
}
2010-09-03 11:55:14 +02:00
this . members = [ ] ;
2012-08-31 10:20:36 +02:00
this . inPosition = [ ] ;
2013-11-27 17:30:14 +01:00
this . formationMembersWithAura = [ ] ;
2013-12-06 11:21:07 +01:00
this . offsets = undefined ;
2010-09-03 11:55:14 +02:00
Engine . DestroyEntity ( this . entity ) ;
} ;
/ * *
2010-09-03 22:04:11 +02:00
* Call obj . funcname ( args ) on UnitAI components of all members .
2010-09-03 11:55:14 +02:00
* /
2010-09-03 22:04:11 +02:00
Formation . prototype . CallMemberFunction = function ( funcname , args )
2010-09-03 11:55:14 +02:00
{
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
2010-09-03 22:04:11 +02:00
cmpUnitAI [ funcname ] . apply ( cmpUnitAI , args ) ;
2010-09-03 11:55:14 +02:00
}
} ;
2012-12-02 02:52:27 +01:00
/ * *
* Call obj . functname ( args ) on UnitAI components of all members ,
* and return true if all calls return true .
* /
Formation . prototype . TestAllMemberFunction = function ( funcname , args )
{
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
if ( ! cmpUnitAI [ funcname ] . apply ( cmpUnitAI , args ) )
return false ;
}
return true ;
} ;
2012-12-02 18:25:23 +01:00
Formation . prototype . GetMaxAttackRangeFunction = function ( target )
{
var result = 0 ;
var range = 0 ;
for each ( var ent in this . members )
{
var cmpAttack = Engine . QueryInterface ( ent , IID _Attack ) ;
if ( ! cmpAttack )
continue ;
2012-12-02 23:02:36 +01:00
var type = cmpAttack . GetBestAttackAgainst ( target ) ;
if ( ! type )
continue ;
range = cmpAttack . GetRange ( type ) ;
2012-12-02 18:25:23 +01:00
if ( range . max > result )
result = range . max ;
}
return result ;
} ;
2010-09-03 11:55:14 +02:00
/ * *
* Set all members to form up into the formation shape .
2010-10-15 21:25:17 +02:00
* If moveCenter is true , the formation center will be reinitialised
* to the center of the units .
2012-12-02 02:52:27 +01:00
* If force is true , all individual orders of the formation units are replaced ,
* otherwise the order to walk into formation is just pushed to the front .
2010-09-03 11:55:14 +02:00
* /
2012-12-02 02:52:27 +01:00
Formation . prototype . MoveMembersIntoFormation = function ( moveCenter , force )
2010-09-03 11:55:14 +02:00
{
var active = [ ] ;
var positions = [ ] ;
for each ( var ent in this . members )
{
var cmpPosition = Engine . QueryInterface ( ent , IID _Position ) ;
if ( ! cmpPosition || ! cmpPosition . IsInWorld ( ) )
continue ;
active . push ( ent ) ;
2013-12-05 11:23:49 +01:00
// query the 2D position as exact hight calculation isn't needed
// but bring the position to the right coordinates
var pos = cmpPosition . GetPosition2D ( ) ;
pos . z = pos . y ;
pos . y = undefined ;
positions . push ( pos ) ;
2010-09-03 11:55:14 +02:00
}
var avgpos = this . ComputeAveragePosition ( positions ) ;
2010-10-15 21:25:17 +02:00
// Reposition the formation if we're told to or if we don't already have a position
2010-09-03 11:55:14 +02:00
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
2013-06-17 01:10:01 +02:00
var inWorld = cmpPosition . IsInWorld ( ) ;
if ( moveCenter || ! inWorld )
{
2010-10-15 21:25:17 +02:00
cmpPosition . JumpTo ( avgpos . x , avgpos . z ) ;
2013-06-17 01:10:01 +02:00
// Don't make the formation controller entity show up in range queries
if ( ! inWorld )
{
var cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
cmpRangeManager . SetEntityFlag ( this . entity , "normal" , false ) ;
}
}
2010-09-03 11:55:14 +02:00
2014-01-02 21:04:50 +01:00
// Switch between column and box if necessary
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
var walkingDistance = cmpUnitAI . ComputeWalkingDistance ( ) ;
var columnar = walkingDistance > g _ColumnDistanceThreshold ;
if ( columnar != this . columnar )
{
this . columnar = columnar ;
this . offsets = undefined ;
}
2014-01-08 11:55:44 +01:00
var newOrientation = this . GetEstimatedOrientation ( avgpos ) ;
2013-12-06 11:21:07 +01:00
var dSin = Math . abs ( newOrientation . sin - this . oldOrientation . sin ) ;
var dCos = Math . abs ( newOrientation . cos - this . oldOrientation . cos ) ;
// If the formation existed, only recalculate positions if the turning agle is somewhat biggish
if ( ! this . offsets || dSin > 1 || dCos > 1 )
2014-01-06 11:16:28 +01:00
this . offsets = this . ComputeFormationOffsets ( active , positions ) ;
2013-12-06 11:21:07 +01:00
this . oldOrientation = newOrientation ;
2013-12-05 20:26:55 +01:00
var xMax = 0 ;
var zMax = 0 ;
2013-12-04 14:14:31 +01:00
2013-12-06 11:21:07 +01:00
for ( var i = 0 ; i < this . offsets . length ; ++ i )
2010-09-03 11:55:14 +02:00
{
2013-12-06 11:21:07 +01:00
var offset = this . offsets [ i ] ;
2010-09-03 11:55:14 +02:00
2011-05-01 22:40:53 +02:00
var cmpUnitAI = Engine . QueryInterface ( offset . ent , IID _UnitAI ) ;
2013-12-11 16:17:43 +01:00
if ( ! cmpUnitAI )
continue ;
2012-12-02 02:52:27 +01:00
if ( force )
{
cmpUnitAI . ReplaceOrder ( "FormationWalk" , {
"target" : this . entity ,
2013-12-04 14:14:31 +01:00
"x" : offset . x ,
"z" : offset . z
2012-12-02 02:52:27 +01:00
} ) ;
}
else
{
cmpUnitAI . PushOrderFront ( "FormationWalk" , {
"target" : this . entity ,
2013-12-04 14:14:31 +01:00
"x" : offset . x ,
"z" : offset . z
2012-12-02 02:52:27 +01:00
} ) ;
}
2013-12-05 20:26:55 +01:00
xMax = Math . max ( xMax , offset . x ) ;
zMax = Math . max ( zMax , offset . z ) ;
2010-09-03 11:55:14 +02:00
}
2013-12-05 20:26:55 +01:00
this . width = xMax * 2 ;
this . depth = zMax * 2 ;
2010-09-03 11:55:14 +02:00
} ;
Formation . prototype . MoveToMembersCenter = function ( )
{
var positions = [ ] ;
for each ( var ent in this . members )
{
var cmpPosition = Engine . QueryInterface ( ent , IID _Position ) ;
if ( ! cmpPosition || ! cmpPosition . IsInWorld ( ) )
continue ;
positions . push ( cmpPosition . GetPosition ( ) ) ;
}
var avgpos = this . ComputeAveragePosition ( positions ) ;
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
2013-06-17 01:10:01 +02:00
var inWorld = cmpPosition . IsInWorld ( ) ;
2010-09-03 11:55:14 +02:00
cmpPosition . JumpTo ( avgpos . x , avgpos . z ) ;
2013-06-17 01:10:01 +02:00
// Don't make the formation controller show up in range queries
if ( ! inWorld )
{
var cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
cmpRangeManager . SetEntityFlag ( this . entity , "normal" , false ) ;
}
2010-09-03 11:55:14 +02:00
} ;
2013-12-26 13:24:52 +01:00
Formation . prototype . GetAvgFootprint = function ( active )
2013-12-05 11:23:49 +01:00
{
2013-12-26 13:24:52 +01:00
var footprints = [ ] ;
2013-12-05 11:23:49 +01:00
for each ( var ent in active )
{
2013-12-26 13:24:52 +01:00
var cmpFootprint = Engine . QueryInterface ( ent , IID _Footprint ) ;
if ( cmpFootprint )
footprints . push ( cmpFootprint . GetShape ( ) ) ;
2013-12-05 11:23:49 +01:00
}
2013-12-26 13:24:52 +01:00
if ( ! footprints . length )
return { "width" : 1 , "depth" : 1 } ;
var r = { "width" : 0 , "depth" : 0 } ;
for each ( var shape in footprints )
{
if ( shape . type == "circle" )
{
r . width += shape . radius * 2 ;
r . depth += shape . radius * 2 ;
}
else if ( shape . type == "square" )
{
r . width += shape . width ;
r . depth += shape . depth ;
}
}
r . width /= footprints . length ;
r . depth /= footprints . length ;
return r ;
2013-12-05 11:23:49 +01:00
} ;
2014-01-06 11:16:28 +01:00
Formation . prototype . ComputeFormationOffsets = function ( active , positions )
2010-09-03 11:55:14 +02:00
{
2013-12-26 13:24:52 +01:00
var separation = this . GetAvgFootprint ( active ) ;
2014-01-05 18:13:22 +01:00
separation . width *= this . separationMultiplier . width ;
separation . depth *= this . separationMultiplier . depth ;
2010-09-03 11:55:14 +02:00
2014-01-06 14:32:48 +01:00
if ( this . columnar )
var sortingClasses = [ "Cavalry" , "Infantry" ] ;
else
2014-01-07 10:23:59 +01:00
var sortingClasses = this . sortingClasses . slice ( ) ;
sortingClasses . push ( "Unknown" ) ;
2014-01-06 14:32:48 +01:00
2013-12-04 14:14:31 +01:00
// the entities will be assigned to positions in the formation in
// the same order as the types list is ordered
2014-01-07 10:23:59 +01:00
var types = { } ;
2014-01-06 14:32:48 +01:00
for ( var i = 0 ; i < sortingClasses . length ; ++ i )
types [ sortingClasses [ i ] ] = [ ] ;
2011-05-01 22:40:53 +02:00
2013-12-05 11:23:49 +01:00
for ( var i in active )
2011-05-01 22:40:53 +02:00
{
2013-12-05 11:23:49 +01:00
var cmpIdentity = Engine . QueryInterface ( active [ i ] , IID _Identity ) ;
2011-05-01 22:40:53 +02:00
var classes = cmpIdentity . GetClassesList ( ) ;
var done = false ;
2014-01-06 14:32:48 +01:00
for ( var c = 0 ; c < sortingClasses . length ; ++ c )
2011-05-01 22:40:53 +02:00
{
2014-01-06 14:32:48 +01:00
if ( classes . indexOf ( sortingClasses [ c ] ) > - 1 )
2011-05-01 22:40:53 +02:00
{
2014-01-06 14:32:48 +01:00
types [ sortingClasses [ c ] ] . push ( { "ent" : active [ i ] , "pos" : positions [ i ] } ) ;
2011-05-01 22:40:53 +02:00
done = true ;
break ;
}
}
if ( ! done )
2013-12-05 11:23:49 +01:00
types [ "Unknown" ] . push ( { "ent" : active [ i ] , "pos" : positions [ i ] } ) ;
2011-05-01 22:40:53 +02:00
}
var count = active . length ;
2014-01-05 18:13:22 +01:00
var shape = this . formationShape ;
2014-01-05 19:50:31 +01:00
var shiftRows = this . shiftRows ;
var centerGap = this . centerGap ;
2014-01-06 14:32:48 +01:00
var sortingOrder = this . sortingOrder ;
2010-09-03 11:55:14 +02:00
2011-05-01 22:40:53 +02:00
var offsets = [ ] ;
// Choose a sensible size/shape for the various formations, depending on number of units
2010-09-03 11:55:14 +02:00
var cols ;
2012-05-04 01:32:10 +02:00
2014-01-06 11:16:28 +01:00
if ( this . columnar )
2014-01-05 18:13:22 +01:00
{
shape = "square" ;
cols = Math . min ( count , 3 ) ;
2014-01-05 19:50:31 +01:00
shiftRows = false ;
centerGap = 0 ;
2014-01-06 14:32:48 +01:00
sortingOrder = null ;
2014-01-05 18:13:22 +01:00
}
2013-12-15 10:51:13 +01:00
else
2014-01-05 18:13:22 +01:00
{
var depth = Math . sqrt ( count / this . widthDepthRatio ) ;
if ( this . maxRows && depth > this . maxRows )
depth = this . maxRows ;
cols = Math . ceil ( count / Math . ceil ( depth ) + ( this . shiftRows ? 0.5 : 0 ) ) ;
if ( cols < this . minColumns )
cols = Math . min ( count , this . minColumns ) ;
if ( this . maxColumns && cols > this . maxColumns && this . maxRows != depth )
cols = this . maxColumns ;
}
2012-05-04 01:32:10 +02:00
2014-01-05 18:13:22 +01:00
// define special formations here
2014-01-07 14:33:08 +01:00
if ( this . template . FormationName == "Scatter" )
2010-10-02 21:40:30 +02:00
{
2013-12-26 13:24:52 +01:00
var width = Math . sqrt ( count ) * ( separation . width + separation . depth ) * 2.5 ;
2011-05-01 22:40:53 +02:00
for ( var i = 0 ; i < count ; ++ i )
offsets . push ( { "x" : Math . random ( ) * width , "z" : Math . random ( ) * width } ) ;
2010-10-02 21:40:30 +02:00
}
2010-09-03 11:55:14 +02:00
2014-01-06 10:10:46 +01:00
// For non-special formations, calculate the positions based on the number of entities
2014-01-07 21:21:55 +01:00
this . maxColumnsUsed = [ ] ;
this . maxRowsUsed = 0 ;
2014-01-06 10:10:46 +01:00
if ( shape != "special" )
2011-05-01 22:40:53 +02:00
{
2014-01-06 14:32:48 +01:00
offsets = [ ] ;
2014-01-05 18:13:22 +01:00
var r = 0 ;
2011-05-01 22:40:53 +02:00
var left = count ;
2014-01-06 10:10:46 +01:00
// while there are units left, start a new row in the formation
2014-01-05 18:13:22 +01:00
while ( left > 0 )
2011-05-01 22:40:53 +02:00
{
2014-01-06 10:10:46 +01:00
// save the position of the row
var z = - r * separation . depth ;
// switch between the left and right side of the center to have a symmetrical distribution
var side = 1 ;
// determine the number of entities in this row of the formation
if ( shape == "square" )
{
var n = cols ;
if ( shiftRows )
n -= r % 2 ;
}
else if ( shape == "triangle" )
{
if ( shiftRows )
var n = r + 1 ;
else
var n = r * 2 + 1 ;
}
if ( ! shiftRows && n > left )
2014-01-05 18:13:22 +01:00
n = left ;
for ( var c = 0 ; c < n && left > 0 ; ++ c )
2011-05-01 22:40:53 +02:00
{
2014-01-06 10:10:46 +01:00
// switch sides for the next entity
side *= - 1 ;
2014-01-05 18:13:22 +01:00
if ( n % 2 == 0 )
2014-01-06 10:10:46 +01:00
var x = side * ( Math . floor ( c / 2 ) + 0.5 ) * separation . width ;
2014-01-05 18:13:22 +01:00
else
2014-01-06 10:10:46 +01:00
var x = side * Math . ceil ( c / 2 ) * separation . width ;
2014-01-05 19:50:31 +01:00
if ( centerGap )
{
if ( x == 0 ) // don't use the center position with a center gap
continue ;
2014-01-06 10:10:46 +01:00
x += side * centerGap / 2 ;
2014-01-05 19:50:31 +01:00
}
2014-01-07 21:21:55 +01:00
var column = Math . ceil ( n / 2 ) + Math . ceil ( c / 2 ) * side ;
offsets . push ( { "x" : x , "z" : z , "row" : r + 1 , "column" : column } ) ;
2014-01-05 18:13:22 +01:00
left --
2011-05-01 22:40:53 +02:00
}
2014-01-05 18:13:22 +01:00
++ r ;
2014-01-07 21:21:55 +01:00
this . maxColumnsUsed [ r ] = n ;
2011-05-01 22:40:53 +02:00
}
2014-01-07 21:21:55 +01:00
this . maxRowsUsed = r ;
2011-05-01 22:40:53 +02:00
}
2014-01-05 18:13:22 +01:00
2013-12-04 14:14:31 +01:00
// make sure the average offset is zero, as the formation is centered around that
// calculating offset distances without a zero average makes no sense, as the formation
// will jump to a different position any time
var avgoffset = this . ComputeAveragePosition ( offsets ) ;
for each ( var offset in offsets )
2011-05-01 22:40:53 +02:00
{
2013-12-04 14:14:31 +01:00
offset . x -= avgoffset . x ;
offset . z -= avgoffset . z ;
2011-05-01 22:40:53 +02:00
}
2013-12-04 14:14:31 +01:00
2013-12-05 11:23:49 +01:00
// sort the available places in certain ways
// the places first in the list will contain the heaviest units as defined by the order
// of the types list
2014-01-06 14:32:48 +01:00
if ( this . sortingOrder == "fillFromTheSides" )
offsets . sort ( function ( o1 , o2 ) { return Math . abs ( o1 . x ) < Math . abs ( o2 . x ) ; } ) ;
else if ( this . sortingOrder == "fillToTheCenter" )
offsets . sort ( function ( o1 , o2 ) {
return Math . max ( Math . abs ( o1 . x ) , Math . abs ( o1 . z ) ) < Math . max ( Math . abs ( o2 . x ) , Math . abs ( o2 . z ) ) ;
} ) ;
2013-12-05 11:23:49 +01:00
// query the 2D position of the formation, and bring to the right coordinate system
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
var formationPos = cmpPosition . GetPosition2D ( ) ;
formationPos . z = formationPos . y ;
formationPos . y = undefined ;
// use realistic place assignment,
2013-12-04 14:14:31 +01:00
// every soldier searches the closest available place in the formation
var newOffsets = [ ] ;
2013-12-05 11:23:49 +01:00
var realPositions = this . GetRealOffsetPositions ( offsets , formationPos ) ;
2014-01-06 14:32:48 +01:00
for ( var i = 0 ; i < sortingClasses . length ; ++ i )
2010-09-03 11:55:14 +02:00
{
2014-01-06 14:32:48 +01:00
var t = types [ sortingClasses [ i ] ] ;
2013-12-04 14:14:31 +01:00
var usedOffsets = offsets . splice ( 0 , t . length ) ;
var usedRealPositions = realPositions . splice ( 0 , t . length ) ;
2013-12-05 11:23:49 +01:00
for each ( var entPos in t )
2011-05-01 22:40:53 +02:00
{
2013-12-05 11:23:49 +01:00
var closestOffsetId = this . TakeClosestOffset ( entPos , usedRealPositions ) ;
2013-12-04 14:14:31 +01:00
usedRealPositions . splice ( closestOffsetId , 1 ) ;
newOffsets . push ( usedOffsets . splice ( closestOffsetId , 1 ) [ 0 ] ) ;
2013-12-05 11:23:49 +01:00
newOffsets [ newOffsets . length - 1 ] . ent = entPos . ent ;
2011-05-01 22:40:53 +02:00
}
2013-12-04 14:14:31 +01:00
}
2011-05-01 22:40:53 +02:00
2013-12-04 14:14:31 +01:00
return newOffsets ;
} ;
2011-05-01 22:40:53 +02:00
2013-12-04 14:14:31 +01:00
/ * *
* Search the closest position in the realPositions list to the given entity
* @ param ent , the queried entity
* @ param realPositions , the world coordinates of the available offsets
* @ return the index of the closest offset position
* /
2013-12-05 11:23:49 +01:00
Formation . prototype . TakeClosestOffset = function ( entPos , realPositions )
2013-12-04 14:14:31 +01:00
{
2013-12-05 11:23:49 +01:00
var pos = entPos . pos ;
2013-12-04 14:14:31 +01:00
var closestOffsetId = - 1 ;
var offsetDistanceSq = Infinity ;
for ( var i = 0 ; i < realPositions . length ; i ++ )
{
var dx = realPositions [ i ] . x - pos . x ;
2013-12-05 11:23:49 +01:00
var dz = realPositions [ i ] . z - pos . z ;
2013-12-04 14:14:31 +01:00
var distSq = dx * dx + dz * dz ;
if ( distSq < offsetDistanceSq )
2010-09-03 11:55:14 +02:00
{
2013-12-04 14:14:31 +01:00
offsetDistanceSq = distSq ;
closestOffsetId = i ;
2010-09-03 11:55:14 +02:00
}
2013-12-04 14:14:31 +01:00
}
2014-01-07 21:21:55 +01:00
this . memberPositions [ entPos . ent ] = { "row" : realPositions [ closestOffsetId ] . row , "column" : realPositions [ closestOffsetId ] . column } ;
2013-12-04 14:14:31 +01:00
return closestOffsetId ;
} ;
2011-05-01 22:40:53 +02:00
2013-12-04 14:14:31 +01:00
/ * *
* Get the world positions for a list of offsets in this formation
* /
2013-12-05 11:23:49 +01:00
Formation . prototype . GetRealOffsetPositions = function ( offsets , pos )
2013-12-04 14:14:31 +01:00
{
var offsetPositions = [ ] ;
2014-01-08 11:55:44 +01:00
var { sin , cos } = this . GetEstimatedOrientation ( pos ) ;
2013-12-06 11:21:07 +01:00
// calculate the world positions
for each ( var o in offsets )
offsetPositions . push ( {
2014-01-07 21:21:55 +01:00
"x" : pos . x + o . z * sin + o . x * cos ,
"z" : pos . z + o . z * cos - o . x * sin ,
"row" : o . row ,
"column" : o . column
2013-12-06 11:21:07 +01:00
} ) ;
return offsetPositions ;
} ;
/ * *
* calculate the estimated rotation of the formation
2014-01-08 11:55:44 +01:00
* based on the first unitAI target position when ordered to walk ,
* based on the current rotation in other cases
2013-12-06 11:21:07 +01:00
* Return the sine and cosine of the angle
* /
2014-01-08 11:55:44 +01:00
Formation . prototype . GetEstimatedOrientation = function ( pos )
2013-12-06 11:21:07 +01:00
{
2013-12-04 14:14:31 +01:00
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
2014-01-08 11:55:44 +01:00
var r = { "sin" : 0 , "cos" : 1 } ;
if ( cmpUnitAI . GetCurrentState ( ) == "FORMATIONCONTROLLER.WALKING" )
2013-12-04 14:14:31 +01:00
{
2014-01-08 11:55:44 +01:00
var targetPos = cmpUnitAI . GetTargetPositions ( ) ;
if ( ! targetPos . length )
return r ;
2013-12-04 14:14:31 +01:00
var dx = targetPos [ 0 ] . x - pos . x ;
2013-12-05 11:23:49 +01:00
var dz = targetPos [ 0 ] . z - pos . z ;
2014-01-08 11:55:44 +01:00
if ( ! dx && ! dz )
return r ;
var dist = Math . sqrt ( dx * dx + dz * dz ) ;
r . cos = dz / dist ;
r . sin = dx / dist ;
}
else
{
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
if ( ! cmpPosition )
return r ;
var rot = cmpPosition . GetRotation ( ) . y ;
r . sin = Math . sin ( rot ) ;
r . cos = Math . cos ( rot ) ;
2010-09-03 11:55:14 +02:00
}
2014-01-08 11:55:44 +01:00
return r ;
2010-09-03 11:55:14 +02:00
} ;
Formation . prototype . ComputeAveragePosition = function ( positions )
{
var sx = 0 ;
var sz = 0 ;
for each ( var pos in positions )
{
sx += pos . x ;
sz += pos . z ;
}
return { "x" : sx / positions . length , "z" : sz / positions . length } ;
} ;
/ * *
* Set formation controller ' s radius and speed based on its current members .
* /
Formation . prototype . ComputeMotionParameters = function ( )
{
var maxRadius = 0 ;
var minSpeed = Infinity ;
for each ( var ent in this . members )
{
var cmpObstruction = Engine . QueryInterface ( ent , IID _Obstruction ) ;
if ( cmpObstruction )
maxRadius = Math . max ( maxRadius , cmpObstruction . GetUnitRadius ( ) ) ;
var cmpUnitMotion = Engine . QueryInterface ( ent , IID _UnitMotion ) ;
if ( cmpUnitMotion )
minSpeed = Math . min ( minSpeed , cmpUnitMotion . GetWalkSpeed ( ) ) ;
}
2014-01-05 18:13:22 +01:00
minSpeed *= this . GetSpeedMultiplier ( ) ;
2010-09-03 11:55:14 +02:00
var cmpUnitMotion = Engine . QueryInterface ( this . entity , IID _UnitMotion ) ;
cmpUnitMotion . SetUnitRadius ( maxRadius ) ;
cmpUnitMotion . SetSpeed ( minSpeed ) ;
// TODO: we also need to do something about PassabilityClass, CostClass
} ;
2014-01-02 21:04:50 +01:00
Formation . prototype . ShapeUpdate = function ( )
2010-10-02 21:40:30 +02:00
{
2014-01-02 21:04:50 +01:00
// Check the distance to twin formations, and merge if when
// the formations could collide
for ( var i = this . twinFormations . length - 1 ; i >= 0 ; -- i )
{
// only do the check on one side
if ( this . twinFormations [ i ] <= this . entity )
continue ;
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
var cmpOtherPosition = Engine . QueryInterface ( this . twinFormations [ i ] , IID _Position ) ;
var cmpOtherFormation = Engine . QueryInterface ( this . twinFormations [ i ] , IID _Formation ) ;
if ( ! cmpPosition || ! cmpOtherPosition || ! cmpOtherFormation )
continue ;
var thisPosition = cmpPosition . GetPosition2D ( ) ;
var otherPosition = cmpOtherPosition . GetPosition2D ( ) ;
var dx = thisPosition . x - otherPosition . x ;
var dy = thisPosition . y - otherPosition . y ;
var dist = Math . sqrt ( dx * dx + dy * dy ) ;
var thisSize = this . GetSize ( ) ;
var otherSize = cmpOtherFormation . GetSize ( ) ;
var minDist = Math . max ( thisSize . width / 2 , thisSize . depth / 2 ) +
Math . max ( otherSize . width / 2 , otherSize . depth / 2 ) +
this . formationSeparation ;
if ( minDist < dist )
continue ;
// merge the members from the twin formation into this one
// twin formations should always have exactly the same orders
this . AddMembers ( cmpOtherFormation . members ) ;
Engine . DestroyEntity ( this . twinFormations [ i ] ) ;
this . twinFormations . splice ( i , 1 ) ;
}
2010-10-02 21:40:30 +02:00
// Switch between column and box if necessary
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
var walkingDistance = cmpUnitAI . ComputeWalkingDistance ( ) ;
2013-12-15 10:51:13 +01:00
var columnar = walkingDistance > g _ColumnDistanceThreshold ;
2010-10-02 21:40:30 +02:00
if ( columnar != this . columnar )
2013-12-15 10:51:13 +01:00
{
this . offsets = undefined ;
this . columnar = columnar ;
2012-12-02 02:52:27 +01:00
this . MoveMembersIntoFormation ( false , true ) ;
2010-10-15 21:25:17 +02:00
// (disable moveCenter so we can't get stuck in a loop of switching
// shape causing center to change causing shape to switch back)
2013-12-15 10:51:13 +01:00
}
2010-10-02 21:40:30 +02:00
} ;
2010-09-03 11:55:14 +02:00
Formation . prototype . OnGlobalOwnershipChanged = function ( msg )
{
// When an entity is captured or destroyed, it should no longer be
// controlled by this formation
if ( this . members . indexOf ( msg . entity ) != - 1 )
this . RemoveMembers ( [ msg . entity ] ) ;
} ;
2011-05-02 17:03:01 +02:00
Formation . prototype . OnGlobalEntityRenamed = function ( msg )
{
if ( this . members . indexOf ( msg . entity ) != - 1 )
{
2013-12-11 16:17:43 +01:00
this . offsets = undefined ;
2012-08-28 01:14:10 +02:00
var cmpNewUnitAI = Engine . QueryInterface ( msg . newentity , IID _UnitAI ) ;
if ( cmpNewUnitAI )
this . members [ this . members . indexOf ( msg . entity ) ] = msg . newentity ;
2011-05-02 17:03:01 +02:00
var cmpOldUnitAI = Engine . QueryInterface ( msg . entity , IID _UnitAI ) ;
cmpOldUnitAI . SetFormationController ( INVALID _ENTITY ) ;
2012-08-28 01:14:10 +02:00
if ( cmpNewUnitAI )
cmpNewUnitAI . SetFormationController ( this . entity ) ;
2012-12-01 01:34:03 +01:00
// Because the renamed entity might have different characteristics,
// (e.g. packed vs. unpacked siege), we need to recompute motion parameters
this . ComputeMotionParameters ( ) ;
2011-05-02 17:03:01 +02:00
}
}
2014-01-02 21:04:50 +01:00
Formation . prototype . RegisterTwinFormation = function ( entity )
{
var cmpFormation = Engine . QueryInterface ( entity , IID _Formation ) ;
if ( ! cmpFormation )
return ;
this . twinFormations . push ( entity ) ;
cmpFormation . twinFormations . push ( this . entity ) ;
} ;
Formation . prototype . DeleteTwinFormations = function ( )
{
for each ( var ent in this . twinFormations )
{
var cmpFormation = Engine . QueryInterface ( ent , IID _Formation ) ;
if ( cmpFormation )
cmpFormation . twinFormations . splice ( cmpFormation . twinFormations . indexOf ( this . entity ) , 1 ) ;
}
this . twinFormations = [ ] ;
} ;
2014-01-07 14:33:08 +01:00
Formation . prototype . LoadFormation = function ( newTemplate )
2011-05-01 22:40:53 +02:00
{
2014-01-07 14:33:08 +01:00
// get the old formation info
var members = this . members . slice ( ) ;
var cmpThisUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
var orders = cmpThisUnitAI . GetOrders ( ) . slice ( ) ;
2014-01-05 11:09:42 +01:00
this . Disband ( ) ;
2014-01-07 14:33:08 +01:00
var newFormation = Engine . AddEntity ( newTemplate ) ;
// apply the info from the old formation to the new one
2014-01-05 11:09:42 +01:00
var cmpFormation = Engine . QueryInterface ( newFormation , IID _Formation ) ;
var cmpNewUnitAI = Engine . QueryInterface ( newFormation , IID _UnitAI ) ;
2014-01-07 14:33:08 +01:00
cmpFormation . SetMembers ( members ) ;
2014-01-05 18:13:22 +01:00
if ( orders . length )
cmpNewUnitAI . AddOrders ( orders ) ;
else
cmpNewUnitAI . MoveIntoFormation ( ) ;
2011-05-01 22:40:53 +02:00
} ;
2010-09-03 11:55:14 +02:00
Engine . RegisterComponentType ( IID _Formation , "Formation" , Formation ) ;