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-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-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-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 ;
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
2010-10-02 21:40:30 +02:00
this . members = [ ] ; // entity IDs currently belonging to this formation
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
2014-01-05 11:09:42 +01:00
this . formationName = this . template . FormationName ;
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 ] ;
} ;
2012-08-31 10:20:36 +02:00
/ * *
* Permits formation members to register that they ' ve reached their
* destination , and automatically disbands the formation if all members
* are at their final positions and no controller orders remain .
* /
Formation . prototype . SetInPosition = function ( ent )
{
if ( this . inPosition . indexOf ( ent ) != - 1 )
return ;
// Only consider automatically disbanding if there are no orders left.
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
if ( cmpUnitAI . GetOrders ( ) . length )
{
this . inPosition = [ ] ;
return ;
}
this . inPosition . push ( ent ) ;
if ( this . inPosition . length >= this . members . length )
this . Disband ( ) ;
} ;
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 ;
for each ( var ent in this . members )
{
var cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
cmpUnitAI . SetFormationController ( this . entity ) ;
2014-01-05 11:09:42 +01:00
cmpUnitAI . SetLastFormationName ( this . formationName ) ;
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 ( ) ;
2013-10-14 17:51:21 +02:00
2010-09-03 11:55:14 +02:00
} ;
/ * *
* 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 ;
}
2013-12-06 11:21:07 +01:00
var newOrientation = this . GetTargetOrientation ( avgpos ) ;
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 )
this . offsets = this . ComputeFormationOffsets ( active , positions , this . columnar ) ;
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
} ;
Formation . prototype . ComputeFormationOffsets = function ( active , positions , columnar )
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
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
2011-05-01 22:40:53 +02:00
var types = {
"Cavalry" : [ ] ,
"Hero" : [ ] ,
"Melee" : [ ] ,
"Ranged" : [ ] ,
"Support" : [ ] ,
"Unknown" : [ ]
2013-12-04 14:14:31 +01:00
} ;
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 ;
for each ( var cla in classes )
{
if ( cla in types )
{
2013-12-05 11:23:49 +01:00
types [ cla ] . 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 ;
2013-12-05 11:23:49 +01:00
var ordering = [ ] ;
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
if ( 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-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
switch ( this . formationName )
2010-10-02 21:40:30 +02:00
{
2012-05-04 01:32:10 +02:00
case "Scatter" :
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 } ) ;
2012-05-04 01:32:10 +02:00
break ;
case "Box" :
2011-05-01 22:40:53 +02:00
var root = Math . ceil ( Math . sqrt ( count ) ) ;
var left = count ;
var meleeleft = types [ "Melee" ] . length ;
for ( var sq = Math . floor ( root / 2 ) ; sq >= 0 ; -- sq )
{
var width = sq * 2 + 1 ;
var stodo ;
if ( sq == 0 )
{
stodo = left ;
}
else
{
if ( meleeleft >= width * width - ( width - 2 ) * ( width - 2 ) ) // form a complete box
{
stodo = width * width - ( width - 2 ) * ( width - 2 ) ;
meleeleft -= stodo ;
}
else // compact
2012-05-04 01:32:10 +02:00
{
2011-05-01 22:40:53 +02:00
stodo = Math . max ( 0 , left - ( width - 2 ) * ( width - 2 ) ) ;
2012-05-04 01:32:10 +02:00
}
2011-05-01 22:40:53 +02:00
}
for ( var r = - sq ; r <= sq && stodo ; ++ r )
{
for ( var c = - sq ; c <= sq && stodo ; ++ c )
{
if ( Math . abs ( r ) == sq || Math . abs ( c ) == sq )
{
2013-12-26 13:24:52 +01:00
var x = c * separation . width ;
var z = - r * separation . depth ;
2011-05-01 22:40:53 +02:00
offsets . push ( { "x" : x , "z" : z } ) ;
stodo -- ;
left -- ;
}
}
}
}
2012-05-04 01:32:10 +02:00
break ;
case "Battle Line" :
2013-12-05 11:23:49 +01:00
ordering . push ( "FillFromTheSides" ) ;
2012-05-04 01:32:10 +02:00
break ;
default :
break ;
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
if ( shape != "special" )
2011-05-01 22:40:53 +02:00
{
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
}
2011-05-01 22:40:53 +02:00
offsets . push ( { "x" : x , "z" : z } ) ;
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 ;
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
for each ( var order in ordering )
{
if ( order == "FillFromTheSides" )
offsets . sort ( function ( o1 , o2 ) { return Math . abs ( o1 . x ) < Math . abs ( o2 . x ) ; } ) ;
else if ( order == "FillFromTheCenter" )
offsets . sort ( function ( o1 , o2 ) { return Math . abs ( o1 . x ) > Math . abs ( o2 . x ) ; } ) ;
else if ( order == "FillFromTheFront" )
offsets . sort ( function ( o1 , o2 ) { return o1 . z < o2 . z ; } ) ;
}
// 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 ) ;
2013-12-04 14:14:31 +01:00
for each ( var t in types )
2010-09-03 11:55:14 +02:00
{
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
}
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 = [ ] ;
2013-12-06 11:21:07 +01:00
var { sin , cos } = this . GetTargetOrientation ( pos ) ;
// calculate the world positions
for each ( var o in offsets )
offsetPositions . push ( {
"x" : pos . x + o . z * sin + o . x * cos ,
"z" : pos . z + o . z * cos - o . x * sin
} ) ;
return offsetPositions ;
} ;
/ * *
* calculate the estimated rotation of the formation
* based on the first unitAI target position
* Return the sine and cosine of the angle
* /
Formation . prototype . GetTargetOrientation = function ( pos )
{
2013-12-04 14:14:31 +01:00
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
var targetPos = cmpUnitAI . GetTargetPositions ( ) ;
var sin = 1 ;
var cos = 0 ;
if ( targetPos . length )
{
var dx = targetPos [ 0 ] . x - pos . x ;
2013-12-05 11:23:49 +01:00
var dz = targetPos [ 0 ] . z - pos . z ;
2013-12-04 14:14:31 +01:00
if ( dx || dz )
2011-05-01 22:40:53 +02:00
{
2013-12-04 14:14:31 +01:00
var dist = Math . sqrt ( dx * dx + dz * dz ) ;
2013-12-06 11:21:07 +01:00
cos = dz / dist ;
sin = dx / dist ;
2011-05-01 22:40:53 +02:00
}
2010-09-03 11:55:14 +02:00
}
2013-12-06 11:21:07 +01:00
return { "sin" : sin , "cos" : cos } ;
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 = [ ] ;
} ;
2011-05-01 22:40:53 +02:00
Formation . prototype . LoadFormation = function ( formationName )
{
2014-01-05 18:13:22 +01:00
if ( formationName == this . formationName )
{
var cmpUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
cmpUnitAI . MoveIntoFormation ( ) ;
return ;
}
2014-01-05 11:09:42 +01:00
var members = this . members ;
this . Disband ( ) ;
var newFormation = Engine . AddEntity ( "formations/" + formationName . replace ( /\s+/g , "_" ) . toLowerCase ( ) ) ;
var cmpFormation = Engine . QueryInterface ( newFormation , IID _Formation ) ;
cmpFormation . SetMembers ( members ) ;
var cmpThisUnitAI = Engine . QueryInterface ( this . entity , IID _UnitAI ) ;
var cmpNewUnitAI = Engine . QueryInterface ( newFormation , IID _UnitAI ) ;
2014-01-05 18:13:22 +01:00
var orders = cmpThisUnitAI . GetOrders ( ) ;
if ( orders . length )
cmpNewUnitAI . AddOrders ( orders ) ;
else
cmpNewUnitAI . MoveIntoFormation ( ) ;
2014-01-05 11:09:42 +01:00
2011-05-01 22:40:53 +02:00
} ;
2010-09-03 11:55:14 +02:00
Engine . RegisterComponentType ( IID _Formation , "Formation" , Formation ) ;