Split tasks from ProductionQueue.

The task of the production queue should first and foremost be that; a
queue for production items.
Hence, the specifics of training/researching are delegated to specific
components.

As a side effect, this improves the test coverage and fixes:
- Resource not refunding when hitting the entity limit. Introduced in
b8758c8941.
- Autoqueue changing when unable to spawn. Introduced in 956b3f96db.

Modders can change their templates using
https://code.wildfiregames.com/P256.

Differential revision: https://code.wildfiregames.com/D4333
Fixes: #6363
Comments by: @Silier
Refs. #6364

This was SVN commit r26000.
This commit is contained in:
Freagarach 2021-11-16 07:08:39 +00:00
parent b5d85e279f
commit 0c4f59d0a7
169 changed files with 2755 additions and 2056 deletions

View file

@ -471,11 +471,11 @@ function GetTemplateDataHelper(template, player, auraTemplates, modifiers = {})
}
}
if (template.ProductionQueue)
if (template.Researcher)
{
ret.techCostMultiplier = {};
for (let res in template.ProductionQueue.TechCostMultiplier)
ret.techCostMultiplier[res] = getEntityValue("ProductionQueue/TechCostMultiplier/" + res);
for (const res in template.Researcher.TechCostMultiplier)
ret.techCostMultiplier[res] = getEntityValue("Researcher/TechCostMultiplier/" + res);
}
if (template.Trader)

View file

@ -123,7 +123,7 @@ class TemplateLister
let template = this.TemplateLoader.loadEntityTemplate(templateName, civCode);
let templateLists = this.TemplateLoader.deriveProductionQueue(template, civCode);
const templateLists = this.TemplateLoader.deriveProduction(template, civCode);
templateLists.structures = this.TemplateLoader.deriveBuildQueue(template, civCode);
if (template.WallSet)

View file

