Index: binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/GuiInterface.js +++ binaries/data/mods/public/simulation/components/GuiInterface.js @@ -340,6 +340,13 @@ "isUpgrading": cmpUpgrade.IsUpgrading() }; + const cmpResearcher = Engine.QueryInterface(ent, IID_Researcher); + if (cmpResearcher) + ret.rsearcher = { + "technologies": cmpResearcher.GetTechnologiesList(), + "techCostMultiplier": cmpResearcher.GetTechCostMultiplier() + }; + let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver); if (cmpStatusEffects) ret.statusEffects = cmpStatusEffects.GetActiveStatuses(); @@ -347,13 +354,16 @@ 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() }; + let cmpTrainer = Engine.QueryInterface(ent, IID_Trainer); + if (cmpTrainer) + ret.trainer = { + "entities": cmpTrainer.GetEntitiesList() + }; + let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { 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 @@ -43,36 +43,14 @@ Queue items are: { "id": 1, - "player": 1, // Who paid for this batch; we need this to cope with refunds cleanly. - "productionStarted": false, // true iff production has started (we have reserved population). - "timeTotal": 15000, // msecs - "timeRemaining": 10000, // msecs + "player": 1, // Who issued this item. "paused": false, // Whether the item is currently paused (e.g. not the first item or parent is garrisoned). - "resources": { "wood": 100, ... }, // Total resources of the item when queued. - "entity": { - "template": "units/example", - "count": 10, - "neededSlots": 3, // Number of population slots missing for production to begin. - "population": 1, // Population per unit, multiply by count to get total. - "resources": { "wood": 100, ... }, // Resources per entity, multiply by count to get total. - "entityCache": [189, ...], // The entities created but not spawned yet. - }, - "technology": { - "template": "example_tech", - "resources": { "wood": 100, ... }, - } + "entity": 1, // The ID of the batch in the Trainer component. + "technology": 1 // The ID of the item in the Researcher component. } */ }; -/* - * Returns list of entities that can be trained by this building. - */ -ProductionQueue.prototype.GetEntitiesList = function() -{ - return Array.from(this.entitiesMap.values()); -}; - /** * @return {boolean} - Whether we are automatically queuing items. */ @@ -97,244 +75,6 @@ delete this.autoqueuing; }; -/** - * Calculate the new list of producible entities - * and update any entities currently being produced. - */ -ProductionQueue.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; - - let string = this.template.Entities._string; - // Tokens can be added -> process an empty list to get them. - let addedTokens = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", "", this.entity); - if (!addedTokens && !string) - return; - - addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/); - - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let cmpPlayer = QueryOwnerInterface(this.entity); - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - - let 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). - */ - - let removeAllQueuedTemplate = (token) => { - let queue = clone(this.queue); - let template = this.entitiesMap.get(token); - for (let item of queue) - if (item.entity?.template && item.entity.template === template) - this.RemoveItem(item.id); - }; - let updateAllQueuedTemplate = (token, updateTo) => { - let template = this.entitiesMap.get(token); - for (let item of this.queue) - if (item.entity?.template && item.entity.template === template) - item.entity.template = updateTo; - }; - - let toks = string.split(/\s+/); - for (let tok of addedTokens) - toks.push(tok); - - let addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {}); - this.entitiesMap = toks.reduce((entMap, token) => { - let rawToken = token; - if (!(token in addedDict)) - { - // This is a bit wasteful but I can't think of a simpler/better way. - // The list of token is unlikely to be a performance bottleneck anyways. - token = ApplyValueModificationsToEntity("ProductionQueue/Entities/_string", token, this.entity); - token = token.split(/\s+/); - if (token.every(tok => addedTokens.indexOf(tok) !== -1)) - { - removeAllQueuedTemplate(rawToken); - return entMap; - } - token = token[0]; - } - // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. - if (cmpIdentity) - token = token.replace(/\{native\}/g, cmpIdentity.GetCiv()); - if (cmpPlayer) - token = token.replace(/\{civ\}/g, cmpPlayer.GetCiv()); - - // Filter out disabled and invalid entities. - if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token)) - { - removeAllQueuedTemplate(rawToken); - return entMap; - } - - token = this.GetUpgradedTemplate(token); - entMap.set(rawToken, token); - updateAllQueuedTemplate(rawToken, token); - return entMap; - }, new Map()); -}; - -/* - * Returns the upgraded template name if necessary. - */ -ProductionQueue.prototype.GetUpgradedTemplate = function(templateName) -{ - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return templateName; - - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let template = cmpTemplateManager.GetTemplate(templateName); - while (template && template.Promotion !== undefined) - { - let requiredXp = ApplyValueModificationsToTemplate( - "Promotion/RequiredXp", - +template.Promotion.RequiredXp, - cmpPlayer.GetPlayerID(), - template); - if (requiredXp > 0) - break; - templateName = template.Promotion.Entity; - template = cmpTemplateManager.GetTemplate(templateName); - } - return templateName; -}; - -/* - * 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. @@ -381,106 +121,26 @@ const item = { "player": player, - "metadata": metadata, - "productionStarted": false, - "resources": {}, // The total resource costs. - "paused": false + "metadata": metadata }; - // ToDo: Still some duplication here, some can might be combined, - // but requires some more refactoring. if (type == "unit") { - if (!Number.isInteger(count) || count <= 0) - { - error("Invalid batch count " + count); + const cmpTrainer = Engine.QueryInterface(this.entity, IID_Trainer); + if (!cmpTrainer) return false; - } - - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let template = cmpTemplateManager.GetTemplate(this.GetUpgradedTemplate(templateName)); - if (!template) + item.entity = cmpTrainer.QueueBatch(templateName, count, metadata); + if (item.entity == -1) return false; - - item.entity = { - "template": templateName, - "count": count, - "population": ApplyValueModificationsToTemplate( - "Cost/Population", - +template.Cost.Population, - player, - template), - "resources": {}, // The resource costs per entity. - }; - - for (let res in template.Cost.Resources) - { - item.entity.resources[res] = ApplyValueModificationsToTemplate( - "Cost/Resources/" + res, - +template.Cost.Resources[res], - player, - template); - - item.resources[res] = Math.floor(count * item.entity.resources[res]); - } - - if (template.TrainingRestrictions) - { - let unitCategory = template.TrainingRestrictions.Category; - let cmpPlayerEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits); - if (cmpPlayerEntityLimits) - { - if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, count, templateName, template.TrainingRestrictions.MatchLimit)) - // Already warned, return. - return false; - cmpPlayerEntityLimits.ChangeCount(unitCategory, count); - if (template.TrainingRestrictions.MatchLimit) - cmpPlayerEntityLimits.ChangeMatchCount(templateName, count); - } - } - - const buildTime = ApplyValueModificationsToTemplate( - "Cost/BuildTime", - +template.Cost.BuildTime, - player, - template); - const time = this.GetBatchTime(count) * buildTime * 1000; - item.timeTotal = time; - item.timeRemaining = time; } 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 +148,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) { @@ -502,29 +158,6 @@ else this.queue.push(item); - const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); - if (item.entity) - cmpTrigger.CallEvent("OnTrainingQueued", { - "playerid": player, - "unitTemplate": item.entity.template, - "count": count, - "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); if (!this.timer) @@ -534,7 +167,6 @@ /* * Removes an item from the queue. - * Refunds resource costs and population reservations. * item.player is used as this.entity's owner may have changed. */ ProductionQueue.prototype.RemoveItem = function(id) @@ -545,60 +177,20 @@ let item = this.queue[itemIndex]; - // Destroy any cached entities (those which didn't spawn for some reason). - if (item.entity?.cache?.length) - { - for (const ent of item.entity.cache) - Engine.DestroyEntity(ent); - - delete item.entity.cache; - } - - const cmpPlayer = QueryPlayerIDInterface(item.player); - if (item.entity) { - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - const template = cmpTemplateManager.GetTemplate(item.entity.template); - if (template.TrainingRestrictions) - { - let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); - if (cmpPlayerEntityLimits) - cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -item.entity.count); - if (template.TrainingRestrictions.MatchLimit) - cmpPlayerEntityLimits.ChangeMatchCount(item.entity.template, -item.entity.count); - } - if (cmpPlayer) - { - if (item.productionStarted) - cmpPlayer.UnReservePopulationSlots(item.entity.population * item.entity.count); - if (itemIndex == 0) - cmpPlayer.UnBlockTraining(); - } - } - - let cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); - - const totalCosts = {}; - for (let resource in item.resources) - { - 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]); + const cmpTrainer = QueryInterface(this.entity, IID_Trainer); + if (cmpTrainer) + cmpTrainer.StopBatch(item.entity); + if (itemIndex == 0) + QueryPlayerIDInterface(item.player)?.UnBlockTraining(); } - if (cmpPlayer) - cmpPlayer.AddResources(totalCosts); - if (item.technology) { - let cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager); - if (cmpTechnologyManager) - cmpTechnologyManager.StoppedResearch(item.technology.template, true); + const cmpResearcher = QueryInterface(this.entity, IID_Researcher); + if (cmpResearcher) + cmpResearcher.StopResearching(item.technology); } this.queue.splice(itemIndex, 1); @@ -620,17 +212,18 @@ */ ProductionQueue.prototype.GetQueue = function() { - return this.queue.map(item => ({ - "id": item.id, - "unitTemplate": item.entity?.template, - "technologyTemplate": item.technology?.template, - "count": item.entity?.count, - "neededSlots": item.entity?.neededSlots, - "progress": 1 - (item.timeRemaining / (item.timeTotal || 1)), - "timeRemaining": item.timeRemaining, - "paused": item.paused, - "metadata": item.metadata - })); + const cmpResearcher = Engine.QueryInterface(this.entity, IID_Researcher); + const cmpTrainer = Engine.QueryInterface(this.entity, IID_Trainer); + return this.queue.map(item => { + let result; + if (item.technology) + result = cmpResearcher.GetResearchingTechnology(item.technology); + else if (item.entity) + result = cmpTrainer.GetBatch(item.unit); + result.id = item.id; + result.paused = item.paused; + return result; + }); }; /* @@ -644,18 +237,6 @@ this.DisableAutoQueue(); }; -/* - * Returns batch build time. - */ -ProductionQueue.prototype.GetBatchTime = function(batchSize) -{ - // TODO: work out what equation we should use here. - return Math.pow(batchSize, ApplyValueModificationsToEntity( - "ProductionQueue/BatchTimeModifier", - +this.template.BatchTimeModifier, - this.entity)); -}; - ProductionQueue.prototype.OnOwnershipChanged = function(msg) { // Reset the production queue whenever the owner changes. @@ -669,121 +250,6 @@ this.CalculateEntitiesMap(); }; -ProductionQueue.prototype.OnCivChanged = function() -{ - this.CalculateEntitiesMap(); -}; - -/* - * 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). - * @param {Object} item - The item to spawn units for. - * @param {number} item.entity.count - The number of entities to spawn. - * @param {string} item.player - The owner of the item. - * @param {string} item.entity.template - The template to spawn. - * @param {any} - item.metadata - Optionally any metadata to add to the TrainingFinished message. - * - * @return {number} - The number of successfully created entities - */ -ProductionQueue.prototype.SpawnUnits = function(item) -{ - let createdEnts = []; - let spawnedEnts = []; - - // We need entities to test spawning, but we don't want to waste resources, - // so only create them once and use as needed. - if (!item.entity.cache) - { - item.entity.cache = []; - for (let i = 0; i < item.entity.count; ++i) - item.entity.cache.push(Engine.AddEntity(item.entity.template)); - } - - let autoGarrison; - let cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint); - if (cmpRallyPoint) - { - let data = cmpRallyPoint.GetData()[0]; - if (data && data.target && data.target == this.entity && data.command == "garrison") - autoGarrison = true; - } - - let cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint); - let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); - let positionSelf = cmpPosition && cmpPosition.GetPosition(); - - let cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits); - let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker); - while (item.entity.cache.length) - { - const ent = item.entity.cache[0]; - let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); - let garrisoned = false; - - if (autoGarrison) - { - let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); - if (cmpGarrisonable) - { - // Temporary owner affectation needed for GarrisonHolder checks. - cmpNewOwnership.SetOwnerQuiet(item.player); - garrisoned = cmpGarrisonable.Garrison(this.entity); - cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER); - } - } - - if (!garrisoned) - { - let pos = cmpFootprint.PickSpawnPoint(ent); - if (pos.y < 0) - break; - - let cmpNewPosition = Engine.QueryInterface(ent, IID_Position); - cmpNewPosition.JumpTo(pos.x, pos.z); - - if (positionSelf) - cmpNewPosition.SetYRotation(positionSelf.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) - { - let cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions); - if (cmpTrainingRestrictions) - cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1); - } - cmpNewOwnership.SetOwner(item.player); - - if (cmpPlayerStatisticsTracker) - cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent); - - item.entity.cache.shift(); - createdEnts.push(ent); - } - - if (spawnedEnts.length && !autoGarrison && cmpRallyPoint) - for (let com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts)) - ProcessCommand(item.player, com); - - if (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.entity, MT_TrainingFinished, { - "entities": createdEnts, - "owner": item.player, - "metadata": item.metadata - }); - } - - return createdEnts.length; -}; - /* * Increments progress on the first item in the production queue and blocks the * queue if population limit is reached or some units failed to spawn. @@ -795,15 +261,10 @@ if (this.paused) return; - let cmpPlayer = QueryOwnerInterface(this.entity); - if (!cmpPlayer) - return; - // Allocate available time to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second). let time = this.ProgressInterval + lateness; - let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); while (this.queue.length) { @@ -813,101 +274,35 @@ if (!item.productionStarted) { if (item.entity) - { - const template = cmpTemplateManager.GetTemplate(item.entity.template); - item.entity.population = ApplyValueModificationsToTemplate( - "Cost/Population", - +template.Cost.Population, - item.player, - template); - - item.entity.neededSlots = cmpPlayer.TryReservePopulationSlots(item.entity.population * item.entity.count); - if (item.entity.neededSlots) - { - cmpPlayer.BlockTraining(); - return; - } this.SetAnimation("training"); - - cmpPlayer.UnBlockTraining(); - - 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.timeRemaining > time) - { - item.timeRemaining -= time; - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); - return; - } - + let finishedItem = false; if (item.entity) { - let numSpawned = this.SpawnUnits(item); - if (numSpawned) - cmpPlayer.UnReservePopulationSlots(item.entity.population * numSpawned); - if (numSpawned == item.entity.count) - { - cmpPlayer.UnBlockTraining(); - delete this.spawnNotified; - } - else - { - if (numSpawned) - { - item.entity.count -= numSpawned; - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); - } - - cmpPlayer.BlockTraining(); - - if (!this.spawnNotified) - { - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.PushNotification({ - "players": [cmpPlayer.GetPlayerID()], - "message": markForTranslation("Can't find free space to spawn trained units"), - "translateMessage": true - }); - this.spawnNotified = true; - } - return; - } + const cmpTrainer = Engine.QueryInterface(this.entity, IID_Trainer); + time = cmpTrainer.Progress(item.entity, time); + finishedItem = !cmpTrainer.HasItem(item.entity); } 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 cmpResearcher = Engine.QueryInterface(this.entity, IID_Researcher); + time = cmpResearcher.Progress(item.technology, time); + finishedItem = !cmpResearcher.HasItem(item.technology) && finishedItem; + } - 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); - } + if (!finishedItem) + { + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); + return; } - time -= item.timeRemaining; this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); - +warn("ToDo"); // If autoqueuing, push a new unit on the queue immediately, // but don't start right away. This 'wastes' some time, making // autoqueue slightly worse than regular queuing, and also ensures @@ -920,7 +315,7 @@ this.DisableAutoQueue(); const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ - "players": [cmpPlayer.GetPlayerID()], + "players": [QueryOwnerInterface(this.entity).GetPlayerID()], "message": markForTranslation("Could not auto-queue unit, de-activating."), "translateMessage": true }); @@ -939,6 +334,7 @@ this.paused = true; if (this.queue[0]) this.queue[0].paused = true; + this.StopTimer(); }; ProductionQueue.prototype.UnpauseProduction = function() @@ -974,39 +370,11 @@ delete this.timer; }; -ProductionQueue.prototype.OnValueModification = function(msg) -{ - // If the promotion requirements of units is changed, - // update the entities list so that automatically promoted units are shown - // appropriately in the list. - if (msg.component != "Promotion" && (msg.component != "ProductionQueue" || - !msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/")))) - return; - - if (msg.entities.indexOf(this.entity) === -1) - return; - - // This also updates the queued production if necessary. - this.CalculateEntitiesMap(); - - // Inform the GUI that it'll need to recompute the selection panel. - // TODO: it would be better to only send the message if something actually changing - // for the current production queue. - let cmpPlayer = QueryOwnerInterface(this.entity); - if (cmpPlayer) - Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); -}; - ProductionQueue.prototype.HasQueuedProduction = function() { return this.queue.length > 0; }; -ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg) -{ - this.CalculateEntitiesMap(); -}; - ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg) { if (msg.holderID != INVALID_ENTITY) Index: binaries/data/mods/public/simulation/components/Researcher.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/Researcher.js @@ -0,0 +1,412 @@ +function Researcher() {} + +Researcher.prototype.Schema = + "Allows the entity to research technologies." + + "" + + "" + + "0.5" + + "0.1" + + "0" + + "2" + + "" + + "" + + "" + + "\n phase_town_{civ}\n phase_metropolis_ptol\n unlock_shared_los\n wonder_population_cap\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] === 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 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() +{ + const 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); + this.finished = true; +}; + +/** + * @param {number} allocatedTime - The time allocated to this item. + * @return {number} - The time not used for this item. + */ +Researcher.prototype.Item.prototype.Progress = 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( + "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 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) +{ + 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) +{ + this.queue.has(id); +}; + +/** + * @parameter {number} id - The id of the technology. + * @return {Object} - Some basic information about the technology. + */ +Researcher.prototype.GetResearchingTechnology = function(id) +{ + const item = this.queue.get(id); + return item && { + "technology": item.templateName, + "progress": 1 - (item.timeRemaining / item.timeTotal), + "timeRemaining": item.timeRemaining, + "paused": item.paused, + "metadata": item.metadata + }; +}; + +/** + * @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 didn't use (because the item costed less than the allocated time). + */ +Researcher.prototype.Progress = function(id, allocatedTime) +{ + const item = this.queue.get(id); + const remainingTime = item.Progress(allocatedTime); + if (item.finished) + this.queue.delete(id); + return remainingTime; +}; + +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/Trainer.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/Trainer.js @@ -0,0 +1,675 @@ +function Trainer() {} + +Trainer.prototype.Schema = + "Allows the entity to train new units." + + "" + + "0.7" + + "" + + "\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "tokens" + + "" + + "" + + "" + + ""; + +/** + * This object represents a batch of entities being trained. + */ +Trainer.prototype.Item = function() {} + +Trainer.prototype.Item.prototype.Init = function(templateName, count, trainer, metadata) +{ + this.count = count; + this.templateName = templateName; + this.trainer = trainer; + this.metadata = metadata; +}; + +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; +}; + +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 (let 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) + } + + delete this.resources; +}; + +Trainer.prototype.Item.prototype.Start = function() +{ + const cmpPlayer = QueryOwnerInterface(this.trainer); + 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.entity, 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 not 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 0; + + if (this.timeRemaining > allocatedTime) + { + this.timeRemaining -= allocatedTime; + return 0; + } + this.Finish(); + return allocatedTime - this.timeRemaining; +}; + +Trainer.prototype.Item.prototype.Pause = function() +{ + this.paused = true; +}; + +Trainer.prototype.Item.prototype.Unpause = function() +{ + delete this.paused; +}; + +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 { + "nextID": this.nextID, + "queue": queue + }; +}; + +Trainer.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 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("ProductionQueue/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) => { + let queue = clone(this.queue); + let template = this.entitiesMap.get(token); + for (let item of queue) + if (item.entity?.template && item.entity.template === template) + this.RemoveItem(item.id); + }; +// ToDo: Fix these. ^v + const updateAllQueuedTemplate = (token, updateTo) => { + let template = this.entitiesMap.get(token); + for (let item of this.queue) + if (item.entity?.template && item.entity.template === template) + item.entity.template = 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("ProductionQueue/Entities/_string", token, this.entity); + token = token.split(/\s+/); + if (token.every(tok => addedTokens.indexOf(tok) !== -1)) + { + removeAllQueuedTemplate(rawToken); + return entMap; + } + token = token[0]; + } + // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. + if (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; +}; + +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 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 && { + "unitTemplate": item.templateName, + "count": item.count, + "neededSlots": item.missingPopSpace, + "progress": 1 - (item.timeRemaining / item.timeTotal), + "timeRemaining": item.timeRemaining, + "paused": item.paused, + "metadata": item.metadata + }; +}; + +/** + * @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 didn't use (because the item costed less than the allocated time). + */ +Trainer.prototype.Progress = function(id, allocatedTime) +{ + const item = this.queue.get(id); + const remainingTime = item.Progress(allocatedTime); + if (item.finished) + this.queue.delete(id); + return remainingTime; +}; + +Trainer.prototype.OnCivChanged = function() +{ + 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 != "ProductionQueue" || + !msg.valueNames.some(val => val.startsWith("ProductionQueue/Entities/")))) + return; + + if (msg.entities.indexOf(this.entity) === -1) + return; + + // This also updates the queued production if necessary. + this.CalculateEntitiesMap(); + + // Inform the GUI that it'll need to recompute the selection panel. + // TODO: it would be better to only send the message if something actually changing + // for the current production queue. + let cmpPlayer = QueryOwnerInterface(this.entity); + if (cmpPlayer) + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID()); +}; + +Trainer.prototype.OnDisabledTemplatesChanged = function(msg) +{ + this.CalculateEntitiesMap(); +}; + +Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer); Index: binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js +++ binaries/data/mods/public/simulation/components/interfaces/ProductionQueue.js @@ -5,15 +5,3 @@ * 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"); 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/interfaces/Trainer.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/Trainer.js @@ -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"); Index: binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -6,7 +6,6 @@ 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/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"); 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,308 +1,18 @@ 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/Timer.js"); -Engine.LoadComponentScript("interfaces/TrainingRestrictions.js"); -Engine.LoadComponentScript("interfaces/Trigger.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); -function testEntitiesList() -{ - Engine.RegisterGlobal("TechnologyTemplates", { - "Has": name => name == "phase_town_athen" || name == "phase_city_athen", - "Get": () => ({}) - }); - - const productionQueueId = 6; - const playerId = 1; - const playerEntityID = 2; - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) - }); - - let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", { - "Entities": { "_string": "units/{civ}/cavalry_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(SYSTEM_ENTITY, IID_PlayerManager, { - "GetPlayerByID": id => playerEntityID - }); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({}), - "GetPlayerID": () => playerId - }); - - AddMock(playerEntityID, IID_TechnologyManager, { - "CheckTechnologyRequirements": () => true, - "IsInProgress": () => false, - "IsTechnologyResearched": () => false - }); - - AddMock(productionQueueId, IID_Ownership, { - "GetOwner": () => playerId - }); - - AddMock(productionQueueId, IID_Identity, { - "GetCiv": () => "iber" - }); - - AddMock(productionQueueId, IID_Upgrade, { - "IsUpgrading": () => false - }); - - 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"] - ); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": name => name == "units/iber/support_female_citizen", - "GetTemplate": name => ({}) - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber/support_female_citizen"]); - - AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "TemplateExists": () => true, - "GetTemplate": name => ({}) - }); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/athen/infantry_swordsman_b": true }), - "GetPlayerID": () => playerId - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber/cavalry_javelineer_b", "units/iber/infantry_swordsman_b", "units/iber/support_female_citizen"] - ); - - AddMock(playerEntityID, IID_Player, { - "GetCiv": () => "iber", - "GetDisabledTechnologies": () => ({}), - "GetDisabledTemplates": () => ({ "units/iber/infantry_swordsman_b": true }), - "GetPlayerID": () => playerId - }); - - cmpProductionQueue.CalculateEntitiesMap(); - TS_ASSERT_UNEVAL_EQUALS( - cmpProductionQueue.GetEntitiesList(), - ["units/iber/cavalry_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; @@ -481,9 +191,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 +200,6 @@ // player "GetCiv": () => "test", "GetDisabledTemplates": () => [], - "GetDisabledTechnologies": () => [], "TryReservePopulationSlots": () => false, // Always have pop space. "TrySubtractResources": () => true, "UnBlockTraining": () => {}, @@ -501,11 +207,7 @@ "GetPlayerID": () => 1, // entitylimits "ChangeCount": () => {}, - "AllowedToTrain": () => true, - // techmanager - "CheckTechnologyRequirements": () => true, - "IsTechnologyResearched": () => false, - "IsInProgress": () => false + "AllowedToTrain": () => true })); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryOwnerInterface); @@ -518,9 +220,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, {}); @@ -617,10 +316,9 @@ 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(); +*/ \ No newline at end of file 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. +id = cmpResearcher.QueueTechnology(queuedTech); +TS_ASSERT_EQUALS(cmpResearcher.GetResearchingTechnology(id).progress, 0); +TS_ASSERT_EQUALS(cmpResearcher.Progress(id, 500), 0); +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); Index: binaries/data/mods/public/simulation/components/tests/test_Trainer.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_Trainer.js @@ -0,0 +1,272 @@ +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": () => {} +}); + +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": () => {}, +}); +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), 0); +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 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) => { + QueryOwnerInterface(id, IID_EntityLimits).OnGlobalOwnershipChanged({ + "entity": id, + "from": -1, + "to": pid + }); + }, + "GetOwner": () => playerID + }); + + return id; +}; +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), 0); +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);