# Population limits.

Fix #531 (Enforce Population Limit), based on patch from evans.
Empty training queue and refund resources when buildings are destroyed
or captured.
Specify Cost defaults in XML instead of JS, to simplify some code.

This was SVN commit r8014.
This commit is contained in:
Ykkrosh 2010-08-21 20:43:55 +00:00
parent d2f7973c29
commit b38e032c7e
7 changed files with 143 additions and 55 deletions

View File

@ -13,21 +13,15 @@ Cost.prototype.Schema =
"<metal>25</metal>" +
"</Resources>" +
"</a:example>" +
"<optional>" +
"<element name='Population' a:help='Population cost'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='PopulationBonus' a:help='Population cap increase while this entity exists'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='BuildTime' a:help='Time taken to construct/train this unit (in seconds)'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"</optional>" +
"<element name='Population' a:help='Population cost'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<element name='PopulationBonus' a:help='Population cap increase while this entity exists'>" +
"<data type='nonNegativeInteger'/>" +
"</element>" +
"<element name='BuildTime' a:help='Time taken to construct/train this unit (in seconds)'>" +
"<ref name='positiveDecimal'/>" +
"</element>" +
"<element name='Resources' a:help='Resource costs to construct/train this unit'>" +
"<interleave>" +
"<element name='food'><data type='nonNegativeInteger'/></element>" +
@ -37,27 +31,19 @@ Cost.prototype.Schema =
"</interleave>" +
"</element>";
Cost.prototype.Init = function()
{
};
Cost.prototype.GetPopCost = function()
{
if ("Population" in this.template)
return +this.template.Population;
return 0;
return +this.template.Population;
};
Cost.prototype.GetPopBonus = function()
{
if ("PopulationBonus" in this.template)
return +this.template.PopulationBonus;
return 0;
return +this.template.PopulationBonus;
};
Cost.prototype.GetBuildTime = function()
{
return +(this.template.BuildTime || 1);
return +this.template.BuildTime;
}
Cost.prototype.GetResourceCosts = function()

View File

@ -9,8 +9,9 @@ Player.prototype.Init = function()
this.name = "Unknown";
this.civ = "gaia";
this.colour = { "r": 0.0, "g": 0.0, "b": 0.0, "a": 1.0 };
this.popCount = 0;
this.popLimit = 50;
this.popUsed = 0; // population of units owned by this player
this.popReserved = 0; // population of units currently being trained
this.popLimit = 50; // maximum population
this.resourceCount = {
"food": 2000,
"wood": 1500,
@ -24,6 +25,11 @@ Player.prototype.SetPlayerID = function(id)
this.playerID = id;
};
Player.prototype.GetPlayerID = function()
{
return this.playerID;
};
Player.prototype.SetName = function(name)
{
this.name = name;
@ -54,9 +60,23 @@ Player.prototype.GetColour = function()
return this.colour;
};
Player.prototype.TryReservePopulationSlots = function(num)
{
if (num > this.GetPopulationLimit() - this.GetPopulationCount())
return false;
this.popReserved += num;
return true;
};
Player.prototype.UnReservePopulationSlots = function(num)
{
this.popReserved -= num;
};
Player.prototype.GetPopulationCount = function()
{
return this.popCount;
return this.popUsed + this.popReserved;
};
Player.prototype.GetPopulationLimit = function()
@ -110,6 +130,8 @@ Player.prototype.TrySubtractResources = function(amounts)
return true;
};
// Keep track of population effects of all entities that
// become owned or unowned by this player
Player.prototype.OnGlobalOwnershipChanged = function(msg)
{
if (msg.from == this.playerID)
@ -117,7 +139,7 @@ Player.prototype.OnGlobalOwnershipChanged = function(msg)
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
this.popCount -= cost.GetPopCost();
this.popUsed -= cost.GetPopCost();
this.popLimit -= cost.GetPopBonus();
}
}
@ -127,7 +149,7 @@ Player.prototype.OnGlobalOwnershipChanged = function(msg)
var cost = Engine.QueryInterface(msg.entity, IID_Cost);
if (cost)
{
this.popCount += cost.GetPopCost();
this.popUsed += cost.GetPopCost();
this.popLimit += cost.GetPopBonus();
}
}

View File