@ -137,36 +137,36 @@ class TemplateLoader
};
}
deriveProductionQueue(template, civCode)
deriveProduction(template, civCode)
{
let production = {
const production = {
"techs": [],
"units": []
};
if (!template.ProductionQueue)
if (!template.Researcher && !template.Trainer)
return production;
if (template.ProductionQueue.Entities && template.ProductionQueue.Entities._string)
for (let templateName of template.ProductionQueue.Entities._string.split(" "))
if (template.Trainer?.Entities?._string)
for (let templateName of template.Trainer.Entities._string.split(" "))
{
templateName = templateName.replace(/\{(civ|native)\}/g, civCode);
if (Engine.TemplateExists(templateName))
production.units.push(templateName);
}
let appendTechnology = (technologyName) => {
let technology = this.loadTechnologyTemplate(technologyName, civCode);
const appendTechnology = (technologyName) => {
const technology = this.loadTechnologyTemplate(technologyName, civCode);
if (DeriveTechnologyRequirements(technology, civCode))
production.techs.push(technologyName);
};
if (template.ProductionQueue.Technologies && template.ProductionQueue.Technologies._string)
for (let technologyName of template.ProductionQueue.Technologies._string.split(" "))
if (template.Researcher?.Technologies?._string)
for (let technologyName of template.Researcher.Technologies._string.split(" "))
{
if (technologyName.indexOf("{civ}") != -1)
{
let civTechName = technologyName.replace("{civ}", civCode);
const civTechName = technologyName.replace("{civ}", civCode);
technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic");
}

View file

@ -65,7 +65,7 @@ class TemplateParser
parsed.history = template.Identity.History;
parsed.production = this.TemplateLoader.deriveProductionQueue(template, civCode);
parsed.production = this.TemplateLoader.deriveProduction(template, civCode);
if (template.Builder)
parsed.builder = this.TemplateLoader.deriveBuildQueue(template, civCode);

View file

@ -1402,9 +1402,9 @@ function OnTrainMouseWheel(dir)
function getBuildingsWhichCanTrainEntity(entitiesToCheck, trainEntType)
{
return entitiesToCheck.filter(entity => {
let state = GetEntityState(entity);
return state && state.production && state.production.entities.length &&
state.production.entities.indexOf(trainEntType) != -1 && (!state.upgrade || !state.upgrade.isUpgrading);
const state = GetEntityState(entity);
return state?.trainer?.entities?.indexOf(trainEntType) != -1 &&
(!state.upgrade || !state.upgrade.isUpgrading);
});
}

View file

@ -627,10 +627,10 @@ g_SelectionPanels.Research = {
{
let ret = [];
if (unitEntStates.length == 1)
return !unitEntStates[0].production || !unitEntStates[0].production.technologies ? ret :
unitEntStates[0].production.technologies.map(tech => ({
return !unitEntStates[0].researcher || !unitEntStates[0].researcher.technologies ? ret :
unitEntStates[0].researcher.technologies.map(tech => ({
"tech": tech,
"techCostMultiplier": unitEntStates[0].production.techCostMultiplier,
"techCostMultiplier": unitEntStates[0].researcher.techCostMultiplier,
"researchFacilityId": unitEntStates[0].id,
"isUpgrading": !!unitEntStates[0].upgrade && unitEntStates[0].upgrade.isUpgrading
}));
@ -642,11 +642,11 @@ g_SelectionPanels.Research = {
for (let state of sortedEntStates)
{
if (!state.production || !state.production.technologies)
if (!state.researcher || !state.researcher.technologies)
continue;
// Remove the techs we already have in ret (with the same name and techCostMultiplier)
let filteredTechs = state.production.technologies.filter(
const filteredTechs = state.researcher.technologies.filter(
tech => tech != null && !ret.some(
item =>
(item.tech == tech ||
@ -655,14 +655,14 @@ g_SelectionPanels.Research = {
item.tech.bottom == tech.bottom &&
item.tech.top == tech.top) &&
Object.keys(item.techCostMultiplier).every(
k => item.techCostMultiplier[k] == state.production.techCostMultiplier[k])
k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
));
if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
ret = ret.concat(filteredTechs.map(tech => ({
"tech": tech,
"techCostMultiplier": state.production.techCostMultiplier,
"techCostMultiplier": state.researcher.techCostMultiplier,
"researchFacilityId": state.id,
"isUpgrading": !!state.upgrade && state.upgrade.isUpgrading
})));

View file

@ -558,7 +558,7 @@ function turnAutoQueueOn()
"type": "autoqueue-on",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return !!state?.production?.entities.length &&
return !!state?.trainer?.entities?.length &&
!state.production.autoqueue;
})
});
@ -570,7 +570,7 @@ function turnAutoQueueOff()
"type": "autoqueue-off",
"entities": g_Selection.filter(ent => {
let state = GetEntityState(ent);
return !!state?.production?.entities.length &&
return !!state?.trainer?.entities?.length &&
state.production.autoqueue;
})
});

View file

@ -1058,8 +1058,8 @@ var g_UnitActions =
// Don't allow the rally point to be set on any of the currently selected entities (used for unset)
// except if the autorallypoint hotkey is pressed and the target can produce entities.
if (targetState && (!Engine.HotkeyIsPressed("session.autorallypoint") ||
!targetState.production ||
!targetState.production.entities.length))
!targetState.trainer ||
!targetState.trainer.entities.length))
for (const ent of g_Selection.toList())
if (targetState.id == ent)
return false;
@ -1154,9 +1154,9 @@ var g_UnitActions =
{
// Find a trader (if any) that this structure can train.
let trader;
if (entState.production && entState.production.entities.length)
for (let i = 0; i < entState.production.entities.length; ++i)
if ((trader = GetTemplateData(entState.production.entities[i]).trader))
if (entState.trainer?.entities?.length)
for (let i = 0; i < entState.trainer.entities.length; ++i)
if ((trader = GetTemplateData(entState.trainer.entities[i]).trader))
break;
let traderData = {
@ -1793,7 +1793,7 @@ var g_EntityCommands =
"autoqueue-on": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.production || !entState.production.entities.length || entState.production.autoqueue))
if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueon") +
@ -1813,7 +1813,7 @@ var g_EntityCommands =
"autoqueue-off": {
"getInfo": function(entStates)
{
if (entStates.every(entState => !entState.production || !entState.production.entities.length || !entState.production.autoqueue))
if (entStates.every(entState => !entState.trainer || !entState.trainer.entities.length || !entState.production.autoqueue))
return false;
return {
"tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.queueunit.autoqueueoff") +

View file

@ -191,8 +191,8 @@ function getAllTrainableEntities(selection)
for (let ent of selection)
{
let state = GetEntityState(ent);
if (state && state.production && state.production.entities.length)
trainableEnts = trainableEnts.concat(state.production.entities);
if (state?.trainer?.entities?.length)
trainableEnts = trainableEnts.concat(state.trainer.entities);
}
// Remove duplicates

View file

@ -119,7 +119,7 @@ m.Template = m.Class({
},
"techCostMultiplier": function(type) {
return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1);
return +(this.get("Researcher/TechCostMultiplier/"+type) || 1);
},
/**
@ -340,14 +340,14 @@ m.Template = m.Class({
},
"trainableEntities": function(civ) {
let templates = this.get("ProductionQueue/Entities/_string");
const templates = this.get("Trainer/Entities/_string");
if (!templates)
return undefined;
return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
},
"researchableTechs": function(gameState, civ) {
let templates = this.get("ProductionQueue/Technologies/_string");
const templates = this.get("Researcher/Technologies/_string");
if (!templates)
return undefined;
let techs = templates.split(/\s+/);
@ -452,10 +452,10 @@ m.Template = m.Class({
"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
"buildTime": function(productionQueue) {
"buildTime": function(researcher) {
let time = +this.get("Cost/BuildTime");
if (productionQueue)
time *= productionQueue.techCostMultiplier("time");
if (researcher)
time *= researcher.techCostMultiplier("time");
return time;
},
@ -940,7 +940,7 @@ m.Entity = m.Class({
return this;
},
"train": function(civ, type, count, metadata, promotedTypes, pushFront = false)
"train": function(civ, type, count, metadata, pushFront = false)
{
let trainable = this.trainableEntities(civ);
if (!trainable)
@ -960,7 +960,6 @@ m.Entity = m.Class({
"template": type,
"count": count,
"metadata": metadata,
"promoted": promotedTypes,
"pushFront": pushFront
});
return this;

View file

@ -65,7 +65,7 @@ m.Technology.prototype.pairedWith = function()
return this._pairedWith;
};
m.Technology.prototype.cost = function(productionQueue)
m.Technology.prototype.cost = function(researcher)
{
if (!this._template.cost)
return undefined;
@ -73,15 +73,15 @@ m.Technology.prototype.cost = function(productionQueue)
for (let type in this._template.cost)
{
cost[type] = +this._template.cost[type];
if (productionQueue)
cost[type] *= productionQueue.techCostMultiplier(type);
if (researcher)
cost[type] *= researcher.techCostMultiplier(type);
}
return cost;
};
m.Technology.prototype.costSum = function(productionQueue)
m.Technology.prototype.costSum = function(researcher)
{
let cost = this.cost(productionQueue);
const cost = this.cost(researcher);
if (!cost)
return 0;
let ret = 0;

View file

@ -124,7 +124,7 @@ PETRA.TrainingPlan.prototype.start = function(gameState)
if (this.metadata && this.metadata.base !== undefined && this.metadata.base === 0)
this.metadata.base = this.trainers[0].getMetadata(PlayerID, "base");
this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata, this.promotedTypes(gameState));
this.trainers[0].train(gameState.getPlayerCiv(), this.type, this.number, this.metadata);
this.onStart(gameState);
};
@ -134,36 +134,6 @@ PETRA.TrainingPlan.prototype.addItem = function(amount = 1)
this.number += amount;
};
/** Find the promoted types corresponding to this.type */
PETRA.TrainingPlan.prototype.promotedTypes = function(gameState)
{
let types = [];
let promotion = this.template.promotion();
let previous;
let template;
while (promotion)
{
types.push(promotion);
previous = promotion;
template = gameState.getTemplate(promotion);
if (!template)
{
if (gameState.ai.Config.debug > 0)
API3.warn(" promotion template " + promotion + " is not found");
promotion = undefined;
break;
}
promotion = template.promotion();
if (previous === promotion)
{
if (gameState.ai.Config.debug > 0)
API3.warn(" unit " + promotion + " is its own promoted unit");
promotion = undefined;
}
}
return types;
};
PETRA.TrainingPlan.prototype.Serialize = function()
{
return {

View file

@ -340,6 +340,13 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
"isUpgrading": cmpUpgrade.IsUpgrading()
};
const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher);
if (cmpResearcher)
ret.researcher = {
"technologies": cmpResearcher.GetTechnologiesList(),
"techCostMultiplier": cmpResearcher.GetTechCostMultiplier()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
@ -347,13 +354,16 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue(),
"autoqueue": cmpProductionQueue.IsAutoQueueing()
};
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
if (cmpTrainer)
ret.trainer = {
"entities": cmpTrainer.GetEntitiesList()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
@ -689,10 +699,10 @@ GuiInterface.prototype.GetStartedResearch = function(player)
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
const cmpResearcher = Engine.QueryInterface(ret[tech].researcher, IID_Researcher);
if (cmpResearcher)
{
const research = cmpProductionQueue.GetQueue().find(item => item.technologyTemplate === tech);
const research = cmpResearcher.GetResearchingTechnologyByName(tech);
ret[tech].progress = research.progress;
ret[tech].timeRemaining = research.timeRemaining;
ret[tech].paused = research.paused;
@ -1978,11 +1988,7 @@ GuiInterface.prototype.CanAttack = function(player, data)
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
return Engine.QueryInterface(data.entity, IID_Trainer)?.GetBatchTime(data.batchSize) || 0;
};
GuiInterface.prototype.IsMapRevealed = function(player)

View file

@ -390,6 +390,16 @@ Player.prototype.TrySubtractResources = function(amounts)
return true;
};
Player.prototype.RefundResources = function(amounts)
{
const cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker);
if (cmpStatisticsTracker)
for (const type in amounts)
cmpStatisticsTracker.IncreaseResourceUsedCounter(type, -amounts[type]);
this.AddResources(amounts);
};
Player.prototype.GetNextTradingGoods = function()
{
let value = randFloat(0, 100);

View file

@ -0,0 +1,447 @@
function Researcher() {}
Researcher.prototype.Schema =
"<a:help>Allows the entity to research technologies.</a:help>" +
"<a:example>" +
"<TechCostMultiplier>" +
"<food>0.5</food>" +
"<wood>0.1</wood>" +
"<stone>0</stone>" +
"<metal>2</metal>" +
"<time>0.9</time>" +
"</TechCostMultiplier>" +
"<Technologies datatype='tokens'>" +
"\n phase_town_{civ}\n phase_metropolis_ptol\n unlock_shared_los\n wonder_population_cap\n " +
"</Technologies>" +
"</a:example>" +
"<optional>" +
"<element name='Technologies' a:help='Space-separated list of technology names that this building can research. When present, the special string \"{civ}\" will be automatically replaced either by the civ code of the building&apos;s owner if such a tech exists, or by \"generic\".'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='TechCostMultiplier' a:help='Multiplier to modify resources cost and research time of technologies researched in this building.'>" +
Resources.BuildSchema("nonNegativeDecimal", ["time"]) +
"</element>" +
"</optional>";
/**
* This object represents a technology being researched.
*/
Researcher.prototype.Item = function() {};
/**
* @param {string} templateName - The name of the template we ought to research.
* @param {number} researcher - The entity ID of our researcher.
* @param {string} metadata - Optionally any metadata to attach to us.
*/
Researcher.prototype.Item.prototype.Init = function(templateName, researcher, metadata)
{
this.templateName = templateName;
this.researcher = researcher;
this.metadata = metadata;
};
/**
* Prepare for the queue.
* @param {Object} techCostMultiplier - The multipliers to use when calculating costs.
* @return {boolean} - Whether the item was successfully initiated.
*/
Researcher.prototype.Item.prototype.Queue = function(techCostMultiplier)
{
const template = TechnologyTemplates.Get(this.templateName);
if (!template)
return false;
this.resources = {};
if (template.cost)
for (const res in template.cost)
this.resources[res] = Math.floor((techCostMultiplier[res] === undefined ? 1 : techCostMultiplier[res]) * template.cost[res]);
const cmpPlayer = QueryOwnerInterface(this.researcher);
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer?.TrySubtractResources(this.resources))
return false;
this.player = cmpPlayer.GetPlayerID();
const time = (techCostMultiplier.time || 1) * (template.researchTime || 0) * 1000;
this.timeRemaining = time;
this.timeTotal = time;
// Tell the technology manager that we have queued researching this
// such that players can't research the same thing twice.
const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(this.templateName, this.researcher);
return true;
};
Researcher.prototype.Item.prototype.Stop = function()
{
const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
if (cmpTechnologyManager)
cmpTechnologyManager.StoppedResearch(this.templateName, true);
QueryPlayerIDInterface(this.player)?.RefundResources(this.resources);
delete this.resources;
};
/**
* Called when the first work is performed.
*/
Researcher.prototype.Item.prototype.Start = function()
{
const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
cmpTechnologyManager.StartedResearch(this.templateName, true);
this.started = true;
};
Researcher.prototype.Item.prototype.Finish = function()
{
const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager);
cmpTechnologyManager.ResearchTechnology(this.templateName);
const template = TechnologyTemplates.Get(this.templateName);
if (template?.soundComplete)
Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher);
this.finished = true;
};
/**
* @param {number} allocatedTime - The time allocated to this item.
* @return {number} - The time used for this item.
*/
Researcher.prototype.Item.prototype.Progress = function(allocatedTime)
{
if (!this.started)
this.Start();
if (this.timeRemaining > allocatedTime)
{
this.timeRemaining -= allocatedTime;
return allocatedTime;
}
this.Finish();
return this.timeRemaining;
};
Researcher.prototype.Item.prototype.Pause = function()
{
this.paused = true;
};
Researcher.prototype.Item.prototype.Unpause = function()
{
delete this.paused;
};
/**
* @return {Object} - Some basic information of this item.
*/
Researcher.prototype.Item.prototype.GetBasicInfo = function()
{
return {
"technologyTemplate": this.templateName,
"progress": 1 - (this.timeRemaining / this.timeTotal),
"timeRemaining": this.timeRemaining,
"paused": this.paused,
"metadata": this.metadata
};
};
Researcher.prototype.Item.prototype.Serialize = function(id)
{
return {
"id": id,
"metadata": this.metadata,
"paused": this.paused,
"player": this.player,
"researcher": this.researcher,
"resource": this.resources,
"started": this.started,
"templateName": this.templateName,
"timeRemaining": this.timeRemaining,
"timeTotal": this.timeTotal,
};
};
Researcher.prototype.Item.prototype.Deserialize = function(data)
{
this.Init(data.templateName, data.researcher, data.metadata);
this.paused = data.paused;
this.player = data.player;
this.researcher = data.researcher;
this.resources = data.resources;
this.started = data.started;
this.timeRemaining = data.timeRemaining;
this.timeTotal = data.timeTotal;
};
Researcher.prototype.Init = function()
{
this.nextID = 1;
this.queue = new Map();
};
Researcher.prototype.Serialize = function()
{
const queue = [];
for (const [id, item] of this.queue)
queue.push(item.Serialize(id));
return {
"nextID": this.nextID,
"queue": queue
};
};
Researcher.prototype.Deserialize = function(data)
{
this.Init();
this.nextID = data.nextID;
for (const item of data.queue)
{
const newItem = new this.Item();
newItem.Deserialize(item);
this.queue.set(item.id, newItem);
}
};
/*
* Returns list of technologies that can be researched by this entity.
*/
Researcher.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
let string = this.template.Technologies._string;
string = ApplyValueModificationsToEntity("Researcher/Technologies/_string", string, this.entity);
if (!string)
return [];
const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
const cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return [];
let techs = string.split(/\s+/);
// Replace the civ specific technologies.
for (let i = 0; i < techs.length; ++i)
{
const tech = techs[i];
if (tech.indexOf("{civ}") == -1)
continue;
const 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));
const techList = [];
const superseded = {};
const disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons.
// Also store what technology is superseded in the superseded object { "tech1":"techWhichSupercedesTech1", ... }.
for (const tech of techs)
{
if (disabledTechnologies && disabledTechnologies[tech])
continue;
const 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 (const i in techList)
{
let tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
tech = superseded[tech];
techList[i] = tech;
}
const ret = [];
// This inserts the techs into the correct positions to line up the technology pairs.
for (let i = 0; i < techList.length; ++i)
{
const tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
const template = TechnologyTemplates.Get(tech);
if (template.top)
ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom };
else
ret[i] = tech;
}
return ret;
};
/**
* @return {Object} - The multipliers to change the costs of any research with.
*/
Researcher.prototype.GetTechCostMultiplier = function()
{
const techCostMultiplier = {};
for (const res in this.template.TechCostMultiplier)
techCostMultiplier[res] = ApplyValueModificationsToEntity(
"Researcher/TechCostMultiplier/" + res,
+this.template.TechCostMultiplier[res],
this.entity);
return techCostMultiplier;
};
/**
* Checks whether we can research the given technology, minding paired techs.
*/
Researcher.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
const 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);
};
/**
* @param {string} templateName - The technology to queue.
* @param {string} metadata - Any metadata attached to the item.
* @return {number} - The ID of the item. -1 if the item could not be researched.
*/
Researcher.prototype.QueueTechnology = function(templateName, metadata)
{
if (!this.GetTechnologiesList().some(tech =>
tech && (tech == templateName ||
tech.pair && (tech.top == templateName || tech.bottom == templateName))))
{
error("This entity cannot research " + templateName + ".");
return -1;
}
const item = new this.Item();
item.Init(templateName, this.entity, metadata);
const techCostMultiplier = this.GetTechCostMultiplier();
if (!item.Queue(techCostMultiplier))
return -1;
const id = this.nextID++;
this.queue.set(id, item);
return id;
};
/**
* @param {number} id - The id of the technology researched here we need to stop.
*/
Researcher.prototype.StopResearching = function(id)
{
this.queue.get(id).Stop();
this.queue.delete(id);
};
/**
* @param {number} id - The id of the technology.
*/
Researcher.prototype.PauseTechnology = function(id)
{
this.queue.get(id).Pause();
};
/**
* @param {number} id - The id of the technology.
*/
Researcher.prototype.UnpauseTechnology = function(id)
{
this.queue.get(id).Unpause();
};
/**
* @param {number} id - The ID of the item to check.
* @return {boolean} - Whether we are currently training the item.
*/
Researcher.prototype.HasItem = function(id)
{
return this.queue.has(id);
};
/**
* @parameter {number} id - The id of the research.
* @return {Object} - Some basic information about the research.
*/
Researcher.prototype.GetResearchingTechnology = function(id)
{
return this.queue.get(id).GetBasicInfo();
};
/**
* @parameter {string} technologyName - The name of the research.
* @return {Object} - Some basic information about the research.
*/
Researcher.prototype.GetResearchingTechnologyByName = function(technologyName)
{
let techID;
for (const [id, value] of this.queue)
if (value.templateName === technologyName)
{
techID = id;
break;
}
if (!techID)
return undefined;
return this.GetResearchingTechnology(techID);
};
/**
* @param {number} id - The ID of the item we spent time on.
* @param {number} allocatedTime - The time we spent on the given item.
* @return {number} - The time we've actually used.
*/
Researcher.prototype.Progress = function(id, allocatedTime)
{
const item = this.queue.get(id);
const usedTime = item.Progress(allocatedTime);
if (item.finished)
this.queue.delete(id);
return usedTime;
};
Engine.RegisterComponentType(IID_Researcher, "Researcher", Researcher);

