forked from 0ad/0ad
# 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:
parent
d2f7973c29
commit
b38e032c7e
@ -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='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;
|
||||
};
|
||||
|
||||
Cost.prototype.GetPopBonus = function()
|
||||
{
|
||||
if ("PopulationBonus" in this.template)
|
||||
return +this.template.PopulationBonus;
|
||||
return 0;
|
||||
};
|
||||
|
||||
Cost.prototype.GetBuildTime = function()
|
||||
{
|
||||
return +(this.template.BuildTime || 1);
|
||||
return +this.template.BuildTime;
|
||||
}
|
||||
|
||||
Cost.prototype.GetResourceCosts = function()
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,12 @@ TrainingQueue.prototype.Init = function()
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
@ -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":
|
||||
|
38
binaries/data/mods/public/simulation/helpers/Player.js
Normal file
38
binaries/data/mods/public/simulation/helpers/Player.js
Normal 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);
|
@ -11,6 +11,8 @@
|
||||
<Type>structure</Type>
|
||||
</Minimap>
|
||||
<Cost>
|
||||
<Population>0</Population>
|
||||
<PopulationBonus>0</PopulationBonus>
|
||||
<BuildTime>10</BuildTime>
|
||||
<Resources>
|
||||
<food>0</food>
|
||||
|
@ -9,6 +9,8 @@
|
||||
<UnitAI/>
|
||||
<Cost>
|
||||
<Population>1</Population>
|
||||
<PopulationBonus>0</PopulationBonus>
|
||||
<BuildTime>1</BuildTime>
|
||||
<Resources>
|
||||
<food>0</food>
|
||||
<wood>0</wood>
|
||||
|
Loading…
Reference in New Issue
Block a user