2010-04-19 21:47:23 +02:00
var g _ProgressInterval = 1000 ;
2010-10-16 23:53:50 +02:00
const MAX _QUEUE _SIZE = 16 ;
2010-04-19 21:47:23 +02:00
function TrainingQueue ( ) { }
TrainingQueue . prototype . Schema =
2010-04-23 18:09:03 +02:00
"<a:help>Allows the building to train new units.</a:help>" +
"<a:example>" +
2010-07-03 03:23:23 +02:00
"<Entities datatype='tokens'>" +
2010-04-23 18:09:03 +02:00
"\n units/{civ}_support_female_citizen\n units/{civ}_support_trader\n units/celt_infantry_spearman_b\n " +
"</Entities>" +
"</a:example>" +
"<element name='Entities' a:help='Space-separated list of entity template names that this building can train. The special string \"{civ}\" will be automatically replaced by the building's four-character civ code'>" +
2010-07-03 03:23:23 +02:00
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
2010-04-19 21:47:23 +02:00
"<text/>" +
"</element>" ;
TrainingQueue . prototype . Init = function ( )
{
2010-08-21 22:43:55 +02:00
this . nextID = 1 ;
2010-04-19 21:47:23 +02:00
this . queue = [ ] ;
// Queue items are:
// {
// "id": 1,
2010-08-21 22:43:55 +02:00
// "player": 1, // who paid for this batch; we need this to cope with refunds cleanly
2010-04-19 21:47:23 +02:00
// "template": "units/example",
// "count": 10,
2011-08-24 00:43:34 +02:00
// "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total
// "population": 1, // population per unit, multiply by count to get total
2010-08-21 22:43:55 +02:00
// "trainingStarted": false, // true iff we have reserved population
2010-04-19 21:47:23 +02:00
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
this . timer = undefined ; // g_ProgressInterval msec timer, active while the queue is non-empty
2011-08-24 00:43:34 +02:00
this . entityCache = [ ] ;
this . spawnNotified = false ;
2010-04-19 21:47:23 +02:00
} ;
2011-08-24 00:43:34 +02:00
/ *
* Returns list of entities that can be trained by this building .
* /
2010-04-19 21:47:23 +02:00
TrainingQueue . prototype . GetEntitiesList = function ( )
{
2010-07-03 03:23:23 +02:00
var string = this . template . Entities . _string ;
2010-04-19 21:47:23 +02:00
// Replace the "{civ}" codes with this entity's civ ID
var cmpIdentity = Engine . QueryInterface ( this . entity , IID _Identity ) ;
if ( cmpIdentity )
string = string . replace ( /\{civ\}/g , cmpIdentity . GetCiv ( ) ) ;
return string . split ( /\s+/ ) ;
} ;
2011-08-24 00:43:34 +02:00
/ *
* Adds a new batch of identical units to the training queue .
* /
2011-02-05 21:35:34 +01:00
TrainingQueue . prototype . AddBatch = function ( templateName , count , metadata )
2010-04-19 21:47:23 +02:00
{
// TODO: there should probably be a limit on the number of queued batches
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits)
2010-10-16 23:53:50 +02:00
var cmpPlayer = QueryOwnerInterface ( this . entity , IID _Player ) ;
2010-04-19 21:47:23 +02:00
2010-10-16 23:53:50 +02:00
if ( this . queue . length < MAX _QUEUE _SIZE )
{
// Find the template data so we can determine the build costs
var cmpTempMan = Engine . QueryInterface ( SYSTEM _ENTITY , IID _TemplateManager ) ;
var template = cmpTempMan . GetTemplate ( templateName ) ;
if ( ! template )
return ;
2010-08-13 15:14:59 +02:00
2010-10-16 23:53:50 +02:00
// Apply a time discount to larger batches.
// TODO: work out what equation we should use here.
var timeMult = Math . pow ( count , 0.7 ) ;
2010-04-19 21:47:23 +02:00
2010-10-16 23:53:50 +02:00
var time = timeMult * template . Cost . BuildTime ;
2010-08-21 22:43:55 +02:00
2011-08-24 00:43:34 +02:00
var totalCosts = { } ;
2010-10-16 23:53:50 +02:00
for each ( var r in [ "food" , "wood" , "stone" , "metal" ] )
2011-08-24 00:43:34 +02:00
totalCosts [ r ] = Math . floor ( count * template . Cost . Resources [ r ] ) ;
2010-04-19 21:47:23 +02:00
2011-08-24 00:43:34 +02:00
var population = template . Cost . Population ;
2010-10-16 23:53:50 +02:00
2010-08-14 00:02:27 +02:00
// TrySubtractResources should report error to player (they ran out of resources)
2011-08-24 00:43:34 +02:00
if ( ! cmpPlayer . TrySubtractResources ( totalCosts ) )
2010-10-16 23:53:50 +02:00
return ;
this . queue . push ( {
"id" : this . nextID ++ ,
"player" : cmpPlayer . GetPlayerID ( ) ,
"template" : templateName ,
"count" : count ,
2011-02-05 21:35:34 +01:00
"metadata" : metadata ,
2011-08-24 00:43:34 +02:00
"resources" : template . Cost . Resources ,
2010-10-16 23:53:50 +02:00
"population" : population ,
"trainingStarted" : false ,
"timeTotal" : time * 1000 ,
"timeRemaining" : time * 1000 ,
} ) ;
2011-02-05 21:35:34 +01:00
Engine . PostMessage ( this . entity , MT _TrainingQueueChanged , { } ) ;
2010-10-16 23:53:50 +02:00
// If this is the first item in the queue, start the timer
if ( ! this . timer )
{
var cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
this . timer = cmpTimer . SetTimeout ( this . entity , IID _TrainingQueue , "ProgressTimeout" , g _ProgressInterval , { } ) ;
}
2010-04-19 21:47:23 +02:00
}
2010-10-16 23:53:50 +02:00
else
2010-04-19 21:47:23 +02:00
{
2010-10-16 23:53:50 +02:00
var notification = { "player" : cmpPlayer . GetPlayerID ( ) , "message" : "The training queue is full." } ;
var cmpGUIInterface = Engine . QueryInterface ( SYSTEM _ENTITY , IID _GuiInterface ) ;
cmpGUIInterface . PushNotification ( notification ) ;
2010-04-19 21:47:23 +02:00
}
} ;
2011-08-24 00:43:34 +02:00
/ *
* Removes an existing batch of units from the training queue .
* Refunds resource costs and population reservations .
* /
2010-08-21 22:43:55 +02:00
TrainingQueue . prototype . RemoveBatch = function ( id )
2010-04-19 21:47:23 +02:00
{
2011-08-24 00:43:34 +02:00
// Destroy any cached entities (those which didn't spawn for some reason)
for ( var i = 0 ; i < this . entityCache . length ; ++ i )
{
Engine . DestroyEntity ( this . entityCache [ i ] ) ;
}
this . entityCache = [ ] ;
2010-04-19 21:47:23 +02:00
for ( var i = 0 ; i < this . queue . length ; ++ i )
{
var item = this . queue [ i ] ;
if ( item . id != id )
continue ;
// Now we've found the item to remove
2010-08-21 22:43:55 +02:00
var cmpPlayer = QueryPlayerIDInterface ( item . player , IID _Player ) ;
2010-04-19 21:47:23 +02:00
// Refund the resource cost for this batch
2011-08-24 00:43:34 +02:00
var totalCosts = { } ;
for each ( var r in [ "food" , "wood" , "stone" , "metal" ] )
totalCosts [ r ] = Math . floor ( item . count * item . resources [ r ] ) ;
cmpPlayer . AddResources ( totalCosts ) ;
2010-04-19 21:47:23 +02:00
2010-08-21 22:43:55 +02:00
// Remove reserved population slots if necessary
if ( item . trainingStarted )
2011-08-24 00:43:34 +02:00
cmpPlayer . UnReservePopulationSlots ( item . population * item . count ) ;
2010-08-21 22:43:55 +02:00
2010-04-19 21:47:23 +02:00
// Remove from the queue
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty)
this . queue . splice ( i , 1 ) ;
2011-02-05 21:35:34 +01:00
Engine . PostMessage ( this . entity , MT _TrainingQueueChanged , { } ) ;
2010-04-19 21:47:23 +02:00
return ;
}
2010-08-21 22:43:55 +02:00
} ;
2010-04-19 21:47:23 +02:00
2011-08-24 00:43:34 +02:00
/ *
* Returns basic data from all batches in the training queue .
* /
2010-04-19 21:47:23 +02:00
TrainingQueue . prototype . GetQueue = function ( )
{
var out = [ ] ;
for each ( var item in this . queue )
{
out . push ( {
"id" : item . id ,
"template" : item . template ,
"count" : item . count ,
"progress" : 1 - ( item . timeRemaining / item . timeTotal ) ,
2011-02-05 21:35:34 +01:00
"metadata" : item . metadata ,
2010-04-19 21:47:23 +02:00
} ) ;
}
return out ;
} ;
2011-08-24 00:43:34 +02:00
/ *
* Removes all existing batches from the queue .
* /
2010-08-21 22:43:55 +02:00
TrainingQueue . prototype . ResetQueue = function ( )
{
// Empty the training queue and refund all the resource costs
// to the player. (This is to avoid players having to micromanage their
// buildings' queues when they're about to be destroyed or captured.)
while ( this . queue . length )
this . RemoveBatch ( this . queue [ 0 ] . id ) ;
} ;
TrainingQueue . prototype . OnOwnershipChanged = function ( msg )
{
2010-11-30 13:27:38 +01:00
if ( msg . from != - 1 )
{
// Unset flag that previous owner's training queue may be blocked
var cmpPlayer = QueryPlayerIDInterface ( msg . from , IID _Player ) ;
if ( cmpPlayer && this . queue . length > 0 )
cmpPlayer . UnBlockTrainingQueue ( ) ;
}
2010-09-04 15:24:52 +02:00
2010-08-21 22:43:55 +02:00
// Reset the training queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
// an enemy building, and then loads of the enemy's civ's soldiers get
// created from it. Also it means we don't have to worry about
// updating the reserved pop slots.)
this . ResetQueue ( ) ;
} ;
2010-04-19 21:47:23 +02:00
TrainingQueue . prototype . OnDestroy = function ( )
{
2010-08-21 22:43:55 +02:00
// Reset the queue to refund any resources
this . ResetQueue ( ) ;
2010-04-19 21:47:23 +02:00
if ( this . timer )
{
var cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
cmpTimer . CancelTimer ( this . timer ) ;
}
} ;
2011-08-24 00:43:34 +02:00
/ *
* This function creates the entities and places them in world if possible .
* returns the number of successfully spawned entities .
* /
2011-02-05 21:35:34 +01:00
TrainingQueue . prototype . SpawnUnits = function ( templateName , count , metadata )
2010-04-19 21:47:23 +02:00
{
var cmpFootprint = Engine . QueryInterface ( this . entity , IID _Footprint ) ;
var cmpPosition = Engine . QueryInterface ( this . entity , IID _Position ) ;
var cmpOwnership = Engine . QueryInterface ( this . entity , IID _Ownership ) ;
2010-08-05 12:20:47 +02:00
var cmpRallyPoint = Engine . QueryInterface ( this . entity , IID _RallyPoint ) ;
2011-08-24 00:43:34 +02:00
var spawnedEnts = [ ] ;
if ( this . entityCache . length == 0 )
{
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed
for ( var i = 0 ; i < count ; ++ i )
{
this . entityCache . push ( Engine . AddEntity ( templateName ) ) ;
}
}
2010-09-03 11:55:14 +02:00
2010-04-19 21:47:23 +02:00
for ( var i = 0 ; i < count ; ++ i )
{
2011-08-24 00:43:34 +02:00
var ent = this . entityCache [ 0 ] ;
2010-04-19 21:47:23 +02:00
var pos = cmpFootprint . PickSpawnPoint ( ent ) ;
if ( pos . y < 0 )
{
2011-08-24 00:43:34 +02:00
// Fail: there wasn't any space to spawn the unit
break ;
2010-04-19 21:47:23 +02:00
}
2011-08-24 00:43:34 +02:00
else
{
// Successfully spawned
var cmpNewPosition = Engine . QueryInterface ( ent , IID _Position ) ;
cmpNewPosition . JumpTo ( pos . x , pos . z ) ;
// TODO: what direction should they face in?
2010-04-19 21:47:23 +02:00
2011-08-24 00:43:34 +02:00
var cmpNewOwnership = Engine . QueryInterface ( ent , IID _Ownership ) ;
cmpNewOwnership . SetOwner ( cmpOwnership . GetOwner ( ) ) ;
var cmpPlayerStatisticsTracker = QueryOwnerInterface ( this . entity , IID _StatisticsTracker ) ;
cmpPlayerStatisticsTracker . IncreaseTrainedUnitsCounter ( ) ;
2010-08-10 03:25:24 +02:00
2011-08-24 00:43:34 +02:00
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects)
if ( spawnedEnts . length == 0 )
PlaySound ( "trained" , ent ) ;
this . entityCache . shift ( ) ;
spawnedEnts . push ( ent ) ;
}
2010-04-19 21:47:23 +02:00
}
2010-09-03 11:55:14 +02:00
2011-08-24 00:43:34 +02:00
if ( spawnedEnts . length > 0 )
2010-09-03 11:55:14 +02:00
{
2011-08-24 00:43:34 +02:00
// If a rally point is set, walk towards it (in formation)
if ( cmpRallyPoint )
2010-09-03 11:55:14 +02:00
{
2011-08-24 00:43:34 +02:00
var rallyPos = cmpRallyPoint . GetPosition ( ) ;
if ( rallyPos )
{
ProcessCommand ( cmpOwnership . GetOwner ( ) , {
"type" : "walk" ,
"entities" : spawnedEnts ,
"x" : rallyPos . x ,
"z" : rallyPos . z ,
"queued" : false
} ) ;
}
2010-09-03 11:55:14 +02:00
}
2011-02-05 21:35:34 +01:00
2011-08-24 00:43:34 +02:00
Engine . PostMessage ( this . entity , MT _TrainingFinished , {
"entities" : spawnedEnts ,
"owner" : cmpOwnership . GetOwner ( ) ,
"metadata" : metadata ,
} ) ;
}
return spawnedEnts . length ;
2010-04-19 21:47:23 +02:00
} ;
2011-08-24 00:43:34 +02:00
/ *
* Increments progress on the first batch in the training queue , and blocks the
* queue if population limit is reached or some units failed to spawn .
* /
2010-04-19 21:47:23 +02:00
TrainingQueue . prototype . ProgressTimeout = function ( data )
{
// Allocate the 1000msecs to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second)
var time = g _ProgressInterval ;
2010-08-21 22:43:55 +02:00
var cmpPlayer = QueryOwnerInterface ( this . entity , IID _Player ) ;
2010-04-19 21:47:23 +02:00
while ( time > 0 && this . queue . length )
{
var item = this . queue [ 0 ] ;
2010-08-21 22:43:55 +02:00
if ( ! item . trainingStarted )
{
// Batch's training hasn't started yet.
// Try to reserve the necessary population slots
2011-08-24 00:43:34 +02:00
if ( ! cmpPlayer . TryReservePopulationSlots ( item . population * item . count ) )
2010-08-21 22:43:55 +02:00
{
// No slots available - don't train this batch now
// (we'll try again on the next timeout)
2010-09-04 15:24:52 +02:00
// Set flag that training queue is blocked
cmpPlayer . BlockTrainingQueue ( ) ;
2010-08-21 22:43:55 +02:00
break ;
}
2010-09-04 15:24:52 +02:00
// Unset flag that training queue is blocked
cmpPlayer . UnBlockTrainingQueue ( ) ;
2010-08-21 22:43:55 +02:00
item . trainingStarted = true ;
}
// If we won't finish the batch now, just update its timer
2010-04-19 21:47:23 +02:00
if ( item . timeRemaining > time )
{
item . timeRemaining -= time ;
break ;
}
2011-08-24 00:43:34 +02:00
var numSpawned = this . SpawnUnits ( item . template , item . count , item . metadata ) ;
if ( numSpawned > 0 )
{
// This could be only partially finised
cmpPlayer . UnReservePopulationSlots ( item . population * numSpawned ) ;
item . count -= numSpawned ;
Engine . PostMessage ( this . entity , MT _TrainingQueueChanged , { } ) ;
}
if ( item . count == 0 )
{
// All entities spawned, this batch finished
time -= item . timeRemaining ;
this . queue . shift ( ) ;
// Unset flag that training queue is blocked
cmpPlayer . UnBlockTrainingQueue ( ) ;
this . spawnNotified = false ;
}
else
{
// Some entities failed to spawn
// Set flag that training queue is blocked
cmpPlayer . BlockTrainingQueue ( ) ;
if ( ! this . spawnNotified )
{
var cmpPlayer = QueryOwnerInterface ( this . entity , IID _Player ) ;
var notification = { "player" : cmpPlayer . GetPlayerID ( ) , "message" : "Can't find free space to spawn trained units" } ;
var cmpGUIInterface = Engine . QueryInterface ( SYSTEM _ENTITY , IID _GuiInterface ) ;
cmpGUIInterface . PushNotification ( notification ) ;
this . spawnNotified = true ;
}
break ;
}
2010-04-19 21:47:23 +02:00
}
// If the queue's empty, delete the timer, else repeat it
if ( this . queue . length == 0 )
{
this . timer = undefined ;
2010-09-04 15:24:52 +02:00
// Unset flag that training queue is blocked
// (This might happen when the player unqueues all batches)
cmpPlayer . UnBlockTrainingQueue ( ) ;
2010-04-19 21:47:23 +02:00
}
else
{
var cmpTimer = Engine . QueryInterface ( SYSTEM _ENTITY , IID _Timer ) ;
this . timer = cmpTimer . SetTimeout ( this . entity , IID _TrainingQueue , "ProgressTimeout" , g _ProgressInterval , data ) ;
}
}
Engine . RegisterComponentType ( IID _TrainingQueue , "TrainingQueue" , TrainingQueue ) ;