View file

@ -282,6 +282,17 @@ TechnologyManager.prototype.ResearchTechnology = function(tech)
TechnologyManager.prototype.QueuedResearch = function(tech, researcher)
{
this.researchQueued.set(tech, researcher);
const cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!cmpPlayer)
return;
const playerID = cmpPlayer.GetPlayerID();
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).CallEvent("OnResearchQueued", {
"playerid": playerID,
"technologyTemplate": tech,
"researcherEntity": researcher
});
};
// Marks a technology as actively being researched

View file

@ -0,0 +1,727 @@
function Trainer() {}
Trainer.prototype.Schema =
"<a:help>Allows the entity to train new units.</a:help>" +
"<a:example>" +
"<BatchTimeModifier>0.7</BatchTimeModifier>" +
"<Entities datatype='tokens'>" +
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
"</Entities>" +
"</a:example>" +
"<optional>" +
"<element name='BatchTimeModifier' a:help='Modifier that influences the time benefit for batch training. Defaults to 1, which means no benefit.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='Entities' a:help='Space-separated list of entity template names that this entity can train. The special string \"{civ}\" will be automatically replaced by the civ code of the entity&apos;s owner, while the string \"{native}\" will be automatically replaced by the entity&apos;s civ code.'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"</optional>";
/**
* This object represents a batch of entities being trained.
*/
Trainer.prototype.Item = function() {};
/**
* @param {string} templateName - The name of the template we ought to train.
* @param {number} count - The size of the batch to train.
* @param {number} trainer - The entity ID of our trainer.
* @param {string} metadata - Optionally any metadata to attach to us.
*/
Trainer.prototype.Item.prototype.Init = function(templateName, count, trainer, metadata)
{
this.count = count;
this.templateName = templateName;
this.trainer = trainer;
this.metadata = metadata;
};
/**
* Prepare for the queue.
* @param {Object} trainCostMultiplier - The multipliers to use when calculating costs.
* @param {number} batchTimeMultiplier - The factor to use when training this batches.
*
* @return {boolean} - Whether the item was successfully initiated.
*/
Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier)
{
if (!Number.isInteger(this.count) || this.count <= 0)
{
error("Invalid batch count " + this.count + ".");
return false;
}
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const template = cmpTemplateManager.GetTemplate(this.templateName);
if (!template)
return false;
const cmpPlayer = QueryOwnerInterface(this.trainer);
if (!cmpPlayer)
return false;
this.player = cmpPlayer.GetPlayerID();
this.resources = {};
const totalResources = {};
for (const res in template.Cost.Resources)
{
this.resources[res] = (trainCostMultiplier[res] === undefined ? 1 : trainCostMultiplier[res]) *
ApplyValueModificationsToTemplate(
"Cost/Resources/" + res,
+template.Cost.Resources[res],
this.player,
template);
totalResources[res] = Math.floor(this.count * this.resources[res]);
}
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(totalResources))
return false;
this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template);
if (template.TrainingRestrictions)
{
const unitCategory = template.TrainingRestrictions.Category;
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
{
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit))
// Already warned, return.
{
cmpPlayer.RefundResources(totalResources);
return false;
}
// ToDo: Should warn here v and return?
cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count);
}
}
const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template);
const time = batchTimeMultiplier * (trainCostMultiplier.time || 1) * buildTime * 1000;
this.timeRemaining = time;
this.timeTotal = time;
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnTrainingQueued", {
"playerid": this.player,
"unitTemplate": this.templateName,
"count": this.count,
"metadata": this.metadata,
"trainerEntity": this.trainer
});
return true;
};
/**
* Destroy cached entities, refund resources and free (population) limits.
*/
Trainer.prototype.Item.prototype.Stop = function()
{
// Destroy any cached entities (those which didn't spawn for some reason).
if (this.entities?.length)
{
for (const ent of this.entities)
Engine.DestroyEntity(ent);
delete this.entities;
}
const cmpPlayer = QueryPlayerIDInterface(this.player);
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const template = cmpTemplateManager.GetTemplate(this.templateName);
if (template.TrainingRestrictions)
{
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count);
}
const cmpStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
const totalCosts = {};
for (const resource in this.resources)
{
totalCosts[resource] = Math.floor(this.count * this.resources[resource]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
}
if (cmpPlayer)
{
if (this.started)
cmpPlayer.UnReservePopulationSlots(this.population * this.count);
cmpPlayer.RefundResources(totalCosts);
cmpPlayer.UnBlockTraining();
}
delete this.resources;
};
/**
* This starts the item, reserving population.
* @return {boolean} - Whether the item was started successfully.
*/
Trainer.prototype.Item.prototype.Start = function()
{
const cmpPlayer = QueryPlayerIDInterface(this.player);
if (!cmpPlayer)
return false;
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName);
this.population = ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
this.player,
template);
this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count);
if (this.missingPopSpace)
{
cmpPlayer.BlockTraining();
return false;
}
cmpPlayer.UnBlockTraining();
Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer });
this.started = true;
return true;
};
Trainer.prototype.Item.prototype.Finish = function()
{
this.Spawn();
if (!this.count)
this.finished = true;
};
/*
* This function creates the entities and places them in world if possible
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
*/
Trainer.prototype.Item.prototype.Spawn = function()
{
const createdEnts = [];
const 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.entities)
{
this.entities = [];
for (let i = 0; i < this.count; ++i)
this.entities.push(Engine.AddEntity(this.templateName));
}
let autoGarrison;
const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint);
if (cmpRallyPoint)
{
const data = cmpRallyPoint.GetData()[0];
if (data?.target && data.target == this.trainer && data.command == "garrison")
autoGarrison = true;
}
const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint);
const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position);
const positionTrainer = cmpPosition && cmpPosition.GetPosition();
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
while (this.entities.length)
{
const ent = this.entities[0];
const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
if (autoGarrison)
{
const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
{
// Temporary owner affectation needed for GarrisonHolder checks.
cmpNewOwnership.SetOwnerQuiet(this.player);
garrisoned = cmpGarrisonable.Garrison(this.trainer);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
}
if (!garrisoned)
{
const pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
const cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
if (positionTrainer)
cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos));
spawnedEnts.push(ent);
}
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
// i.e. we replace a 'trained' entity by 'alive' one.
// Must be done after spawn check so EntityLimits decrements only if unit spawns.
if (cmpPlayerEntityLimits)
{
const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
}
cmpNewOwnership.SetOwner(this.player);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
this.count--;
this.entities.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
ProcessCommand(this.player, com);
const cmpPlayer = QueryOwnerInterface(this.trainer);
if (createdEnts.length)
{
if (this.population)
cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
PlaySound("trained", createdEnts[0]);
Engine.PostMessage(this.trainer, MT_TrainingFinished, {
"entities": createdEnts,
"owner": this.player,
"metadata": this.metadata
});
}
if (this.count)
{
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Can't find free space to spawn trained units."),
"translateMessage": true
});
this.spawnNotified = true;
}
}
else
{
cmpPlayer.UnBlockTraining();
delete this.spawnNotified;
}
};
/**
* @param {number} allocatedTime - The time allocated to this item.
* @return {number} - The time used for this item.
*/
Trainer.prototype.Item.prototype.Progress = function(allocatedTime)
{
// We couldn't start this timeout, try again later.
if (!this.started && !this.Start())
return allocatedTime;
if (this.timeRemaining > allocatedTime)
{
this.timeRemaining -= allocatedTime;
return allocatedTime;
}
this.Finish();
return this.timeRemaining;
};
Trainer.prototype.Item.prototype.Pause = function()
{
this.paused = true;
};
Trainer.prototype.Item.prototype.Unpause = function()
{
delete this.paused;
};
/**
* @return {Object} - Some basic information of this batch.
*/
Trainer.prototype.Item.prototype.GetBasicInfo = function()
{
return {
"unitTemplate": this.templateName,
"count": this.count,
"neededSlots": this.missingPopSpace,
"progress": 1 - (this.timeRemaining / this.timeTotal),
"timeRemaining": this.timeRemaining,
"paused": this.paused,
"metadata": this.metadata
};
};
Trainer.prototype.Item.prototype.Serialize = function(id)
{
return {
"id": id,
"count": this.count,
"entities": this.entities,
"metadata": this.metadata,
"missingPopSpace": this.missingPopSpace,
"paused": this.paused,
"player": this.player,
"trainer": this.trainer,
"resource": this.resources,
"started": this.started,
"templateName": this.templateName,
"timeRemaining": this.timeRemaining,
"timeTotal": this.timeTotal,
};
};
Trainer.prototype.Item.prototype.Deserialize = function(data)
{
this.Init(data.templateName, data.count, data.trainer, data.metadata);
this.entities = data.entities;
this.missingPopSpace = data.missingPopSpace;
this.paused = data.paused;
this.player = data.player;
this.trainer = data.trainer;
this.resources = data.resources;
this.started = data.started;
this.timeRemaining = data.timeRemaining;
this.timeTotal = data.timeTotal;
};
Trainer.prototype.Init = function()
{
this.nextID = 1;
this.queue = new Map();
};
Trainer.prototype.Serialize = function()
{
const queue = [];
for (const [id, item] of this.queue)
queue.push(item.Serialize(id));
return {
"entitiesMap": this.entitiesMap,
"nextID": this.nextID,
"queue": queue
};
};
Trainer.prototype.Deserialize = function(data)
{
this.Init();
this.entitiesMap = data.entitiesMap;
this.nextID = data.nextID;
for (const item of data.queue)
{
const newItem = new this.Item();
newItem.Deserialize(item);
this.queue.set(item.id, newItem);
}
};
/*
* Returns list of entities that can be trained by this entity.
*/
Trainer.prototype.GetEntitiesList = function()
{
return Array.from(this.entitiesMap.values());
};
/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
Trainer.prototype.CalculateEntitiesMap = function()
{
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
if (!this.template.Entities)
return;
const string = this.template.Entities._string;
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const cmpPlayer = QueryOwnerInterface(this.entity);
const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
/**
* 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).
*/
const removeAllQueuedTemplate = (token) => {
const queue = clone(this.queue);
const template = this.entitiesMap.get(token);
for (const [id, item] of queue)
if (item.templateName == template)
this.StopBatch(id);
};
// ToDo: Notice this doesn't account for entity limits changing due to the template change.
const updateAllQueuedTemplate = (token, updateTo) => {
const template = this.entitiesMap.get(token);
for (const [id, item] of this.queue)
if (item.templateName === template)
item.templateName = updateTo;
};
const toks = string.split(/\s+/);
for (const tok of addedTokens)
toks.push(tok);
const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv();
const playerCiv = cmpPlayer?.GetCiv();
const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
const 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("Trainer/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 (nativeCiv)
token = token.replace(/\{native\}/g, nativeCiv);
if (playerCiv)
token = token.replace(/\{civ\}/g, playerCiv);
// 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.
*/
Trainer.prototype.GetUpgradedTemplate = function(templateName)
{
const cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
const requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
/**
* @return {Object} - The multipliers to change the costs of any training activity with.
*/
Trainer.prototype.GetTrainCostMultiplier = function()
{
const trainCostMultiplier = {};
for (const res in this.template.TrainCostMultiplier)
trainCostMultiplier[res] = ApplyValueModificationsToEntity(
"Trainer/TrainCostMultiplier/" + res,
+this.template.TrainCostMultiplier[res],
this.entity);
return trainCostMultiplier;
};
/*
* Returns batch build time.
*/
Trainer.prototype.GetBatchTime = function(batchSize)
{
// TODO: work out what equation we should use here.
return Math.pow(batchSize, ApplyValueModificationsToEntity(
"Trainer/BatchTimeModifier",
+(this.template.BatchTimeModifier || 1),
this.entity));
};
/**
* @param {string} templateName - The template name to check.
* @return {boolean} - Whether we can train this template.
*/
Trainer.prototype.CanTrain = function(templateName)
{
return this.GetEntitiesList().includes(templateName);
};
/**
* @param {string} templateName - The entity to queue.
* @param {number} count - The batch size.
* @param {string} metadata - Any metadata attached to the item.
*
* @return {number} - The ID of the item. -1 if the item could not be queued.
*/
Trainer.prototype.QueueBatch = function(templateName, count, metadata)
{
const item = new this.Item();
item.Init(templateName, count, this.entity, metadata);
const trainCostMultiplier = this.GetTrainCostMultiplier();
const batchTimeMultiplier = this.GetBatchTime(count);
if (!item.Queue(trainCostMultiplier, batchTimeMultiplier))
return -1;
const id = this.nextID++;
this.queue.set(id, item);
return id;
};
/**
* @param {number} id - The ID of the batch being trained here we need to stop.
*/
Trainer.prototype.StopBatch = function(id)
{
this.queue.get(id).Stop();
this.queue.delete(id);
};
/**
* @param {number} id - The ID of the training.
*/
Trainer.prototype.PauseBatch = function(id)
{
this.queue.get(id).Pause();
};
/**
* @param {number} id - The ID of the training.
*/
Trainer.prototype.UnpauseBatch = function(id)
{
this.queue.get(id).Unpause();
};
/**
* @param {number} id - The ID of the batch to check.
* @return {boolean} - Whether we are currently training the batch.
*/
Trainer.prototype.HasBatch = function(id)
{
return this.queue.has(id);
};
/**
* @parameter {number} id - The id of the training.
* @return {Object} - Some basic information about the training.
*/
Trainer.prototype.GetBatch = function(id)
{
const item = this.queue.get(id);
return item?.GetBasicInfo();
};
/**
* @param {number} id - The ID of the item we spent time on.
* @param {number} allocatedTime - The time we spent on the given item.
* @return {number} - The time we've actually used.
*/
Trainer.prototype.Progress = function(id, allocatedTime)
{
const item = this.queue.get(id);
const usedTime = item.Progress(allocatedTime);
if (item.finished)
this.queue.delete(id);
return usedTime;
};
Trainer.prototype.OnCivChanged = function()
{
this.CalculateEntitiesMap();
};
Trainer.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesMap();
};
Trainer.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" && (msg.component != "Trainer" ||
!msg.valueNames.some(val => val.startsWith("Trainer/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 training queue.
const cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
Trainer.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.CalculateEntitiesMap();
};
Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer);

View file

@ -5,15 +5,3 @@ Engine.RegisterInterface("ProductionQueue");
* sent from ProductionQueue component to the current entity whenever the training queue changes.
*/
Engine.RegisterMessageType("ProductionQueueChanged");
/**
* Message of the form { "entity": number }
* sent from ProductionQueue component to the current entity whenever a unit is about to be trained.
*/
Engine.RegisterMessageType("TrainingStarted");
/**
* Message of the form { "entities": number[], "owner": number, "metadata": object }
* sent from ProductionQueue component to the current entity whenever a unit has been trained.
*/
Engine.RegisterMessageType("TrainingFinished");

View file

@ -0,0 +1 @@
Engine.RegisterInterface("Researcher");

View file

@ -0,0 +1,13 @@
Engine.RegisterInterface("Trainer");
/**
* Message of the form { "entity": number }
* sent from Trainer component to the current entity whenever a unit is about to be trained.
*/
Engine.RegisterMessageType("TrainingStarted");
/**
* Message of the form { "entities": number[], "owner": number, "metadata": object }
* sent from Trainer component to the current entity whenever a unit has been trained.
*/
Engine.RegisterMessageType("TrainingFinished");

View file

@ -6,7 +6,6 @@ Engine.LoadComponentScript("interfaces/Barter.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/CeasefireManager.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/DeathDamage.js");
Engine.LoadComponentScript("interfaces/EndGameManager.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
@ -25,12 +24,15 @@ Engine.LoadComponentScript("interfaces/Population.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/Promotion.js");
Engine.LoadComponentScript("interfaces/Repairable.js");
Engine.LoadComponentScript("interfaces/Researcher.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
Engine.LoadComponentScript("interfaces/ResourceDropsite.js");
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
Engine.LoadComponentScript("interfaces/ResourceTrickle.js");
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Trader.js");
Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/TurretHolder.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/Treasure.js");

View file

@ -1,626 +1,83 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/ProductionQueue.js");
Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Researcher.js");
Engine.LoadComponentScript("interfaces/Timer.js");
Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/Upgrade.js");
Engine.LoadComponentScript("EntityLimits.js");
Engine.LoadComponentScript("Timer.js");
Engine.RegisterGlobal("Resources", {
"BuildSchema": (a, b) => {}
});
Engine.LoadComponentScript("ProductionQueue.js");
Engine.LoadComponentScript("TrainingRestrictions.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
const playerEnt = 2;
const playerID = 1;
const testEntity = 3;
function testEntitiesList()
{
Engine.RegisterGlobal("TechnologyTemplates", {
"Has": name => name == "phase_town_athen" || name == "phase_city_athen",
"Get": () => ({})
});
AddMock(SYSTEM_ENTITY, IID_Timer, {
"CancelTimer": (id) => {},
"SetInterval": (ent, iid, func) => 1
});
const productionQueueId = 6;
const playerId = 1;
const playerEntityID = 2;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEnt
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
AddMock(playerEnt, IID_Player, {
"GetPlayerID": () => playerID
});
let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", {
"Entities": { "_string": "units/{civ}/cavalry_javelineer_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(testEntity, IID_Ownership, {
"GetOwner": () => playerID
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEntityID
});
AddMock(testEntity, IID_Trainer, {
"GetBatch": (id) => ({}),
"HasBatch": (id) => false, // Assume we've finished.
"Progress": (time) => time,
"QueueBatch": () => 1,
"StopBatch": (id) => {}
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetDisabledTemplates": () => ({}),
"GetPlayerID": () => playerId
});
const cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", null);
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": () => false
});
AddMock(productionQueueId, IID_Ownership, {
"GetOwner": () => playerId
});
// Test autoqueue.
cmpProdQueue.EnableAutoQueue();
AddMock(productionQueueId, IID_Identity, {
"GetCiv": () => "iber"
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
AddMock(productionQueueId, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProdQueue.RemoveItem(cmpProdQueue.nextID -1);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
cmpProductionQueue.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpProductionQueue.GetEntitiesList(),
["units/iber/cavalry_javelineer_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"]
);
cmpProdQueue.DisableAutoQueue();
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"]);
// Test items which don't use all the time.
AddMock(testEntity, IID_Trainer, {
"GetBatch": (id) => ({}),
"HasBatch": (id) => false, // Assume we've finished.
"PauseBatch": (id) => {},
"Progress": (time) => time - 250,
"QueueBatch": () => 1,
"StopBatch": (id) => {},
"UnpauseBatch": (id) => {}
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
cmpProdQueue.AddItem("some_template", "unit", 2);
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
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_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
// Test pushing an item to the front.
cmpProdQueue.AddItem("some_template", "unit", 2);
cmpProdQueue.AddItem("some_template", "unit", 3, null, true);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue()[0].id, cmpProdQueue.nextID - 1);
TS_ASSERT(cmpProdQueue.GetQueue()[1].paused);
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_javelineer_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_javelineer_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
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, {
"SetInterval": (ent, iid, func) => 1,
"CancelTimer": (id) => {}
});
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({
"Cost": {
"BuildTime": 0,
"Population": 1,
"Resources": {}
},
"TrainingRestrictions": {
"Category": "some_limit",
"MatchLimit": "7"
}
})
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEnt
});
AddMock(playerEnt, IID_Player, {
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"GetTimeMultiplier": () => 0,
"BlockTraining": () => {},
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"AddResources": () => 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));
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5, "some_template", 8));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 10, "some_template", 8));
// Check that the entity limits do get updated if the spawn succeeds.
AddMock(testEntity, IID_Footprint, {
"PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 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 })
});
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 6);
// Check that when the batch is removed the counts are subtracted again.
cmpProdQueue.RemoveItem(cmpProdQueue.GetQueue()[0].id);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3);
}
function test_batch_adding()
{
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
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, {
"SetInterval": (ent, iid, func) => 1
});
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
});
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => true
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
AddMock(testEntity, IID_Upgrade, {
"IsUpgrading": () => false
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
}
function test_batch_removal()
{
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
ConstructComponent(playerEnt, "EntityLimits", {
"Limits": {
"some_limit": 8
},
"LimitChangers": {},
"LimitRemovers": {}
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {}
});
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {}
});
let cmpTimer = ConstructComponent(SYSTEM_ENTITY, "Timer", null);
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
});
let cmpPlayer = AddMock(playerEnt, IID_Player, {
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"GetTimeMultiplier": () => 0,
"BlockTraining": () => {},
"UnBlockTraining": () => {},
"UnReservePopulationSlots": () => {},
"TrySubtractResources": () => true,
"AddResources": () => {},
"TryReservePopulationSlots": () => 1
});
let cmpPlayerBlockSpy = new Spy(cmpPlayer, "BlockTraining");
let cmpPlayerUnblockSpy = new Spy(cmpPlayer, "UnBlockTraining");
AddMock(testEntity, IID_Ownership, {
"GetOwner": () => playerID
});
let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", {
"Entities": { "_string": "some_template" },
"BatchTimeModifier": 1
});
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerBlockSpy._called, 1);
cmpProdQueue.AddItem("some_template", "unit", 2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
cmpProdQueue.RemoveItem(1);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 1);
cmpProdQueue.RemoveItem(2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 2);
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpProdQueue.AddItem("some_template", "unit", 3);
cmpPlayer.TryReservePopulationSlots = () => false;
cmpProdQueue.RemoveItem(3);
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 3);
cmpTimer.OnUpdate({ "turnLength": 1 });
TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 4);
}
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": () => [],
"TryReservePopulationSlots": () => false, // Always have pop space.
"TrySubtractResources": () => true,
"UnBlockTraining": () => {},
"AddResources": () => {},
"GetPlayerID": () => 1,
// entitylimits
"ChangeCount": () => {},
"AllowedToTrain": () => true,
// 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.AddItem("units/test/a", "unit", 1, {});
cmpProductionQueue.AddItem("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);
}
function test_auto_queue()
{
let playerEnt = 2;
let playerID = 1;
let testEntity = 3;
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, {
"SetInterval": (ent, iid, func) => 1
});
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
});
cmpProdQueue.EnableAutoQueue();
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
}
testEntitiesList();
regression_test_d1879();
test_batch_adding();
test_batch_removal();
test_auto_queue();
test_token_changes();
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0);