@ -18,15 +18,18 @@ TrainingQueue.prototype.Schema =
TrainingQueue.prototype.Init = function()
{
this.nextID = 1;
this.nextID = 1;
this.queue = [];
// Queue items are:
// {
// "id": 1,
// "player": 1, // who paid for this batch; we need this to cope with refunds cleanly
// "template": "units/example",
// "count": 10,
// "resources": { "wood": 100, ... },
// "population": 10,
// "trainingStarted": false, // true iff we have reserved population
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
@ -46,7 +49,7 @@ TrainingQueue.prototype.GetEntitiesList = function()
return string.split(/\s+/);
};
TrainingQueue.prototype.AddBatch = function(player, templateName, count)
TrainingQueue.prototype.AddBatch = function(templateName, count)
{
// 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
@ -64,20 +67,15 @@ TrainingQueue.prototype.AddBatch = function(player, templateName, count)
// TODO: work out what equation we should use here.
var timeMult = Math.pow(count, 0.7);
var time = timeMult * (template.Cost.BuildTime || 1);
var time = timeMult * template.Cost.BuildTime;
var costs = {};
for each (var r in ["food", "wood", "stone", "metal"])
{
if (template.Cost.Resources[r])
costs[r] = Math.floor(costMult * template.Cost.Resources[r]);
else
costs[r] = 0;
}
costs[r] = Math.floor(costMult * template.Cost.Resources[r]);
// Find the player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
var population = template.Cost.Population * count;
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (!cmpPlayer.TrySubtractResources(costs))
{
@ -87,9 +85,12 @@ TrainingQueue.prototype.AddBatch = function(player, templateName, count)
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"template": templateName,
"count": count,
"resources": costs,
"population": population,
"trainingStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
@ -102,7 +103,7 @@ TrainingQueue.prototype.AddBatch = function(player, templateName, count)
}
};
TrainingQueue.prototype.RemoveBatch = function(player, id)
TrainingQueue.prototype.RemoveBatch = function(id)
{
for (var i = 0; i < this.queue.length; ++i)
{
@ -112,20 +113,21 @@ TrainingQueue.prototype.RemoveBatch = function(player, id)
// Now we've found the item to remove
// Find the player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
var cmpPlayer = QueryPlayerIDInterface(item.player, IID_Player);
// Refund the resource cost for this batch
cmpPlayer.AddResources(item.resources);
// Remove reserved population slots if necessary
if (item.trainingStarted)
cmpPlayer.UnReservePopulationSlots(item.population);
// 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);
return;
}
}
};
TrainingQueue.prototype.GetQueue = function()
{
@ -142,11 +144,30 @@ TrainingQueue.prototype.GetQueue = function()
return out;
};
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)
{
// 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();
};
TrainingQueue.prototype.OnDestroy = function()
{
// If the building is destroyed while it's got a large training queue,
// you lose all the resources invested in that queue. That'll teach you
// to be so reckless with your buildings.
// Reset the queue to refund any resources
this.ResetQueue();
if (this.timer)
{
@ -205,10 +226,26 @@ TrainingQueue.prototype.ProgressTimeout = function(data)
// 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;
var cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
while (time > 0 && this.queue.length)
{
var item = this.queue[0];
if (!item.trainingStarted)
{
// Batch's training hasn't started yet.
// Try to reserve the necessary population slots
if (!cmpPlayer.TryReservePopulationSlots(item.population))
{
// No slots available - don't train this batch now
// (we'll try again on the next timeout)
break;
}
item.trainingStarted = true;
}
// If we won't finish the batch now, just update its timer
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
@ -217,6 +254,7 @@ TrainingQueue.prototype.ProgressTimeout = function(data)
// This item is finished now
time -= item.timeRemaining;
cmpPlayer.UnReservePopulationSlots(item.population);
this.SpawnUnits(item.template, item.count);
this.queue.shift();
}

View File

@ -51,13 +51,13 @@ function ProcessCommand(player, cmd)
case "train":
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.AddBatch(player, cmd.template, +cmd.count);
queue.AddBatch(cmd.template, +cmd.count);
break;
case "stop-train":
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.RemoveBatch(player, cmd.id);
queue.RemoveBatch(cmd.id);
break;
case "construct":

View File

@ -0,0 +1,38 @@
/**
* Similar to Engine.QueryInterface but applies to the player entity
* that owns the given entity.
* iid is typically IID_Player.
*/
function QueryOwnerInterface(ent, iid)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!cmpOwnership)
return null;
var playerEnt = cmpPlayerMan.GetPlayerByID(cmpOwnership.GetOwner());
if (!playerEnt)
return null;
return Engine.QueryInterface(playerEnt, iid);
}
/**
* Similar to Engine.QueryInterface but applies to the player entity
* with the given ID number.
* iid is typically IID_Player.
*/
function QueryPlayerIDInterface(id, iid)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerEnt = cmpPlayerMan.GetPlayerByID(id);
if (!playerEnt)
return null;
return Engine.QueryInterface(playerEnt, iid);
}
Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface);
Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface);

View File

@ -11,6 +11,8 @@
<Type>structure</Type>
</Minimap>
<Cost>
<Population>0</Population>
<PopulationBonus>0</PopulationBonus>
<BuildTime>10</BuildTime>
<Resources>
<food>0</food>

View File

@ -9,6 +9,8 @@
<UnitAI/>
<Cost>
<Population>1</Population>
<PopulationBonus>0</PopulationBonus>
<BuildTime>1</BuildTime>
<Resources>
<food>0</food>
<wood>0</wood>