Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 22374)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 22375)
@@ -1,844 +1,833 @@
var g_ProgressInterval = 1000;
const MAX_QUEUE_SIZE = 16;
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"Allows the building to train new units and research technologies" +
"" +
"0.7" +
"" +
"\n units/{civ}_support_female_citizen\n units/{native}_support_trader\n units/athen_infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
"";
ProductionQueue.prototype.Init = function()
{
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
// "unitTemplate": "units/example",
// "count": 10,
// "neededSlots": 3, // number of population slots missing for production to begin
// "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total
// "population": 1, // population per unit, multiply by count to get total
// "productionStarted": false, // true iff we have reserved population
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
//
// {
// "id": 1,
// "player": 1, // who paid for this research; we need this to cope with refunds cleanly
// "technologyTemplate": "example_tech",
// "resources": { "wood": 100, ... }, // resources needed for research
// "productionStarted": false, // true iff production has started
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty
this.paused = false;
this.entityCache = [];
this.spawnNotified = false;
};
/*
* Returns list of entities that can be trained by this building.
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return this.entitiesList;
};
ProductionQueue.prototype.CalculateEntitiesList = function()
{
this.entitiesList = [];
if (!this.template.Entities)
return;
let string = this.template.Entities._string;
if (!string)
return;
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (cmpIdentity)
string = string.replace(/\{native\}/g, cmpIdentity.GetCiv());
let entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/);
// filter out disabled and invalid entities
let disabledEntities = cmpPlayer.GetDisabledTemplates();
entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent));
// check if some templates need to show their advanced or elite version
let upgradeTemplate = function(templateName)
{
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
let requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
for (let templateName of entitiesList)
this.entitiesList.push(upgradeTemplate(templateName));
for (let item of this.queue)
if (item.unitTemplate)
item.unitTemplate = upgradeTemplate(item.unitTemplate);
};
/*
* Returns list of technologies that can be researched by this building.
*/
ProductionQueue.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
var string = this.template.Technologies._string;
if (!string)
return [];
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
var cmpPlayer = QueryOwnerInterface(this.entity);
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpPlayer || !cmpIdentity)
return [];
var techs = string.split(/\s+/);
// Replace the civ specific technologies
for (let i = 0; i < techs.length; ++i)
{
let tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
let civTech = tech.replace("{civ}", cmpPlayer.GetCiv());
techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic");
}
// Remove any technologies that can't be researched by this civ
techs = techs.filter(tech =>
cmpTechnologyManager.CheckTechnologyRequirements(
DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()),
true));
var techList = [];
var superseded = {}; // Stores the tech which supersedes the key
var disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons
// Also store what a technology is superceded by in the superceded object {"tech1":"techWhichSupercedesTech1", ...}
for (var i in techs)
{
var tech = techs[i];
if (disabledTechnologies && disabledTechnologies[tech])
continue;
let template = TechnologyTemplates.Get(tech);
if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
techList.push(tech);
else
superseded[template.supersedes] = tech;
}
// Now make researched/in progress techs invisible
for (var i in techList)
{
var tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech];
techList[i] = tech;
}
var ret = [];
// This inserts the techs into the correct positions to line up the technology pairs
for (var i = 0; i < techList.length; i++)
{
var tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
let template = TechnologyTemplates.Get(tech);
if (template.top)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
else
ret[i] = tech;
}
return ret;
};
ProductionQueue.prototype.GetTechCostMultiplier = function()
{
let techCostMultiplier = {};
for (let res in this.template.TechCostMultiplier)
techCostMultiplier[res] = ApplyValueModificationsToEntity("ProductionQueue/TechCostMultiplier/"+res, +this.template.TechCostMultiplier[res], this.entity);
return techCostMultiplier;
};
ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
let template = TechnologyTemplates.Get(tech);
if (template.top)
return cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top) ||
cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom);
return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech);
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
*/
ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata)
{
// 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)
let cmpPlayer = QueryOwnerInterface(this.entity);
if (this.queue.length < MAX_QUEUE_SIZE)
{
if (type == "unit")
{
if (!Number.isInteger(count) || count <= 0)
{
error("Invalid batch count " + count);
return;
}
// Find the template data so we can determine the build costs
- var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
- var template = cmpTemplateManager.GetTemplate(templateName);
+ let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
+ let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return;
- if (template.Promotion)
+ if (template.Promotion && ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template) == 0)
{
- var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template);
-
- if (requiredXp == 0)
- {
- this.AddBatch(template.Promotion.Entity, type, count, metadata);
- return;
- }
+ this.AddBatch(template.Promotion.Entity, type, count, metadata);
+ return;
}
// Apply a time discount to larger batches.
- var timeMult = this.GetBatchTime(count);
+ let timeMult = this.GetBatchTime(count);
// We need the costs after tech modifications
// Obviously we don't have the entities yet, so we must use template data
- var costs = {};
- var totalCosts = {};
- var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template);
- var time = timeMult * buildTime;
+ let costs = {};
+ let totalCosts = {};
+ let buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template);
+ let time = timeMult * buildTime;
for (let res in template.Cost.Resources)
{
costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +template.Cost.Resources[res], cmpPlayer.GetPlayerID(), template);
totalCosts[res] = Math.floor(count * costs[res]);
}
- var population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, cmpPlayer.GetPlayerID(), template);
-
// TrySubtractResources should report error to player (they ran out of resources)
if (!cmpPlayer.TrySubtractResources(totalCosts))
return;
// Update entity count in the EntityLimits component
if (template.TrainingRestrictions)
{
- var unitCategory = template.TrainingRestrictions.Category;
- var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
+ let unitCategory = template.TrainingRestrictions.Category;
+ let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
}
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"resources": costs,
- "population": population,
+ "population": ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, cmpPlayer.GetPlayerID(), template),
"productionStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
- var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("TrainingQueued", { "playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity });
}
else if (type == "technology")
{
if (!TechnologyTemplates.Has(templateName))
return;
if (!this.GetTechnologiesList().some(tech =>
tech &&
(tech == templateName ||
tech.pair &&
(tech.top == templateName || tech.bottom == templateName))))
{
error("This entity cannot research " + templateName);
return;
}
let template = TechnologyTemplates.Get(templateName);
let techCostMultiplier = this.GetTechCostMultiplier();
let time = techCostMultiplier.time * template.researchTime * cmpPlayer.GetTimeMultiplier();
let cost = {};
for (let res in template.cost)
cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]);
// TrySubtractResources should report error to player (they ran out of resources)
if (!cmpPlayer.TrySubtractResources(cost))
return;
// Tell the technology manager that we have started researching this so that people can't research the same
// thing twice.
- var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
+ let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(templateName, this.entity);
if (this.queue.length == 0)
cmpTechnologyManager.StartedResearch(templateName, false);
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"count": 1,
"technologyTemplate": templateName,
"resources": cost,
"productionStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
- var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ResearchQueued", { "playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity });
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return;
}
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
// If this is the first item in the queue, start the timer
if (!this.timer)
{
- var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {});
}
}
else
{
- var notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true };
- var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
+ let notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true };
+ let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
};
/*
* Removes an existing batch of units from the production queue.
* Refunds resource costs and population reservations.
*/
ProductionQueue.prototype.RemoveBatch = function(id)
{
// Destroy any cached entities (those which didn't spawn for some reason)
for (let ent of this.entityCache)
Engine.DestroyEntity(ent);
this.entityCache = [];
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
var cmpPlayer = QueryPlayerIDInterface(item.player);
// Update entity count in the EntityLimits component
if (item.unitTemplate)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(item.unitTemplate);
if (template.TrainingRestrictions)
{
var unitCategory = template.TrainingRestrictions.Category;
var cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
cmpPlayerEntityLimits.ChangeCount(unitCategory, -item.count);
}
}
// Refund the resource cost for this batch
var totalCosts = {};
var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
for (let r in item.resources)
{
totalCosts[r] = Math.floor(item.count * item.resources[r]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]);
}
cmpPlayer.AddResources(totalCosts);
// Remove reserved population slots if necessary
if (item.productionStarted && item.unitTemplate)
cmpPlayer.UnReservePopulationSlots(item.population * item.count);
// Mark the research as stopped if we cancel it
if (item.technologyTemplate)
{
// item.player is used as this.entity's owner may be invalid (deletion, etc.)
var cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
cmpTechnologyManager.StoppedResearch(item.technologyTemplate, true);
}
// 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);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
return;
}
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
var out = [];
for (var item of this.queue)
{
out.push({
"id": item.id,
"unitTemplate": item.unitTemplate,
"technologyTemplate": item.technologyTemplate,
"count": item.count,
"neededSlots": item.neededSlots,
"progress": 1 - (item.timeRemaining / (item.timeTotal || 1)),
"timeRemaining": item.timeRemaining,
"metadata": item.metadata,
});
}
return out;
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
// Empty the production 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);
};
/*
* Returns batch build time.
*/
ProductionQueue.prototype.GetBatchTime = function(batchSize)
{
var cmpPlayer = QueryOwnerInterface(this.entity);
var batchTimeModifier = ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity);
// TODO: work out what equation we should use here.
return Math.pow(batchSize, batchTimeModifier) * cmpPlayer.GetTimeMultiplier();
};
ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
if (msg.from != INVALID_PLAYER)
{
// Unset flag that previous owner's training may be blocked
var cmpPlayer = QueryPlayerIDInterface(msg.from);
if (cmpPlayer && this.queue.length > 0)
cmpPlayer.UnBlockTraining();
}
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesList();
// Reset the production 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();
};
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesList();
};
ProductionQueue.prototype.OnDestroy = function()
{
// Reset the queue to refund any resources
this.ResetQueue();
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/*
* This function creates the entities and places them in world if possible
* and returns the number of successfully created entities.
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
*/
ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata)
{
- var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
- var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
- var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
+ let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
+ let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
+ let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
- var createdEnts = [];
- var spawnedEnts = [];
+ let createdEnts = [];
+ let spawnedEnts = [];
+ // We need entities to test spawning, but we don't want to waste resources,
+ // so only create them once and use as needed
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)
- {
- var ent = Engine.AddEntity(templateName);
- this.entityCache.push(ent);
-
- // Decrement entity count in the EntityLimits component
- // since it will be increased by EntityLimits.OnGlobalOwnershipChanged function,
- // i.e. we replace a 'trained' entity to an 'alive' one
- var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
- if (cmpTrainingRestrictions)
- {
- var unitCategory = cmpTrainingRestrictions.GetCategory();
- var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
- cmpPlayerEntityLimits.ChangeCount(unitCategory, -1);
- }
- }
- }
+ for (let i = 0; i < count; ++i)
+ this.entityCache.push(Engine.AddEntity(templateName));
let cmpAutoGarrison;
if (cmpRallyPoint)
{
let data = cmpRallyPoint.GetData()[0];
if (data && data.target && data.target == this.entity && data.command == "garrison")
cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
}
for (let i = 0; i < count; ++i)
{
let ent = this.entityCache[0];
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
if (cmpAutoGarrison)
{
// Temporary owner affectation needed for GarrisonHolder checks
cmpNewOwnership.SetOwnerQuiet(cmpOwnership.GetOwner());
garrisoned = cmpAutoGarrison.PerformGarrison(ent);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
if (garrisoned)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.Autogarrison(this.entity);
}
else
{
let pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (cmpPosition)
cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos));
spawnedEnts.push(ent);
}
+ // Decrement entity count in the EntityLimits component
+ // since it will be increased by EntityLimits.OnGlobalOwnershipChanged function,
+ // i.e. we replace a 'trained' entity by 'alive' one
+ // Must be done after spawn check so EntityLimits decrements only if unit spawns
+ let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
+ if (cmpTrainingRestrictions)
+ {
+ let unitCategory = cmpTrainingRestrictions.GetCategory();
+ let cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
+ cmpPlayerEntityLimits.ChangeCount(unitCategory, -1);
+ }
+
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
let cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects)
if (createdEnts.length == 0)
PlaySound("trained", ent);
this.entityCache.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length > 0 && !cmpAutoGarrison)
{
// If a rally point is set, walk towards it (in formation) using a suitable command based on where the
// rally point is placed.
if (cmpRallyPoint)
{
- var rallyPos = cmpRallyPoint.GetPositions()[0];
+ let rallyPos = cmpRallyPoint.GetPositions()[0];
if (rallyPos)
{
- var commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts);
- for (var com of commands)
+ let commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts);
+ for (let com of commands)
ProcessCommand(cmpOwnership.GetOwner(), com);
}
}
}
if (createdEnts.length > 0)
Engine.PostMessage(this.entity, MT_TrainingFinished, {
"entities": createdEnts,
"owner": cmpOwnership.GetOwner(),
"metadata": metadata,
});
return createdEnts.length;
};
/*
* Increments progress on the first batch in the production queue, and blocks the
* queue if population limit is reached or some units failed to spawn.
*/
ProductionQueue.prototype.ProgressTimeout = function(data)
{
// Check if the production is paused (eg the entity is garrisoned)
if (this.paused)
return;
// 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;
var cmpPlayer = QueryOwnerInterface(this.entity);
while (time > 0 && this.queue.length)
{
var item = this.queue[0];
if (!item.productionStarted)
{
// If the item is a unit then do population checks
if (item.unitTemplate)
{
// If something change population cost
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(item.unitTemplate);
item.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, item.player, template);
// Batch's training hasn't started yet.
// Try to reserve the necessary population slots
item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count);
if (item.neededSlots)
{
// Not enough slots available - don't train this batch now
// (we'll try again on the next timeout)
// Set flag that training is blocked
cmpPlayer.BlockTraining();
break;
}
// Unset flag that training is blocked
cmpPlayer.UnBlockTraining();
}
if (item.technologyTemplate)
{
// Mark the research as started.
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.StartedResearch(item.technologyTemplate, true);
}
item.productionStarted = true;
if (item.unitTemplate)
Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
}
// If we won't finish the batch now, just update its timer
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
// send a message for the AIs.
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
break;
}
if (item.unitTemplate)
{
var numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata);
if (numSpawned == item.count)
{
// All entities spawned, this batch finished
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
time -= item.timeRemaining;
this.queue.shift();
// Unset flag that training is blocked
cmpPlayer.UnBlockTraining();
this.spawnNotified = false;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
else
{
if (numSpawned > 0)
{
// Only partially finished
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
item.count -= numSpawned;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
// Some entities failed to spawn
// Set flag that training is blocked
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
var cmpPlayer = QueryOwnerInterface(this.entity);
var notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Can't find free space to spawn trained units"), "translateMessage": true };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
this.spawnNotified = true;
}
break;
}
}
else if (item.technologyTemplate)
{
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.ResearchTechnology(item.technologyTemplate);
let template = TechnologyTemplates.Get(item.technologyTemplate);
if (template && template.soundComplete)
{
var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
}
time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
}
// If the queue's empty, delete the timer, else repeat it
if (this.queue.length == 0)
{
this.timer = undefined;
// Unset flag that training is blocked
// (This might happen when the player unqueues all batches)
cmpPlayer.UnBlockTraining();
}
else
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, data);
}
};
ProductionQueue.prototype.PauseProduction = function()
{
this.timer = undefined;
this.paused = true;
};
ProductionQueue.prototype.UnpauseProduction = function()
{
this.paused = false;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {});
};
ProductionQueue.prototype.OnValueModification = function(msg)
{
// if the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list
if (msg.component == "Promotion")
this.CalculateEntitiesList();
};
ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
{
// if the disabled templates of the player is changed,
// update the entities list so that this is reflected there
this.CalculateEntitiesList();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
Index: ps/trunk/binaries/data/mods/public/simulation/components/TrainingRestrictions.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TrainingRestrictions.js (revision 22374)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TrainingRestrictions.js (revision 22375)
@@ -1,21 +1,25 @@
function TrainingRestrictions() {}
TrainingRestrictions.prototype.Schema =
"Specifies unit training restrictions, currently only unit category" +
"" +
"" +
"Hero" +
"" +
"" +
"" +
"" +
"";
+TrainingRestrictions.prototype.Init = function()
+{
+};
+
TrainingRestrictions.prototype.Serialize = null;
TrainingRestrictions.prototype.GetCategory = function()
{
return this.template.Category;
};
Engine.RegisterComponentType(IID_TrainingRestrictions, "TrainingRestrictions", TrainingRestrictions);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 22374)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 22375)
@@ -1,141 +1,283 @@
Resources = {
"BuildSchema": (a, b) => {}
};
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("ProductionQueue.js");
global.TechnologyTemplates = {
"Has": name => name == "phase_town_athen" || name == "phase_city_athen",
"Get": () => ({})
};
const productionQueueId = 6;
const playerId = 1;
const playerEntityID = 2;
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", {
"Entities": { "_string": "units/{civ}_cavalry_javelinist_b " +
"units/{civ}_infantry_swordsman_b " +
"units/{native}_support_female_citizen" },
"Technologies": { "_string": "gather_fishing_net " +
"phase_town_{civ} " +
"phase_city_{civ}" }
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), []);
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEntityID
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({}),
"GetPlayerID": () => playerId
});
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": () => false
});
AddMock(productionQueueId, IID_Ownership, {
"GetOwner": () => playerId
});
AddMock(productionQueueId, IID_Identity, {
"GetCiv": () => "iber"
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"]
);
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": name => name == "units/iber_support_female_citizen",
"GetTemplate": name => ({})
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({ "units/iber_infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber_cavalry_javelinist_b", "units/iber_support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "athen",
"GetDisabledTechnologies": () => ({ "gather_fishing_net": true }),
"GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }),
"GetPlayerID": () => playerId
});
cmpProductionQueue.CalculateEntitiesList();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/athen_cavalry_javelinist_b", "units/iber_support_female_citizen"]
);
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen",
"phase_city_athen"]
);
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": tech => tech == "phase_town_athen"
});
TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetPlayerID": () => playerId
});
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
+
+Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
+Engine.LoadComponentScript("interfaces/EntityLimits.js");
+Engine.LoadComponentScript("interfaces/Foundation.js");
+Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
+Engine.LoadComponentScript("interfaces/Timer.js");
+Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
+Engine.LoadComponentScript("interfaces/Trigger.js");
+Engine.LoadComponentScript("EntityLimits.js");
+Engine.LoadComponentScript("TrainingRestrictions.js");
+Engine.LoadHelperScript("Sound.js");
+
+Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
+Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
+
+function regression_test_d1879()
+{
+ // Setup
+ let playerEnt = 2;
+ let playerID = 1;
+ let testEntity = 3;
+ let spawedEntityIDs = [4, 5, 6, 7, 8];
+ let spawned = 0;
+
+ Engine.AddEntity = () => {
+ let id = spawedEntityIDs[spawned++];
+
+ ConstructComponent(id, "TrainingRestrictions", {
+ "Category": "some_limit"
+ });
+
+ AddMock(id, IID_Identity, {
+ "GetClassesList": () => []
+ });
+
+ AddMock(id, IID_Position, {
+ "JumpTo": () => {}
+ });
+
+ AddMock(id, IID_Ownership, {
+ "SetOwner": (pid) => {
+ let cmpEntLimits = QueryOwnerInterface(id, IID_EntityLimits);
+ cmpEntLimits.OnGlobalOwnershipChanged({
+ "entity": id,
+ "from": -1,
+ "to": pid
+ });
+ },
+ "GetOwner": () => playerID
+ });
+
+ return id;
+ };
+
+ ConstructComponent(playerEnt, "EntityLimits", {
+ "Limits": {
+ "some_limit": 8
+ },
+ "LimitChangers": {},
+ "LimitRemovers": {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
+ "PushNotification": () => {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_Trigger, {
+ "CallEvent": () => {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_Timer, {
+ "SetTimeout": (ent, iid, func) => {}
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
+ "TemplateExists": () => true,
+ "GetTemplate": name => ({
+ "Cost": {
+ "BuildTime": 0,
+ "Population": 1,
+ "Resources": {}
+ },
+ "TrainingRestrictions": {
+ "Category": "some_limit"
+ }
+ })
+ });
+
+ AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
+ "GetPlayerByID": id => playerEnt
+ });
+
+ AddMock(playerEnt, IID_Player, {
+ "GetCiv": () => "iber",
+ "GetPlayerID": () => playerID,
+ "GetTimeMultiplier": () => 0,
+ "BlockTraining": () => {},
+ "UnBlockTraining": () => {},
+ "UnReservePopulationSlots": () => {},
+ "TrySubtractResources": () => true,
+ "TryReservePopulationSlots": () => false // Always have pop space.
+ });
+
+ AddMock(testEntity, IID_Ownership, {
+ "GetOwner": () => playerID
+ });
+
+ let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
+ "Entities": { "_string": "some_template" },
+ "BatchTimeModifier": 1
+ });
+
+ let cmpEntLimits = QueryOwnerInterface(testEntity, IID_EntityLimits);
+ TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 8));
+ TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 9));
+
+ // Check that the entity limits do get updated if the spawn succeeds.
+ AddMock(testEntity, IID_Footprint, {
+ "PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
+ });
+
+ cmpProdQueue.AddBatch("some_template", "unit", 3);
+
+ TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
+
+ Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout();
+
+ TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
+
+ // Now check that it doesn't get updated when the spawn doesn't succeed.
+ AddMock(testEntity, IID_Footprint, {
+ "PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 })
+ });
+
+ cmpProdQueue.AddBatch("some_template", "unit", 3);
+ Engine.QueryInterface(testEntity, IID_ProductionQueue).ProgressTimeout();
+
+ TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
+ TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
+}
+
+regression_test_d1879();