View file

@ -0,0 +1,153 @@
Engine.RegisterGlobal("Resources", {
"BuildSchema": (a, b) => {}
});
Engine.LoadHelperScript("Player.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("interfaces/Researcher.js");
Engine.LoadComponentScript("Researcher.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
const playerID = 1;
const playerEntityID = 11;
const entityID = 21;
Engine.RegisterGlobal("TechnologyTemplates", {
"Has": name => name == "phase_town_athen" || name == "phase_city_athen",
"Get": () => ({})
});
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEntityID
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({})
});
AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": () => false
});
AddMock(entityID, IID_Ownership, {
"GetOwner": () => playerID
});
AddMock(entityID, IID_Identity, {
"GetCiv": () => "iber"
});
const cmpResearcher = ConstructComponent(entityID, "Researcher", {
"Technologies": { "_string": "gather_fishing_net " +
"phase_town_{civ} " +
"phase_city_{civ}" }
});
TS_ASSERT_UNEVAL_EQUALS(
cmpResearcher.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "athen",
"GetDisabledTechnologies": () => ({ "gather_fishing_net": true })
});
TS_ASSERT_UNEVAL_EQUALS(cmpResearcher.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(cmpResearcher.GetTechnologiesList(), [undefined, "phase_city_athen"]);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({})
});
TS_ASSERT_UNEVAL_EQUALS(
cmpResearcher.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic"]
);
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value + " some_test");
TS_ASSERT_UNEVAL_EQUALS(
cmpResearcher.GetTechnologiesList(),
["gather_fishing_net", "phase_town_generic", "phase_city_generic", "some_test"]
);
// Test Queuing a tech.
const queuedTech = "gather_fishing_net";
const cost = {
"food": 10
};
Engine.RegisterGlobal("TechnologyTemplates", {
"Has": () => true,
"Get": () => ({
"cost": cost,
"researchTime": 1
})
});
const cmpPlayer = AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTechnologies": () => ({}),
"GetPlayerID": () => playerID,
"TrySubtractResources": (resources) => {
TS_ASSERT_UNEVAL_EQUALS(resources, cost);
// Just have enough resources.
return true;
},
"RefundResources": (resources) => {
TS_ASSERT_UNEVAL_EQUALS(resources, cost);
},
});
let spyCmpPlayer = new Spy(cmpPlayer, "TrySubtractResources");
const techManager = AddMock(playerEntityID, IID_TechnologyManager, {
"CheckTechnologyRequirements": () => true,
"IsInProgress": () => false,
"IsTechnologyResearched": () => false,
"QueuedResearch": (templateName, researcher) => {
TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
TS_ASSERT_UNEVAL_EQUALS(researcher, entityID);
},
"StoppedResearch": (templateName, _) => {
TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
},
"StartedResearch": (templateName, _) => {
TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
},
"ResearchTechnology": (templateName, _) => {
TS_ASSERT_UNEVAL_EQUALS(templateName, queuedTech);
}
});
let spyTechManager = new Spy(techManager, "QueuedResearch");
let id = cmpResearcher.QueueTechnology(queuedTech);
TS_ASSERT_EQUALS(spyTechManager._called, 1);
TS_ASSERT_EQUALS(spyCmpPlayer._called, 1);
TS_ASSERT_EQUALS(cmpResearcher.queue.size, 1);
// Test removing a queued tech.
spyCmpPlayer = new Spy(cmpPlayer, "RefundResources");
spyTechManager = new Spy(techManager, "StoppedResearch");
cmpResearcher.StopResearching(id);
TS_ASSERT_EQUALS(spyTechManager._called, 1);
TS_ASSERT_EQUALS(spyCmpPlayer._called, 1);
TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0);
// Test finishing a queued tech.
id = cmpResearcher.QueueTechnology(queuedTech);
TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0);
TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 500), 500);
TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0.5);
spyTechManager = new Spy(techManager, "ResearchTechnology");
TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 1000), 500);
TS_ASSERT_EQUALS(spyTechManager._called, 1);
TS_ASSERT_EQUALS(cmpResearcher.queue.size, 0);

