1
0
forked from mirrors/0ad

Allow Modifiers to affect tokens.

This adds a new mode to modifications called "tokens" which allows
clever token parsing.
Technologies, auras and modifiers in general can use this to switch out,
add or delete tokens dynamically.

Currently implemented are production and builder queue tokens.

Reviewed By: Freagarach
Differential Revision: https://code.wildfiregames.com/D270
This was SVN commit r23843.
This commit is contained in:
wraitii
2020-07-17 08:23:45 +00:00
parent 370a669b84
commit 70c71bff0f
12 changed files with 444 additions and 219 deletions
@@ -9,13 +9,16 @@
* Returns modified property value modified by the applicable tech
* modifications.
*
* @param currentTechModifications array of modificiations
* @param modifications array of modificiations
* @param classes Array containing the class list of the template.
* @param originalValue Number storing the original value. Can also be
* non-numberic, but then only "replace" techs can be supported.
* non-numeric, but then only "replace" and "tokens" techs can be supported.
*/
function GetTechModifiedProperty(modifications, classes, originalValue)
{
if (!modifications.length)
return originalValue;
let multiply = 1;
let add = 0;
@@ -25,6 +28,8 @@ function GetTechModifiedProperty(modifications, classes, originalValue)
continue;
if (modification.replace !== undefined)
return modification.replace;
if (modification.tokens !== undefined)
return HandleTokens(originalValue, modification.tokens);
if (modification.multiply)
multiply *= modification.multiply;
else if (modification.add)
@@ -32,11 +37,7 @@ function GetTechModifiedProperty(modifications, classes, originalValue)
else
warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification));
}
// Note, some components pass non-numeric values (for which only the "replace" modification makes sense)
if (typeof originalValue == "number")
return originalValue * multiply + add;
return originalValue;
return originalValue * multiply + add;
}
/**
@@ -47,6 +48,35 @@ function DoesModificationApply(modification, classes)
return MatchesClassList(classes, modification.affects);
}
/**
* Returns a modified list of tokens.
* Supports "A>B" to replace A by B, "-A" to remove A, and the rest will add tokens.
*/
function HandleTokens(originalValue, modification)
{
let tokens = originalValue === "" ? [] : originalValue.split(/\s+/);
let newTokens = modification === "" ? [] : modification.split(/\s+/);
for (let token of newTokens)
{
if (token.indexOf(">") !== -1)
{
let [oldToken, newToken] = token.split(">");
let index = tokens.indexOf(oldToken);
if (index !== -1)
tokens[index] = newToken;
}
else if (token[0] == "-")
{
let index = tokens.indexOf(token.substr(1));
if (index !== -1)
tokens.splice(index, 1);
}
else
tokens.push(token);
}
return tokens.join(" ");
}
/**
* Derives the technology requirements from a given technology template.
* Takes into account the `supersedes` attribute.
@@ -214,12 +214,6 @@ var g_NotificationsTypes =
if (player == Engine.GetPlayerID())
openDialog(notification.dialogName, notification.data, player);
},
"resetselectionpannel": function(notification, player)
{
if (player != Engine.GetPlayerID())
return;
g_Selection.rebuildSelection({});
},
"playercommand": function(notification, player)
{
// For observers, focus the camera on units commanded by the selected player
@@ -620,6 +620,13 @@ function onSimulationUpdate()
}
g_SimState = undefined;
// Some changes may require re-rendering the selection.
if (Engine.GuiInterfaceCall("IsSelectionDirty"))
{
g_Selection.onChange();
Engine.GuiInterfaceCall("ResetSelectionDirty");
}
if (!GetSimState())
return;
@@ -15,8 +15,7 @@ m.Template = m.Class({
this._tpCache = new Map();
},
// helper function to return a template value, optionally adjusting for tech.
// TODO: there's no support for "_string" values here.
// Helper function to return a template value, adjusting for tech.
"get": function(string)
{
let value = this._template;
@@ -79,6 +79,21 @@ PETRA.BuildManager.prototype.checkEvents = function(gameState, events)
if (ent && ent.hasClass("Unit"))
this.incrementBuilderCounters(civ, ent, increment);
}
for (let evt of events.ValueModification)
{
if (evt.component != "Builder" ||
!evt.valueNames.some(val => val.startsWith("Builder/Entities/")))
continue;
// Unfortunately there really is not an easy way to determine the changes
// at this stage, so we simply have to dump the cache.
this.builderCounters = new Map();
let civ = gameState.getPlayerCiv();
for (let ent of gameState.getOwnUnits().values())
this.incrementBuilderCounters(civ, ent, 1);
}
};
@@ -14,6 +14,7 @@ AIInterface.prototype.EventNames = [
"AIMetadata",
"PlayerDefeated",
"EntityRenamed",
"ValueModification",
"OwnershipChanged",
"Garrison",
"UnGarrison",
@@ -251,7 +252,7 @@ AIInterface.prototype.OnTemplateModification = function(msg)
if (!ended)
continue;
// item now contains the template value for this.
let oldValue = +item;
let oldValue = +item == item ? +item : item;
let newValue = ApplyValueModificationsToTemplate(valName, oldValue, msg.player, template);
// Apply the same roundings as in the components
if (valName === "Player/MaxPopulation" || valName === "Cost/Population" ||
@@ -273,6 +274,7 @@ AIInterface.prototype.OnTemplateModification = function(msg)
AIInterface.prototype.OnGlobalValueModification = function(msg)
{
this.events.ValueModification.push(msg);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let ent of msg.entities)
{
@@ -299,7 +301,7 @@ AIInterface.prototype.OnGlobalValueModification = function(msg)
if (!ended)
continue;
// "item" now contains the unmodified template value for this.
let oldValue = +item;
let oldValue = +item == item ? +item : item;
let newValue = ApplyValueModificationsToEntity(valName, oldValue, ent);
// Apply the same roundings as in the components
if (valName === "Player/MaxPopulation" || valName === "Cost/Population" ||
@@ -34,6 +34,8 @@ Builder.prototype.GetEntitiesList = function()
if (!cmpPlayer)
return [];
string = ApplyValueModificationsToEntity("Builder/Entities/_string", string, this.entity);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (cmpIdentity)
string = string.replace(/\{native\}/g, cmpIdentity.GetCiv());
@@ -85,4 +87,15 @@ Builder.prototype.PerformBuilding = function(target)
}
};
Builder.prototype.OnValueModification = function(msg)
{
if (msg.component != "Builder" || !msg.valueNames.some(name => name.endsWith('_string')))
return;
// Token changes may require selection updates.
let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
Engine.RegisterComponentType(IID_Builder, "Builder", Builder);
@@ -35,6 +35,7 @@ GuiInterface.prototype.Init = function()
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
@@ -659,6 +660,7 @@ GuiInterface.prototype.GetNeededResources = function(player, data)
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
@@ -671,6 +673,35 @@ GuiInterface.prototype.ResetTemplateModified = function()
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
@@ -1991,7 +2022,9 @@ let exposedFunctions = {
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
@@ -863,23 +863,12 @@ Player.prototype.AddDisabledTemplate = function(template)
{
this.disabledTemplates[template] = true;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {});
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "resetselectionpannel",
"players": [this.GetPlayerID()]
});
};
Player.prototype.RemoveDisabledTemplate = function(template)
{
this.disabledTemplates[template] = false;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {});
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "resetselectionpannel",
"players": [this.GetPlayerID()]
});
};
Player.prototype.SetDisabledTemplates = function(templates)
@@ -888,12 +877,6 @@ Player.prototype.SetDisabledTemplates = function(templates)
for (let template of templates)
this.disabledTemplates[template] = true;
Engine.BroadcastMessage(MT_DisabledTemplatesChanged, {});
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({
"type": "resetselectionpannel",
"players": [this.GetPlayerID()]
});
};
Player.prototype.GetDisabledTemplates = function()
@@ -75,60 +75,122 @@ ProductionQueue.prototype.Init = function()
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return this.entitiesList;
return Array.from(this.entitiesMap.values());
};
ProductionQueue.prototype.CalculateEntitiesList = function()
/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
ProductionQueue.prototype.CalculateEntitiesMap = function()
{
this.entitiesList = [];
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
if (!this.template.Entities)
return;
let string = this.template.Entities._string;
if (!string)
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
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+/);
let disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
// Filter out disabled and invalid entities.
let disabledEntities = cmpPlayer.GetDisabledTemplates();
entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent));
/**
* Process tokens:
* - process token modifiers (this is a bit tricky).
* - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
* - remove disabled entities
* - upgrade templates where necessary
* This also updates currently queued production (it's more convenient to do it here).
*/
// 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;
let removeAllQueuedTemplate = (token) => {
let queue = clone(this.queue);
let template = this.entitiesMap.get(token);
for (let item of queue)
if (item.unitTemplate && item.unitTemplate === template)
this.RemoveBatch(item.id);
};
let updateAllQueuedTemplate = (token, updateTo) => {
let template = this.entitiesMap.get(token);
for (let item of this.queue)
if (item.unitTemplate && item.unitTemplate === template)
item.unitTemplate = updateTo;
};
for (let templateName of entitiesList)
this.entitiesList.push(upgradeTemplate(templateName));
let toks = string.split(/\s+/);
for (let tok of addedTokens)
toks.push(tok);
for (let item of this.queue)
if (item.unitTemplate)
item.unitTemplate = upgradeTemplate(item.unitTemplate);
let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
let rawToken = token;
if (!(token in addedDict))
{
// This is a bit wasteful but I can't think of a simpler/better way.
// The list of token is unlikely to be a performance bottleneck anyways.
token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity);
token = token.split(/\s+/);
if (token.every(tok => addedTokens.indexOf(tok) !== -1))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = token[0];
}
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
if (cmpIdentity)
token = token.replace(/\{native\}/g, cmpIdentity.GetCiv());
if (cmpPlayer)
token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv());
// Filter out disabled and invalid entities.
if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = this.GetUpgradedTemplate(token);
entMap.set(rawToken, token);
updateAllQueuedTemplate(rawToken, token);
return entMap;
}, new Map());
};
/*
* Returns the upgraded template name if necessary.
*/
ProductionQueue.prototype.GetUpgradedTemplate = function(templateName)
{
let cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
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;
};
/*
@@ -140,6 +202,8 @@ ProductionQueue.prototype.GetTechnologiesList = function()
return [];
let string = this.template.Technologies._string;
string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity);
if (!string)
return [];
@@ -148,8 +212,7 @@ ProductionQueue.prototype.GetTechnologiesList = function()
return [];
let cmpPlayer = QueryOwnerInterface(this.entity);
let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpPlayer || !cmpIdentity)
if (!cmpPlayer)
return [];
let techs = string.split(/\s+/);
@@ -558,7 +621,7 @@ ProductionQueue.prototype.OnOwnershipChanged = function(msg)
cmpPlayer.UnBlockTraining();
}
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesList();
this.CalculateEntitiesMap();
// Reset the production queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
@@ -570,7 +633,7 @@ ProductionQueue.prototype.OnOwnershipChanged = function(msg)
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesList();
this.CalculateEntitiesMap();
};
ProductionQueue.prototype.OnDestroy = function()
@@ -866,15 +929,29 @@ 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();
if (msg.component != "Promotion" && (msg.component != "ProductionQueue" ||
!msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/"))))
return;
if (msg.entities.indexOf(this.entity) === -1)
return;
// This also updates the queued production if necessary.
this.CalculateEntitiesMap();
// Inform the GUI that it'll need to recompute the selection panel.
// TODO: it would be better to only send the message if something actually changing
// for the current production queue.
let cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
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();
this.CalculateEntitiesMap();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
@@ -1,4 +1,3 @@
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("Builder.js");
@@ -11,6 +10,9 @@ AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true
});
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (prop, oVal, ent) => oVal);
let cmpBuilder = ConstructComponent(builderId, "Builder", {
"Rate": 1.0,
"Entities": { "_string": "structures/{civ}_barracks structures/{civ}_civil_centre structures/{native}_house" }
@@ -1,145 +1,7 @@
Resources = {
"BuildSchema": (a, b) => {}
};
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.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");
@@ -148,12 +10,150 @@ Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("EntityLimits.js");
Engine.RegisterGlobal("Resources", {
"BuildSchema": (a, b) => {}
});
Engine.LoadComponentScript("ProductionQueue.js");
Engine.LoadComponentScript("TrainingRestrictions.js");
Engine.LoadHelperScript("Sound.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
function testEntitiesList()
{
Engine.RegisterGlobal("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.GetUpgradedTemplate = (template) => template;
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.CalculateEntitiesMap();
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.CalculateEntitiesMap();
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.CalculateEntitiesMap();
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.CalculateEntitiesMap();
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.CalculateEntitiesMap();
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"]
);
}
function regression_test_d1879()
{
// Setup
@@ -280,4 +280,74 @@ function regression_test_d1879()
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
}
function test_token_changes()
{
const ent = 10;
let cmpProductionQueue = ConstructComponent(10, "ProductionQueue", {
"Entities": { "_string": "units/{civ}_a " +
"units/{civ}_b" },
"Technologies": { "_string": "a " +
"b_{civ} " +
"c_{civ}" },
"BatchTimeModifier": 1
});
cmpProductionQueue.GetUpgradedTemplate = (template) => template;
// Merges interface of multiple components because it's enough here.
Engine.RegisterGlobal("QueryOwnerInterface", () => ({
// player
"GetCiv": () => "test",
"GetDisabledTemplates": () => [],
"GetDisabledTechnologies": () => [],
"TrySubtractResources": () => true,
"AddResources": () => {},
"GetPlayerID": () => 1,
// entitylimits
"ChangeCount": () => {},
// techmanager
"CheckTechnologyRequirements": () => true,
"IsTechnologyResearched": () => false,
"IsInProgress": () => false
}));
Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface);
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"SetSelectionDirty": () => {}
});
// Test Setup
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(), ["units/test_a", "units/test_b"]
);
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetTechnologiesList(), ["a", "b_generic", "c_generic"]
);
// Add a unit of each type to our queue, validate.
cmpProductionQueue.AddBatch("units/test_a", "unit", 1, {});
cmpProductionQueue.AddBatch("units/test_b", "unit", 1, {});
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test_a");
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[1].unitTemplate, "units/test_b");
// Add a modifier that replaces unit A with unit C,
// adds a unit D and removes unit B from the roster.
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, val) => {
return HandleTokens(val, "units/{civ}_a>units/{civ}_c units/{civ}_d -units/{civ}_b");
});
cmpProductionQueue.OnValueModification({
"component": "ProductionQueue",
"valueNames": ["ProductionQueue/Entities/_string"],
"entities": [ent]
});
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(), ["units/test_c", "units/test_d"]
);
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue()[0].unitTemplate, "units/test_c");
TS_ASSERT_EQUALS(cmpProductionQueue.GetQueue().length, 1);
}
testEntitiesList();
regression_test_d1879();
test_token_changes();