Index: binaries/data/mods/public/simulation/components/Player.js =================================================================== --- binaries/data/mods/public/simulation/components/Player.js +++ binaries/data/mods/public/simulation/components/Player.js @@ -390,6 +390,16 @@ 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); Index: binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- binaries/data/mods/public/simulation/components/ProductionQueue.js +++ binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -213,129 +213,6 @@ }; /* - * Returns list of technologies that can be researched by this building. - */ -ProductionQueue.prototype.GetTechnologiesList = function() -{ - if (!this.template.Technologies) - return []; - - let string = this.template.Technologies._string; - string = ApplyValueModificationsToEntity("ProductionQueue/Technologies/_string", string, this.entity); - - if (!string) - return []; - - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (!cmpTechnologyManager) - return []; - - let 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) - { - let tech = techs[i]; - if (tech.indexOf("{civ}") == -1) - continue; - let civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); - techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); - } - - // Remove any technologies that can't be researched by this civ. - techs = techs.filter(tech => - cmpTechnologyManager.CheckTechnologyRequirements( - DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), - true)); - - let techList = []; - // Stores the tech which supersedes the key. - let superseded = {}; - - let 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 (let tech of techs) - { - if (disabledTechnologies && disabledTechnologies[tech]) - continue; - - let template = TechnologyTemplates.Get(tech); - if (!template.supersedes || techs.indexOf(template.supersedes) === -1) - techList.push(tech); - else - superseded[template.supersedes] = tech; - } - - // Now make researched/in progress techs invisible. - for (let i in techList) - { - let tech = techList[i]; - while (this.IsTechnologyResearchedOrInProgress(tech)) - tech = superseded[tech]; - - techList[i] = tech; - } - - let ret = []; - - // This inserts the techs into the correct positions to line up the technology pairs. - for (let i = 0; i < techList.length; ++i) - { - let tech = techList[i]; - if (!tech) - { - ret[i] = undefined; - continue; - } - - let template = TechnologyTemplates.Get(tech); - if (template.top) - ret[i] = { "pair": true, "top": template.top, "bottom": template.bottom }; - else - ret[i] = tech; - } - - return ret; -}; - -ProductionQueue.prototype.GetTechCostMultiplier = function() -{ - let techCostMultiplier = {}; - for (let res in this.template.TechCostMultiplier) - techCostMultiplier[res] = ApplyValueModificationsToEntity( - "ProductionQueue/TechCostMultiplier/" + res, - +this.template.TechCostMultiplier[res], - this.entity); - - return techCostMultiplier; -}; - -ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) -{ - if (!tech) - return false; - - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (!cmpTechnologyManager) - return false; - - let template = TechnologyTemplates.Get(tech); - if (template.top) - return cmpTechnologyManager.IsTechnologyResearched(template.top) || - cmpTechnologyManager.IsInProgress(template.top) || - cmpTechnologyManager.IsTechnologyResearched(template.bottom) || - cmpTechnologyManager.IsInProgress(template.bottom); - - return cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech); -}; - -/* * Adds a new batch of identical units to train or a technology to research to the production queue. * @param {string} templateName - The template to start production on. * @param {string} type - The type of production (i.e. "unit" or "technology"). @@ -447,40 +324,19 @@ const time = this.GetBatchTime(count) * buildTime * 1000; item.timeTotal = time; item.timeRemaining = time; + + // TrySubtractResources should report error to player (they ran out of resources). + if (!cmpPlayer.TrySubtractResources(item.resources)) + return false; } else if (type == "technology") { - if (!TechnologyTemplates.Has(templateName)) + const cmpResearcher = Engine.QueryInterface(this.entity, IID_Researcher); + if (!cmpResearcher) return false; - - if (!this.GetTechnologiesList().some(tech => - tech && - (tech == templateName || - tech.pair && - (tech.top == templateName || tech.bottom == templateName)))) - { - error("This entity cannot research " + templateName); + item.technology = cmpResearcher.QueueTechnology(templateName, metadata); + if (item.technology == -1) return false; - } - - item.technology = { - "template": templateName, - "resources": {} - }; - - let template = TechnologyTemplates.Get(templateName); - let techCostMultiplier = this.GetTechCostMultiplier(); - - if (template.cost) - for (const res in template.cost) - { - item.technology.resources[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]); - item.resources[res] = item.technology.resources[res]; - } - - const time = techCostMultiplier.time * (template.researchTime || 0) * 1000; - item.timeTotal = time; - item.timeRemaining = time; } else { @@ -488,10 +344,6 @@ return false; } - // TrySubtractResources should report error to player (they ran out of resources). - if (!cmpPlayer.TrySubtractResources(item.resources)) - return false; - item.id = this.nextID++; if (pushFront) { @@ -511,19 +363,6 @@ "metadata": metadata, "trainerEntity": this.entity }); - if (item.technology) - { - // Tell the technology manager that we have started researching this - // such that players can't research the same thing twice. - const cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - cmpTechnologyManager.QueuedResearch(templateName, this.entity); - - cmpTrigger.CallEvent("OnResearchQueued", { - "playerid": player, - "technologyTemplate": item.technology.template, - "researcherEntity": this.entity - }); - } Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); @@ -585,8 +424,6 @@ totalCosts[resource] = 0; if (item.entity) totalCosts[resource] += Math.floor(item.entity.count * item.entity.resources[resource]); - if (item.technology) - totalCosts[resource] += item.technology.resources[resource]; if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]); } @@ -596,9 +433,9 @@ if (item.technology) { - let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.StoppedResearch(item.technology.template, true); + let cmpResearcher = QueryInterface(this.entity, IID_Researcher); + if (cmpResearcher) + cmpResearcher.StopResearching(item.technology); } this.queue.splice(itemIndex, 1); @@ -623,7 +460,7 @@ return this.queue.map(item => ({ "id": item.id, "unitTemplate": item.entity?.template, - "technologyTemplate": item.technology?.template, + "technology": item.technology, "count": item.entity?.count, "neededSlots": item.entity?.neededSlots, "progress": 1 - (item.timeRemaining / (item.timeTotal || 1)), @@ -834,18 +671,12 @@ Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity }); } if (item.technology) - { - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.StartedResearch(item.technology.template, true); - else - warn("Failed to start researching " + item.technology.template + ": No TechnologyManager available."); - this.SetAnimation("researching"); - } item.productionStarted = true; } + if (item.technology) + time = Engine.QueryInterface(this.entity, IID_Researcher).ProgressTimeout(id, time); if (item.timeRemaining > time) { @@ -887,22 +718,6 @@ return; } } - if (item.technology) - { - let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.ResearchTechnology(item.technology.template); - else - warn("Failed to finish researching " + item.technology.template + ": No TechnologyManager available."); - - const template = TechnologyTemplates.Get(item.technology.template); - if (template && template.soundComplete) - { - let cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager); - if (cmpSoundManager) - cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); - } - } time -= item.timeRemaining; this.queue.shift(); Index: binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/Researcher.js @@ -0,0 +1,361 @@ +function Researcher() {} + +Researcher.prototype.Schema = + "Allows the entity to research technologies." + + "" + + "0.7" + + "" + + "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + + "" + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + "" + + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + + "" + + ""; + +/** + * This object represents a technology being researched. + */ +Researcher.prototype.Item = function() {} + +Researcher.prototype.Item.prototype.Init = function(templateName, researcher, metadata) +{ + this.templateName = templateName; + this.researcher = researcher; + this.metadata = metadata; +}; + +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] || 1) * 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 started researching this + // such that players can't research the same thing twice. + const cmpTechnologyManager = QueryOwnerInterface(this.researcher, IID_TechnologyManager); + cmpTechnologyManager.QueuedResearch(this.templateName, this.researcher); + + return true; +}; + +Researcher.prototype.Item.prototype.Stop = function() +{ + let cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + if (cmpTechnologyManager) + cmpTechnologyManager.StoppedResearch(this.templateName, true); + + QueryPlayerIDInterface(this.player)?.RefundResources(this.resources); + delete this.resources; +}; + +Researcher.prototype.Item.prototype.Start = function() +{ + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + if (cmpTechnologyManager) + cmpTechnologyManager.StartedResearch(this.templateName, true); + else + warn("Failed to start researching " + this.templateName + ": No TechnologyManager available."); + this.started = true; +}; + +Researcher.prototype.Item.prototype.Finish = function() +{ + const cmpTechnologyManager = QueryPlayerIDInterface(this.player, IID_TechnologyManager); + if (cmpTechnologyManager) + cmpTechnologyManager.ResearchTechnology(this.templateName); + else + warn("Failed to finish researching " + this.templateName + ": No TechnologyManager available."); + + const template = TechnologyTemplates.Get(this.templateName); + if (template?.soundComplete) + Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager)?.PlaySoundGroup(template.soundComplete, this.researcher); +}; + +/** + * @param {number} allocatedTime - The time allocated to this item. + * @return {number} - The time not used for this item. + */ +Researcher.prototype.Item.prototype.ProgressTimeout = function(allocatedTime) +{ + if (!this.started) + this.Start(); + + if (this.timeRemaining > allocatedTime) + { + this.timeRemaining -= allocatedTime; + return 0; + } + this.Finish(); + return allocatedTime - this.timeRemaining; +}; + +Researcher.prototype.Item.prototype.Pause = function() +{ + this.paused = true; +}; + +Researcher.prototype.Item.prototype.Unpause = function() +{ + delete this.paused; +}; + +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 = []; + // Stores the tech which supersedes the key. + 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; +}; + +Researcher.prototype.GetTechCostMultiplier = function() +{ + const techCostMultiplier = {}; + for (const res in this.template.TechCostMultiplier) + techCostMultiplier[res] = ApplyValueModificationsToEntity( + "ProductionQueue/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 queued. + */ +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) +{ + const item = this.queue.get(id); + item.Stop(); + this.queue.delete(id); +}; + +/** + * @param {number} id - The ID of the tech we spent time on. + * @param {number} allocatedTime - The time we spent on the given tech. + * @return {number} - The time we didn't use (because the tech costed less than the allocated time). + */ +Researcher.prototype.ProgressTimeout = function(id, allocatedTime) +{ + return this.queue.get(id)?.ProgressTimeout(allocatedTime); +}; + +Engine.RegisterComponentType(IID_Researcher, "Researcher", Researcher); Index: binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/TechnologyManager.js +++ binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -282,6 +282,17 @@ 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 Index: binaries/data/mods/public/simulation/components/interfaces/Researcher.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/Researcher.js @@ -0,0 +1 @@ +Engine.RegisterInterface("Researcher"); Index: binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -1,6 +1,5 @@ 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"); @@ -41,10 +40,7 @@ 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}" } + "units/{native}/support_female_citizen" } }); cmpProductionQueue.GetUpgradedTemplate = (template) => template; @@ -54,17 +50,10 @@ AddMock(playerEntityID, IID_Player, { "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), "GetDisabledTemplates": () => ({}), "GetPlayerID": () => playerId }); - AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": () => false - }); - AddMock(productionQueueId, IID_Ownership, { "GetOwner": () => playerId }); @@ -82,10 +71,6 @@ 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"] - ); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": name => name == "units/iber/support_female_citizen", @@ -132,32 +117,6 @@ "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() @@ -481,9 +440,6 @@ 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; @@ -493,7 +449,6 @@ // player "GetCiv": () => "test", "GetDisabledTemplates": () => [], - "GetDisabledTechnologies": () => [], "TryReservePopulationSlots": () => false, // Always have pop space. "TrySubtractResources": () => true, "UnBlockTraining": () => {}, @@ -501,11 +456,7 @@ "GetPlayerID": () => 1, // entitylimits "ChangeCount": () => {}, - "AllowedToTrain": () => true, - // techmanager - "CheckTechnologyRequirements": () => true, - "IsTechnologyResearched": () => false, - "IsInProgress": () => false + "AllowedToTrain": () => true })); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface); @@ -518,9 +469,6 @@ 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, {}); Index: binaries/data/mods/public/simulation/components/tests/test_Researcher.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_Researcher.js @@ -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. +spyTechManager = new Spy(techManager, "StartedResearch"); +id = cmpResearcher.QueueTechnology(queuedTech); +TS_ASSERT_EQUALS(cmpResearcher.ProgressTimeout(id, 500), 0); +// Test it has started. +TS_ASSERT_EQUALS(spyTechManager._called, 1); + +spyTechManager = new Spy(techManager, "ResearchTechnology"); +TS_ASSERT_EQUALS(cmpResearcher.ProgressTimeout(id, 1000), 500); +TS_ASSERT_EQUALS(spyTechManager._called, 1); Index: binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml +++ binaries/data/mods/public/simulation/templates/template_structure_civic_civil_centre.xml @@ -1,5 +1,6 @@ + FemaleCitizen 120