View file

@ -0,0 +1,301 @@
Engine.RegisterGlobal("Resources", {
"BuildSchema": (a, b) => {}
});
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/BuildRestrictions.js");
Engine.LoadComponentScript("interfaces/EntityLimits.js");
Engine.LoadComponentScript("interfaces/Foundation.js");
Engine.LoadComponentScript("interfaces/StatisticsTracker.js");
Engine.LoadComponentScript("interfaces/Trainer.js");
Engine.LoadComponentScript("interfaces/TrainingRestrictions.js");
Engine.LoadComponentScript("interfaces/Trigger.js");
Engine.LoadComponentScript("EntityLimits.js");
Engine.LoadComponentScript("Trainer.js");
Engine.LoadComponentScript("TrainingRestrictions.js");
Engine.RegisterGlobal("ApplyValueModificationsToEntity", (_, value) => value);
Engine.RegisterGlobal("ApplyValueModificationsToTemplate", (_, value) => value);
const playerID = 1;
const playerEntityID = 11;
const entityID = 21;
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
const cmpTrainer = ConstructComponent(entityID, "Trainer", {
"Entities": { "_string": "units/{civ}/cavalry_javelineer_b " +
"units/{civ}/infantry_swordsman_b " +
"units/{native}/support_female_citizen" }
});
cmpTrainer.GetUpgradedTemplate = (template) => template;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
"GetPlayerByID": id => playerEntityID
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTemplates": () => ({}),
"GetPlayerID": () => playerID
});
AddMock(entityID, IID_Ownership, {
"GetOwner": () => playerID
});
AddMock(entityID, IID_Identity, {
"GetCiv": () => "iber"
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": name => name == "units/iber/support_female_citizen",
"GetTemplate": name => ({})
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(cmpTrainer.GetEntitiesList(), ["units/iber/support_female_citizen"]);
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({})
});
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
"GetPlayerID": () => playerID
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }),
"GetPlayerID": () => playerID
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "athen",
"GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }),
"GetPlayerID": () => playerID
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(),
["units/athen/cavalry_javelineer_b", "units/iber/support_female_citizen"]
);
AddMock(playerEntityID, IID_Player, {
"GetCiv": () => "iber",
"GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": false }),
"GetPlayerID": () => playerID
});
cmpTrainer.CalculateEntitiesMap();
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(),
["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"]
);
// Test Queuing a unit.
const queuedUnit = "units/iber/infantry_swordsman_b";
const cost = {
"food": 10
};
AddMock(SYSTEM_ENTITY, IID_TemplateManager, {
"TemplateExists": () => true,
"GetTemplate": name => ({
"Cost": {
"BuildTime": 1,
"Population": 1,
"Resources": cost
},
"TrainingRestrictions": {
"Category": "some_limit",
"MatchLimit": "7"
}
})
});
AddMock(SYSTEM_ENTITY, IID_Trigger, {
"CallEvent": () => {}
});
AddMock(SYSTEM_ENTITY, IID_GuiInterface, {
"PushNotification": () => {},
"SetSelectionDirty": () => {}
});
const cmpPlayer = AddMock(playerEntityID, IID_Player, {
"BlockTraining": () => {},
"GetCiv": () => "iber",
"GetPlayerID": () => playerID,
"RefundResources": (resources) => {
TS_ASSERT_UNEVAL_EQUALS(resources, cost);
},
"TrySubtractResources": (resources) => {
TS_ASSERT_UNEVAL_EQUALS(resources, cost);
// Just have enough resources.
return true;
},
"TryReservePopulationSlots": () => false, // Always have pop space.
"UnReservePopulationSlots": () => {}, // Always have pop space.
"UnBlockTraining": () => {},
"GetDisabledTemplates": () => ({})
});
const spyCmpPlayerSubtract = new Spy(cmpPlayer, "TrySubtractResources");
const spyCmpPlayerRefund = new Spy(cmpPlayer, "RefundResources");
const spyCmpPlayerPop = new Spy(cmpPlayer, "TryReservePopulationSlots");
ConstructComponent(playerEntityID, "EntityLimits", {
"Limits": {
"some_limit": 0
},
"LimitChangers": {},
"LimitRemovers": {}
});
// Test that we can't exceed the entity limit.
TS_ASSERT_EQUALS(cmpTrainer.QueueBatch(queuedUnit, 1), -1);
// And that in that case, the resources are not lost.
// ToDo: This is a bad test, it relies on the order of subtraction in the cmp.
// Better would it be to check the states before and after the queue.
TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, spyCmpPlayerRefund._called);
ConstructComponent(playerEntityID, "EntityLimits", {
"Limits": {
"some_limit": 5
},
"LimitChangers": {},
"LimitRemovers": {}
});
let id = cmpTrainer.QueueBatch(queuedUnit, 1);
TS_ASSERT_EQUALS(spyCmpPlayerSubtract._called, 2);
TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1);
// Test removing a queued batch.
cmpTrainer.StopBatch(id);
TS_ASSERT_EQUALS(spyCmpPlayerRefund._called, 2);
TS_ASSERT_EQUALS(cmpTrainer.queue.size, 0);
const cmpEntLimits = QueryOwnerInterface(entityID, IID_EntityLimits);
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 5));
// Test finishing a queued batch.
id = cmpTrainer.QueueBatch(queuedUnit, 1);
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4));
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0);
TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 500), 500);
TS_ASSERT_EQUALS(spyCmpPlayerPop._called, 1);
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id).progress, 0.5);
const spawedEntityIDs = [4, 5, 6, 7, 8];
let spawned = 0;
Engine.AddEntity = () => {
const ent = spawedEntityIDs[spawned++];
ConstructComponent(ent, "TrainingRestrictions", {
"Category": "some_limit"
});
AddMock(ent, IID_Identity, {
"GetClassesList": () => []
});
AddMock(ent, IID_Position, {
"JumpTo": () => {}
});
AddMock(ent, IID_Ownership, {
"SetOwner": (pid) => {
QueryOwnerInterface(ent, IID_EntityLimits).OnGlobalOwnershipChanged({
"entity": ent,
"from": -1,
"to": pid
});
},
"GetOwner": () => playerID
});
return ent;
};
AddMock(entityID, IID_Footprint, {
"PickSpawnPoint": () => ({ "x": 0, "y": 1, "z": 0 })
});
TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 1000), 500);
TS_ASSERT(!cmpTrainer.HasBatch(id));
TS_ASSERT(!cmpEntLimits.AllowedToTrain("some_limit", 5));
TS_ASSERT(cmpEntLimits.AllowedToTrain("some_limit", 4));
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1);
// Now check that it doesn't get updated when the spawn doesn't succeed. (regression_test_d1879)
cmpPlayer.TrySubtractResources = () => true;
cmpPlayer.RefundResources = () => {};
AddMock(entityID, IID_Footprint, {
"PickSpawnPoint": () => ({ "x": -1, "y": -1, "z": -1 })
});
id = cmpTrainer.QueueBatch(queuedUnit, 2);
TS_ASSERT_EQUALS(cmpTrainer.Progress(id, 2000), 2000);
TS_ASSERT(cmpTrainer.HasBatch(id));
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 3);
// Check that when the batch is removed the counts are subtracted again.
cmpTrainer.StopBatch(id);
TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 1);
TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts()["units/iber/infantry_swordsman_b"], 1);
const queuedSecondUnit = "units/iber/cavalry_javelineer_b";
// Check changing the allowed entities has effect.
const id1 = cmpTrainer.QueueBatch(queuedUnit, 1);
const id2 = cmpTrainer.QueueBatch(queuedSecondUnit, 1);
TS_ASSERT_EQUALS(cmpTrainer.queue.size, 2);
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1).unitTemplate, queuedUnit);
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, queuedSecondUnit);
// 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}/cavalry_javelineer_b>units/{civ}/c units/{civ}/d -units/{civ}/infantry_swordsman_b");
});
cmpTrainer.OnValueModification({
"component": "Trainer",
"valueNames": ["Trainer/Entities/_string"],
"entities": [entityID]
});
TS_ASSERT_UNEVAL_EQUALS(
cmpTrainer.GetEntitiesList(), ["units/iber/c", "units/iber/support_female_citizen", "units/iber/d"]
);
TS_ASSERT_EQUALS(cmpTrainer.queue.size, 1);
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id1), undefined);
TS_ASSERT_EQUALS(cmpTrainer.GetBatch(id2).unitTemplate, "units/iber/c");

View file

@ -2,11 +2,11 @@
"type": "global",
"affects": ["Structure"],
"modifications": [
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 }
],
"auraDescription": "Structures 15% technology resource costs and research time.",
"auraName": "Centre of Scholarship"

View file

@ -3,11 +3,11 @@
"affects": ["Forge"],
"affectedPlayers": ["MutualAlly"],
"modifications": [
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.85 },
{ "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.85 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.85 },
{ "value": "Researcher/TechCostMultiplier/time", "multiply": 0.85 }
],
"auraName": "Products from Gaul",
"auraDescription": "Forges 15% technology resource costs and research time."

View file

@ -6,11 +6,11 @@
{ "value": "Cost/BuildTime", "multiply": 0.5 },
{ "value": "Cost/Resources/wood", "multiply": 0.5 },
{ "value": "Cost/Resources/stone", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraName": "Ashoka's Religious Support",
"auraDescription": "Temples 50% resource costs and building time; Temple technologies 50% resource costs and research time."

View file

@ -2,10 +2,10 @@
"type": "global",
"affects": ["Economic"],
"modifications": [
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Economic Fortune",
"auraDescription": "Solon brought in a new system of weights and measures, fathers were encouraged to find trades for their sons.\nEconomic technologies 10% resource costs."

View file

@ -2,10 +2,10 @@
"type": "global",
"affects": ["Structure"],
"modifications": [
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Great Librarian",
"auraDescription": "Continuing his predecessors' work on the Great Library of Alexandria, he seized every book brought to the city, thus leaving to his people a vast amount of hoarded wisdom.\nStructure technologies 10% resource costs."

View file

@ -6,10 +6,10 @@
{ "value": "Cost/Resources/wood", "multiply": 0.9 },
{ "value": "Cost/Resources/stone", "multiply": 0.9 },
{ "value": "Cost/Resources/metal", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.9 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.9 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.9 }
],
"auraName": "Founder of the Ezida Temple",
"auraDescription": "Antiochus I laid the foundation for the Ezida Temple in Borsippa.\nTemples 10% resource costs; Temple technologies 10% resource costs."

View file

@ -3,7 +3,7 @@
"affects": ["Ship"],
"affectedPlayers": ["MutualAlly"],
"modifications": [
{ "value": "ProductionQueue/BatchTimeModifier", "multiply": 0.7 },
{ "value": "Trainer/BatchTimeModifier", "multiply": 0.7 },
{ "value": "UnitMotion/WalkSpeed", "multiply": 1.5 }
],
"auraName": "Naval Commander",

View file

@ -5,11 +5,11 @@
{ "value": "Cost/BuildTime", "multiply": 0.5 },
{ "value": "Cost/Resources/wood", "multiply": 0.5 },
{ "value": "Cost/Resources/stone", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.5 },
{ "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.5 },
{ "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraDescription": "Temples 50% resource costs and build time. Temple technologies 50% resource costs and research time.",
"auraName": "Buddhism",

View file

@ -2,11 +2,11 @@
"type": "garrison",
"affects": ["Structure"],
"modifications": [
{ "value": "ProductionQueue/TechCostMultiplier/food", "multiply": 0.8 },
{ "value": "ProductionQueue/TechCostMultiplier/wood", "multiply": 0.8 },
{ "value": "ProductionQueue/TechCostMultiplier/stone", "multiply": 0.8 },
{ "value": "ProductionQueue/TechCostMultiplier/metal", "multiply": 0.8 },
{ "value": "ProductionQueue/TechCostMultiplier/time", "multiply": 0.5 }
{ "value": "Researcher/TechCostMultiplier/food", "multiply": 0.8 },
{ "value": "Researcher/TechCostMultiplier/wood", "multiply": 0.8 },
{ "value": "Researcher/TechCostMultiplier/stone", "multiply": 0.8 },
{ "value": "Researcher/TechCostMultiplier/metal", "multiply": 0.8 },
{ "value": "Researcher/TechCostMultiplier/time", "multiply": 0.5 }
],
"auraDescription": "When garrisoned, the Structure's technologies have 20% resource cost and 50% research time.",
"auraName": "Teacher",

View file

@ -10,7 +10,7 @@
"researchTime": 40,
"tooltip": "Barracks 10% batch training time.",
"modifications": [
{ "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 }
{ "value": "Trainer/BatchTimeModifier", "add": -0.1 }
],
"affects": ["Barracks"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"

View file

@ -1,29 +1,29 @@
{
"genericName": "Hoplite Tradition",
"description": "Hoplite soldiers constituted most of the armies of Greece.",
"cost": {
"food": 400,
"metal": 300
},
"requirements": {
"all": [
{ "tech": "phase_town" },
{
"any": [
{ "civ": "athen" },
{ "civ": "spart" }
]
}
]
},
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "armor_corinthian.png",
"researchTime": 60,
"tooltip": "Hoplites 25% training time and 50% promotion experience.",
"modifications": [
{ "value": "Cost/BuildTime", "multiply": 0.75 },
{ "value": "Promotion/RequiredXp", "multiply": 0.5 }
],
"affects": ["Infantry Spearman !Hero"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}
{
"genericName": "Hoplite Tradition",
"description": "Hoplite soldiers constituted most of the armies of Greece.",
"cost": {
"food": 400,
"metal": 300
},
"requirements": {
"all": [
{ "tech": "phase_town" },
{
"any": [
{ "civ": "athen" },
{ "civ": "spart" }
]
}
]
},
"requirementsTooltip": "Unlocked in Town Phase.",
"icon": "armor_corinthian.png",
"researchTime": 60,
"tooltip": "Hoplites 25% training time and 50% promotion experience.",
"modifications": [
{ "value": "Cost/BuildTime", "multiply": 0.75 },
{ "value": "Promotion/RequiredXp", "multiply": 0.5 }
],
"affects": ["Infantry Spearman !Hero"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"
}

View file

@ -10,7 +10,7 @@
"researchTime": 40,
"tooltip": "Stables 10% batch training time.",
"modifications": [
{ "value": "ProductionQueue/BatchTimeModifier", "add": -0.1 }
{ "value": "Trainer/BatchTimeModifier", "add": -0.1 }
],
"affects": ["Stable"],
"soundComplete": "interface/alarm/alarm_upgradearmory.xml"

View file

@ -58,8 +58,9 @@ function Cheat(input)
cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat)."));
return;
case "createunits":
var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
if (!cmpProductionQueue)
{
const cmpTrainer = input.selected.length && Engine.QueryInterface(input.selected[0], IID_Trainer);
if (!cmpTrainer)
{
cmpGuiInterface.PushNotification({
"type": "text",
@ -71,19 +72,18 @@ function Cheat(input)
}
let owner = input.player;
let cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership);
const cmpOwnership = Engine.QueryInterface(input.selected[0], IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
for (let i = 0; i < Math.min(input.parameter, cmpPlayer.GetMaxPopulation() - cmpPlayer.GetPopulationCount()); ++i)
cmpProductionQueue.SpawnUnits({
"player": owner,
"metadata": null,
"entity": {
"template": input.templates[i % input.templates.length],
"count": 1
}
});
{
const batch = new cmpTrainer.Item(input.templates[i % input.templates.length], 1, input.selected[0], null);
batch.player = owner;
batch.Finish();
// ToDo: If not able to spawn, cancel the batch.
}
return;
}
case "fastactions":
{
let cmpModifiersManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModifiersManager);
@ -95,7 +95,7 @@ function Cheat(input)
"ResourceGatherer/BaseSpeed": [{ "affects": [["Structure"], ["Unit"]], "multiply": 1000 }],
"Pack/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }],
"Upgrade/Time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }],
"ProductionQueue/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }]
"Researcher/TechCostMultiplier/time": [{ "affects": [["Structure"], ["Unit"]], "multiply": 0.01 }]
}, playerEnt);
return;
}
@ -121,6 +121,7 @@ function Cheat(input)
Cheat({ "player": input.player, "action": "researchTechnology", "parameter": parameter, "selected": input.selected });
return;
case "researchTechnology":
{
if (!input.parameter.length)
return;
@ -132,8 +133,8 @@ function Cheat(input)
// check, if building is selected
if (input.selected[0])
{
var cmpProductionQueue = Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
if (cmpProductionQueue)
const cmpResearcher = Engine.QueryInterface(input.selected[0], IID_Researcher);
if (cmpResearcher)
{
// try to spilt the input
var tmp = input.parameter.split(/\s+/);
@ -144,7 +145,7 @@ function Cheat(input)
if (number || number === 0)
{
// get name of tech
var techs = cmpProductionQueue.GetTechnologiesList();
const techs = cmpResearcher.GetTechnologiesList();
if (number > 0 && number <= techs.length)
{
var tech = techs[number-1];
@ -167,6 +168,7 @@ function Cheat(input)
!cmpTechnologyManager.IsTechnologyResearched(techname))
cmpTechnologyManager.ResearchTechnology(techname);
return;
}
case "metaCheat":
for (let resource of Resources.GetCodes())
Cheat({ "player": input.player, "action": "addresource", "text": resource, "parameter": input.parameter });

View file

@ -353,26 +353,19 @@ var g_Commands = {
continue;
}
var queue = Engine.QueryInterface(ent, IID_ProductionQueue);
const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
if (!cmpTrainer)
continue;
let templateName = cmd.template;
// Check if the building can train the unit
// TODO: the AI API does not take promotion technologies into account for the list
// of trainable units (taken directly from the unit template). Here is a temporary fix.
if (queue && data.cmpPlayer.IsAI())
{
var list = queue.GetEntitiesList();
if (list.indexOf(cmd.template) === -1 && cmd.promoted)
{
for (var promoted of cmd.promoted)
{
if (list.indexOf(promoted) === -1)
continue;
cmd.template = promoted;
break;
}
}
}
if (queue && queue.GetEntitiesList().indexOf(cmd.template) != -1)
queue.AddItem(cmd.template, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
if (data.cmpPlayer.IsAI())
templateName = cmpTrainer.GetUpgradedTemplate(cmd.template);
if (cmpTrainer.CanTrain(templateName))
Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
}
},

View file

@ -26,18 +26,18 @@
<Obstruction>
<Static width="48.0" depth="48.0"/>
</Obstruction>
<ProductionQueue>
<Entities datatype="tokens">
-units/{civ}/support_female_citizen
campaigns/army_mace_hero_alexander
campaigns/army_mace_standard
</Entities>
</ProductionQueue>
<TerritoryInfluence>
<Root>true</Root>
<Radius>150</Radius>
<Weight>35000</Weight>
</TerritoryInfluence>
<Trainer>
<Entities datatype="tokens">
-units/{civ}/support_female_citizen
campaigns/army_mace_hero_alexander
campaigns/army_mace_standard
</Entities>
</Trainer>
<VisualActor>
<Actor>campaigns/structures/hellenes/settlement_curtainwall.xml</Actor>
</VisualActor>

View file

@ -26,19 +26,19 @@
<Obstruction>
<Static width="48.0" depth="48.0"/>
</Obstruction>
<ProductionQueue>
<TerritoryInfluence>
<Root>true</Root>
<Radius>300</Radius>
<Weight>35000</Weight>
</TerritoryInfluence>
<Trainer>
<Entities datatype="tokens">
-units/{civ}/support_female_citizen
campaigns/army_mace_hero_alexander
campaigns/army_mace_standard
units/{civ}/support_trader
</Entities>
</ProductionQueue>
<TerritoryInfluence>
<Root>true</Root>
<Radius>300</Radius>
<Weight>35000</Weight>
</TerritoryInfluence>
</Trainer>
<VisualActor>
<Actor>campaigns/structures/hellenes/settlement_curtainwall.xml</Actor>
</VisualActor>

View file

@ -16,11 +16,13 @@
<Static width="17.5" depth="30.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<TerritoryInfluence>
<Root>true</Root>
<Radius>100</Radius>
<Weight>65535</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/temple.xml</Actor>
</VisualActor>

View file

@ -1,43 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<BuildRestrictions>
<Territory>own neutral ally</Territory>
</BuildRestrictions>
<Footprint>
<Square width="24.0" depth="25.0"/>
<Height>8.0</Height>
</Footprint>
<Health disable=""/>
<Identity>
<VisibleClasses datatype="tokens">
Shrine
</VisibleClasses>
<Undeletable>true</Undeletable>
</Identity>
<Obstruction>
<Static width="20" depth="21"/>
</Obstruction>
<ProductionQueue>
<Entities datatype="tokens">
-units/{civ}/support_healer_b
units/{native}/support_healer_e
</Entities>
<Technologies datatype="tokens">
-heal_range
-heal_range_2
-heal_rate
-heal_rate_2
-garrison_heal
-health_regen_units
</Technologies>
</ProductionQueue>
<StatusBars>
<HeightOffset>15.0</HeightOffset>
</StatusBars>
<Resistance replace=""/>
<TerritoryDecay disable=""/>
<TerritoryInfluence disable=""/>
<VisualActor>
<FoundationActor>structures/fndn_4x4.xml</FoundationActor>
</VisualActor>
</Entity>
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<BuildRestrictions>
<Territory>own neutral ally</Territory>
</BuildRestrictions>
<Footprint>
<Square width="24.0" depth="25.0"/>
<Height>8.0</Height>
</Footprint>
<Health disable=""/>
<Identity>
<VisibleClasses datatype="tokens">
Shrine
</VisibleClasses>
<Undeletable>true</Undeletable>
</Identity>
<Obstruction>
<Static width="20" depth="21"/>
</Obstruction>
<Researcher>
<Technologies datatype="tokens">
-heal_range
-heal_range_2
-heal_rate
-heal_rate_2
-garrison_heal
-health_regen_units
</Technologies>
</Researcher>
<Resistance replace=""/>
<StatusBars>
<HeightOffset>15.0</HeightOffset>
</StatusBars>
<TerritoryDecay disable=""/>
<TerritoryInfluence disable=""/>
<Trainer>
<Entities datatype="tokens">
-units/{civ}/support_healer_b
units/{native}/support_healer_e
</Entities>
</Trainer>
<VisualActor>
<FoundationActor>structures/fndn_4x4.xml</FoundationActor>
</VisualActor>
</Entity>

View file

@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<Capturable>
<CapturePoints>500</CapturePoints>
</Capturable>
<Health disable=""/>
<Identity>
<GenericName>Trading Post</GenericName>
<Undeletable>true</Undeletable>
</Identity>
<ProductionQueue>
<Technologies datatype="tokens">
-trader_health
-trade_gain_01
-trade_gain_02
-trade_commercial_treaty
</Technologies>
</ProductionQueue>
<Resistance replace=""/>
<TerritoryDecay disable=""/>
<TerritoryInfluence disable=""/>
<VisualActor>
<FoundationActor>structures/fndn_5x5.xml</FoundationActor>
</VisualActor>
</Entity>
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<Capturable>
<CapturePoints>500</CapturePoints>
</Capturable>
<Health disable=""/>
<Identity>
<GenericName>Trading Post</GenericName>
<Undeletable>true</Undeletable>
</Identity>
<Researcher>
<Technologies datatype="tokens">
-trader_health
-trade_gain_01
-trade_gain_02
-trade_commercial_treaty
</Technologies>
</Researcher>
<Resistance replace=""/>
<TerritoryDecay disable=""/>
<TerritoryInfluence disable=""/>
<VisualActor>
<FoundationActor>structures/fndn_5x5.xml</FoundationActor>
</VisualActor>
</Entity>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/arsenal</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/workshop.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/barracks</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/athenians/barracks.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/civil_centre</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/athenians/civil_centre.xml</Actor>
</VisualActor>

View file

@ -10,9 +10,11 @@
<Static width="18.0" depth="20.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/corral</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/corral.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/dock</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/athenians/dock.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/fortress</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/athenians/fortress.xml</Actor>
</VisualActor>

View file

@ -5,7 +5,9 @@
<Tooltip>Changes in a 10-pop house for civilisations with those houses, is deleted for other civs</Tooltip>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer/>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/house.xml</Actor>
</VisualActor>

View file

@ -11,7 +11,9 @@
<Static width="13.0" depth="12.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer/>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/ptolemies/house.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/market</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/market.xml</Actor>
</VisualActor>

View file

@ -10,9 +10,11 @@
<Static width="26.0" depth="26.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/range</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/range.xml</Actor>
</VisualActor>

View file

@ -10,9 +10,11 @@
<Static width="23.0" depth="23.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/stable</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/stable.xml</Actor>
</VisualActor>

View file

@ -4,9 +4,11 @@
<Civ>skirm</Civ>
</Identity>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/temple</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/athenians/temple.xml</Actor>
</VisualActor>

View file

@ -10,9 +10,11 @@
<Static width="59.0" depth="63.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<SkirmishReplacer>
<general>structures/{civ}/wonder</general>
</SkirmishReplacer>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/hellenes/temple_epic.xml</Actor>
</VisualActor>

View file

@ -7,13 +7,13 @@
<Civ>athen</Civ>
<SpecificName>Agora</SpecificName>
</Identity>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_javelineer_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/athenians/civil_centre.xml</Actor>
</VisualActor>

View file

@ -14,13 +14,13 @@
<Obstruction>
<Static width="22.0" depth="26.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/infantry_marine_archer_b
units/{civ}/champion_marine
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/athenians/dock.xml</Actor>
<FoundationActor>structures/fndn_6x4_dock.xml</FoundationActor>

View file

@ -33,19 +33,19 @@
<Obstruction>
<Static width="28.0" depth="28.0"/>
</Obstruction>
<ProductionQueue>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/champion_infantry
units/{civ}/champion_ranged
</Entities>
</ProductionQueue>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_gymnasium.xml</select>
<constructed>interface/complete/building/complete_gymnasium.xml</constructed>
</SoundGroups>
</Sound>
<Trainer>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/champion_infantry
units/{civ}/champion_ranged
</Entities>
</Trainer>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -33,18 +33,12 @@
<Obstruction>
<Static width="24.0" depth="30.0"/>
</Obstruction>
<ProductionQueue>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/hero_themistocles
units/{civ}/hero_pericles
units/{civ}/hero_iphicrates
</Entities>
<Researcher>
<Technologies datatype="tokens">
long_walls
iphicratean_reforms
</Technologies>
</ProductionQueue>
</Researcher>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_tholos.xml</select>
@ -56,6 +50,14 @@
<Radius>38</Radius>
<Weight>40000</Weight>
</TerritoryInfluence>
<Trainer>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/hero_themistocles
units/{civ}/hero_pericles
units/{civ}/hero_iphicrates
</Entities>
</Trainer>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -19,9 +19,11 @@
<Static width="1.5" depth="4.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<Trainer disable=""/>
<VisualActor>
<Actor>props/special/eyecandy/bench_1.xml</Actor>
</VisualActor>

View file

@ -14,13 +14,13 @@
<Obstruction>
<Static width="25.0" depth="25.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
units/{civ}/cavalry_javelineer_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/britons/civic_centre.xml</Actor>
<FoundationActor>structures/fndn_7x8.xml</FoundationActor>

View file

@ -25,7 +25,16 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ProductionQueue>
<RallyPointRenderer>
<LinePassabilityClass>ship</LinePassabilityClass>
</RallyPointRenderer>
<Researcher>
<Technologies datatype="tokens">
-phase_town_{civ}
-hellenistic_metropolis
</Technologies>
</Researcher>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_slinger_b
@ -35,14 +44,7 @@
units/{civ}/ship_bireme
units/{civ}/ship_trireme
</Entities>
<Technologies datatype="tokens">
-phase_town_{civ}
-hellenistic_metropolis
</Technologies>
</ProductionQueue>
<RallyPointRenderer>
<LinePassabilityClass>ship</LinePassabilityClass>
</RallyPointRenderer>
</Trainer>
<VisualActor>
<Actor>structures/britons/crannog.xml</Actor>
<FoundationActor>structures/fndn_8x8.xml</FoundationActor>

View file

@ -15,19 +15,19 @@
<Obstruction>
<Static width="29.0" depth="29.0"/>
</Obstruction>
<ProductionQueue>
<Entities datatype="tokens">
units/{civ}/hero_boudicca
units/{civ}/hero_caratacos
units/{civ}/hero_cunobelin
</Entities>
</ProductionQueue>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_broch.xml</select>
<constructed>interface/complete/building/complete_broch.xml</constructed>
</SoundGroups>
</Sound>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_boudicca
units/{civ}/hero_caratacos
units/{civ}/hero_cunobelin
</Entities>
</Trainer>
<VisualActor>
<Actor>structures/britons/fortress.xml</Actor>
<FoundationActor>structures/fndn_9x9.xml</FoundationActor>

View file

@ -7,16 +7,18 @@
<Civ>cart</Civ>
<SpecificName>Merkāz</SpecificName>
</Identity>
<ProductionQueue>
<Researcher>
<Technologies datatype="tokens">
colonization
</Technologies>
</Researcher>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
</Entities>
<Technologies datatype="tokens">
colonization
</Technologies>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/civil_centre.xml</Actor>
</VisualActor>

View file

@ -10,12 +10,12 @@
<Obstruction>
<Static width="16.0" depth="14.5"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_sanga_trainable
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/corral.xml</Actor>
</VisualActor>

View file

@ -26,7 +26,7 @@
<Obstruction>
<Static width="28.0" depth="28.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{native}/infantry_swordsman_gaul_b
units/{native}/cavalry_swordsman_gaul_b
@ -36,7 +36,7 @@
units/{native}/infantry_swordsman_ital_b
units/{native}/cavalry_spearman_ital_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/embassy.xml</Actor>
<FoundationActor>structures/fndn_8x8.xml</FoundationActor>

View file

@ -25,12 +25,12 @@
<Obstruction>
<Static width="15.0" depth="12.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{native}/infantry_swordsman_gaul_b
units/{native}/cavalry_swordsman_gaul_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/embassy_celtic.xml</Actor>
</VisualActor>

View file

@ -18,13 +18,13 @@
<Obstruction>
<Static width="16.0" depth="16.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{native}/infantry_javelineer_iber_b
units/{native}/infantry_slinger_iber_b
units/{native}/cavalry_swordsman_iber_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/embassy_iberian.xml</Actor>
</VisualActor>

View file

@ -25,12 +25,12 @@
<Obstruction>
<Static width="11.0" depth="14.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{native}/infantry_swordsman_ital_b
units/{native}/cavalry_spearman_ital_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/embassy_italic.xml</Actor>
</VisualActor>

View file

@ -12,13 +12,13 @@
<Obstruction>
<Static width="26.0" depth="28.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_hamilcar
units/{civ}/hero_hannibal
units/{civ}/hero_maharbal
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<FoundationActor>structures/carthaginians/fndn_fortress.xml</FoundationActor>
<Actor>structures/carthaginians/fortress.xml</Actor>

View file

@ -54,14 +54,6 @@
<Floating>true</Floating>
<FloatDepth>0.0</FloatDepth>
</Position>
<ProductionQueue>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/ship_bireme
units/{civ}/ship_trireme
units/{civ}/ship_quinquereme
</Entities>
</ProductionQueue>
<RallyPointRenderer>
<LinePassabilityClass>ship</LinePassabilityClass>
</RallyPointRenderer>
@ -84,6 +76,14 @@
<Radius>200</Radius>
<Weight>25000</Weight>
</TerritoryInfluence>
<Trainer>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/ship_bireme
units/{civ}/ship_trireme
units/{civ}/ship_quinquereme
</Entities>
</Trainer>
<Vision>
<Range>100</Range>
</Vision>

View file

@ -12,11 +12,11 @@
<Obstruction>
<Static width="17.0" depth="30.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/champion_infantry
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/carthaginians/temple_big.xml</Actor>
</VisualActor>

View file

@ -26,9 +26,11 @@
<Bonus>2</Bonus>
</Population>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>7.0</HeightOffset>
</StatusBars>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/celts/hut.xml</Actor>
</VisualActor>

View file

@ -26,6 +26,8 @@
<Bonus>10</Bonus>
</Population>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/celts/longhouse.xml</Actor>
</VisualActor>

View file

@ -19,9 +19,11 @@
<Static width="2.0" depth="2.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>8.0</HeightOffset>
</StatusBars>
<Trainer disable=""/>
<VisualActor>
<Actor>props/special/eyecandy/column_doric.xml</Actor>
</VisualActor>

View file

@ -19,9 +19,11 @@
<Static width="2.0" depth="12.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<Trainer disable=""/>
<VisualActor>
<Actor>props/special/eyecandy/column_doric_fallen.xml</Actor>
</VisualActor>

View file

@ -19,9 +19,11 @@
<Static width="2.0" depth="2.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<Trainer disable=""/>
<VisualActor>
<Actor>props/special/eyecandy/column_doric_fallen_b.xml</Actor>
</VisualActor>

View file

@ -23,10 +23,12 @@
<Static width="1.5" depth="13.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<TerritoryDecay disable=""/>
<Trainer disable=""/>
<VisualActor>
<Actor>temp/fence_wood_long_a.xml</Actor>
</VisualActor>

View file

@ -20,10 +20,12 @@
<Static width="1.5" depth="6.5"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<TerritoryDecay disable=""/>
<Trainer disable=""/>
<Visibility>
<RetainInFog>true</RetainInFog>
</Visibility>

View file

@ -20,10 +20,12 @@
<Static width="1.5" depth="10.5"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<StatusBars>
<HeightOffset>6.0</HeightOffset>
</StatusBars>
<TerritoryDecay disable=""/>
<Trainer disable=""/>
<VisualActor>
<Actor>props/special/eyecandy/fence_stone_medit.xml</Actor>
</VisualActor>

View file

@ -38,15 +38,6 @@
<Obstruction>
<Static width="25.0" depth="25.0"/>
</Obstruction>
<ProductionQueue>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/champion_infantry_trumpeter
units/{civ}/hero_brennus
units/{civ}/hero_viridomarus
units/{civ}/hero_vercingetorix
</Entities>
</ProductionQueue>
<Resistance>
<Entity>
<Damage>
@ -67,6 +58,15 @@
<Radius>40</Radius>
<Weight>40000</Weight>
</TerritoryInfluence>
<Trainer>
<BatchTimeModifier>0.7</BatchTimeModifier>
<Entities datatype="tokens">
units/{civ}/champion_infantry_trumpeter
units/{civ}/hero_brennus
units/{civ}/hero_viridomarus
units/{civ}/hero_vercingetorix
</Entities>
</Trainer>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -14,13 +14,13 @@
<Obstruction>
<Static width="25.0" depth="25.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/gauls/civic_centre.xml</Actor>
</VisualActor>

View file

@ -24,11 +24,11 @@
<Obstruction>
<Static width="22.5" depth="22.5"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/champion_fanatic
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/celts/temple.xml</Actor>
<FoundationActor>structures/fndn_7x7.xml</FoundationActor>

View file

@ -32,10 +32,12 @@
<Static width="27.0" depth="57.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<TerritoryInfluence>
<Radius>60</Radius>
<Weight>65535</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>80</Range>
</Vision>

View file

@ -29,11 +29,13 @@
<Static width="26.0" depth="30.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<TerritoryInfluence>
<Root>false</Root>
<Radius>40</Radius>
<Weight>65535</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>20</Range>
</Vision>

View file

@ -33,11 +33,13 @@
</Obstruction>
<ProductionQueue disable=""/>
<RallyPoint disable=""/>
<Researcher disable=""/>
<TerritoryInfluence>
<Root>false</Root>
<Radius>36</Radius>
<Weight>65535</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -7,13 +7,13 @@
<Civ>iber</Civ>
<SpecificName>Oppidum</SpecificName>
</Identity>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_swordsman_b
units/{civ}/infantry_javelineer_b
units/{civ}/cavalry_javelineer_b
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/iberians/civic_center.xml</Actor>
</VisualActor>

View file

@ -8,13 +8,13 @@
<Obstruction>
<Static width="27.0" depth="27.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_caros
units/{civ}/hero_indibil
units/{civ}/hero_viriato
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/iberians/fortress.xml</Actor>
</VisualActor>

View file

@ -44,6 +44,7 @@
</Obstruction>
<ProductionQueue disable=""/>
<RallyPoint disable=""/>
<Researcher disable=""/>
<Resistance>
<Entity>
<Damage>
@ -60,6 +61,7 @@
</SoundGroups>
</Sound>
<TerritoryDecay disable=""/>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/iberians/sb_1.xml</Actor>
<FoundationActor>structures/fndn_2x2.xml</FoundationActor>

View file

@ -39,6 +39,7 @@
</Obstructions>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_broch.xml</select>
@ -50,6 +51,7 @@
<Radius>38</Radius>
<Weight>40000</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -31,15 +31,15 @@
<Obstruction>
<Static width="29.0" depth="29.0"/>
</Obstruction>
<ProductionQueue>
<Entities datatype="tokens">
units/{native}/cavalry_javelineer_merc_b
</Entities>
</ProductionQueue>
<TerritoryDecay>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<Trainer>
<Entities datatype="tokens">
units/{native}/cavalry_javelineer_merc_b
</Entities>
</Trainer>
<VisualActor>
<Actor>structures/mercenaries/camp_blemmye.xml</Actor>
<FoundationActor>structures/fndn_8x7.xml</FoundationActor>

View file

@ -31,16 +31,16 @@
<Obstruction>
<Static width="35.0" depth="35.0"/>
</Obstruction>
<ProductionQueue>
<Entities datatype="tokens">
units/{native}/infantry_maceman_merc_b
units/{native}/infantry_javelineer_merc_b
</Entities>
</ProductionQueue>
<TerritoryDecay>
<DecayRate>1</DecayRate>
</TerritoryDecay>
<TerritoryInfluence disable=""/>
<Trainer>
<Entities datatype="tokens">
units/{native}/infantry_maceman_merc_b
units/{native}/infantry_javelineer_merc_b
</Entities>
</Trainer>
<VisualActor>
<Actor>structures/mercenaries/camp_nuba.xml</Actor>
<FoundationActor>structures/fndn_8x8.xml</FoundationActor>

View file

@ -11,16 +11,18 @@
<Obstruction>
<Static width="31.0" depth="31"/>
</Obstruction>
<ProductionQueue>
<Researcher>
<Technologies datatype="tokens">
architecture_kush
</Technologies>
</Researcher>
<Trainer>
<Entities datatype="tokens">
units/{civ}/infantry_spearman_b
units/{civ}/infantry_archer_b
units/{civ}/cavalry_javelineer_b
</Entities>
<Technologies datatype="tokens">
architecture_kush
</Technologies>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/kushites/civic_centre_kush.xml</Actor>
</VisualActor>

View file

@ -11,12 +11,12 @@
<Obstruction>
<Static width="12.0" depth="14.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
-gaia/fauna_cattle_cow_trainable
gaia/fauna_cattle_sanga_trainable
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/kushites/corral.xml</Actor>
<FoundationActor>structures/fndn_4x4.xml</FoundationActor>

View file

@ -12,13 +12,13 @@
<Obstruction>
<Static width="28.0" depth="28.0"/>
</Obstruction>
<ProductionQueue>
<Trainer>
<Entities datatype="tokens">
units/{civ}/hero_nastasen
units/{civ}/hero_amanirenas
units/{civ}/hero_arakamani
</Entities>
</ProductionQueue>
</Trainer>
<VisualActor>
<Actor>structures/kushites/fortress.xml</Actor>
</VisualActor>

View file

@ -38,6 +38,7 @@
<Static width="20.0" depth="24.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_temple_10.xml</select>
@ -52,6 +53,7 @@
<Radius>40</Radius>
<Weight>40000</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>40</Range>
</Vision>

View file

@ -38,6 +38,7 @@
<Static width="16.0" depth="16.0"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<Sound>
<SoundGroups>
<select>interface/select/building/sel_temple_10.xml</select>
@ -49,6 +50,7 @@
<Radius>30</Radius>
<Weight>30000</Weight>
</TerritoryInfluence>
<Trainer disable=""/>
<Vision>
<Range>30</Range>
</Vision>

View file

@ -16,6 +16,8 @@
<Static width="20" depth="21"/>
</Obstruction>
<ProductionQueue disable=""/>
<Researcher disable=""/>
<Trainer disable=""/>
<VisualActor>
<Actor>structures/kushites/shrine.xml</Actor>
<FoundationActor>structures/fndn_6x6.xml</FoundationActor>

Some files were not shown because too many files have changed in this diff Show more