Index: ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27007) +++ ps/trunk/binaries/data/mods/public/simulation/components/Upgrade.js (revision 27008) @@ -1,363 +1,369 @@ function Upgrade() {} const UPGRADING_PROGRESS_INTERVAL = 250; Upgrade.prototype.Schema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeInteger") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Upgrade.prototype.Init = function() { - this.upgrading = false; - this.completed = false; this.elapsedTime = 0; - this.timer = undefined; this.expendedResources = {}; - - this.upgradeTemplates = {}; - - for (let choice in this.template) - { - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - let name = this.template[choice].Entity; - if (cmpIdentity) - name = name.replace(/\{civ\}/g, cmpIdentity.GetCiv()); - if (this.upgradeTemplates.name) - warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used."); - this.upgradeTemplates[name] = choice; - } }; // This will also deal with the "OnDestroy" case. Upgrade.prototype.OnOwnershipChanged = function(msg) { if (!this.completed) this.CancelUpgrade(msg.from); if (msg.to != INVALID_PLAYER) + { this.owner = msg.to; + this.DetermineUpgrades(); + } +}; + +Upgrade.prototype.DetermineUpgrades = function() +{ + this.upgradeTemplates = {}; + + for (const choice in this.template) + { + const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity).GetCiv(); + const playerCiv = QueryPlayerIDInterface(this.owner, IID_Identity).GetCiv(); + const name = this.template[choice].Entity. + replace(/\{native\}/g, nativeCiv). + replace(/\{civ\}/g, playerCiv); + + if (!Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).TemplateExists(name)) + continue; + + if (this.upgradeTemplates[name]) + warn("Upgrade Component: entity " + this.entity + " has two upgrades to the same entity, only the last will be used."); + + this.upgradeTemplates[name] = choice; + } }; Upgrade.prototype.ChangeUpgradedEntityCount = function(amount) { if (!this.IsUpgrading()) return; let cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let template = cmpTempMan.GetTemplate(this.upgrading); let categoryTo; if (template.TrainingRestrictions) categoryTo = template.TrainingRestrictions.Category; else if (template.BuildRestrictions) categoryTo = template.BuildRestrictions.Category; if (!categoryTo) return; let categoryFrom; let cmpTrainingRestrictions = Engine.QueryInterface(this.entity, IID_TrainingRestrictions); let cmpBuildRestrictions = Engine.QueryInterface(this.entity, IID_BuildRestrictions); if (cmpTrainingRestrictions) categoryFrom = cmpTrainingRestrictions.GetCategory(); else if (cmpBuildRestrictions) categoryFrom = cmpBuildRestrictions.GetCategory(); if (categoryTo == categoryFrom) return; let cmpEntityLimits = QueryPlayerIDInterface(this.owner, IID_EntityLimits); if (cmpEntityLimits) cmpEntityLimits.ChangeCount(categoryTo, amount); }; Upgrade.prototype.CanUpgradeTo = function(template) { return this.upgradeTemplates[template] !== undefined; }; Upgrade.prototype.GetUpgrades = function() { let ret = []; - let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); - - for (let option in this.template) + for (const option in this.upgradeTemplates) { - let choice = this.template[option]; - let templateName = cmpIdentity ? choice.Entity.replace(/\{civ\}/g, cmpIdentity.GetCiv()) : choice.Entity; + const choice = this.template[this.upgradeTemplates[option]]; let cost = {}; if (choice.Cost) - cost = this.GetResourceCosts(templateName); + cost = this.GetResourceCosts(option); if (choice.Time) - cost.time = this.GetUpgradeTime(templateName); + cost.time = this.GetUpgradeTime(option); let hasCost = choice.Cost || choice.Time; ret.push({ - "entity": templateName, + "entity": option, "icon": choice.Icon || undefined, "cost": hasCost ? cost : undefined, "tooltip": choice.Tooltip || undefined, "requiredTechnology": this.GetRequiredTechnology(option), }); } return ret; }; Upgrade.prototype.CancelTimer = function() { if (!this.timer) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); - this.timer = undefined; + delete this.timer; }; Upgrade.prototype.IsUpgrading = function() { return !!this.upgrading; }; Upgrade.prototype.GetUpgradingTo = function() { return this.upgrading; }; Upgrade.prototype.WillCheckPlacementRestrictions = function(template) { if (!this.upgradeTemplates[template]) return undefined; // is undefined by default so use X in Y return "CheckPlacementRestrictions" in this.template[this.upgradeTemplates[template]]; }; Upgrade.prototype.GetRequiredTechnology = function(templateArg) { let choice = this.upgradeTemplates[templateArg] || templateArg; if (this.template[choice].RequiredTechnology) return this.template[choice].RequiredTechnology; if (!("RequiredTechnology" in this.template[choice])) return undefined; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); let entType = this.template[choice].Entity; if (cmpIdentity) entType = entType.replace(/\{civ\}/g, cmpIdentity.GetCiv()); let template = cmpTemplateManager.GetTemplate(entType); return template.Identity.RequiredTechnology || undefined; }; Upgrade.prototype.GetResourceCosts = function(template) { if (!this.upgradeTemplates[template]) return undefined; if (this.IsUpgrading() && template == this.GetUpgradingTo()) return clone(this.expendedResources); let choice = this.upgradeTemplates[template]; if (!this.template[choice].Cost) return {}; let costs = {}; for (let r in this.template[choice].Cost) costs[r] = ApplyValueModificationsToEntity("Upgrade/Cost/"+r, +this.template[choice].Cost[r], this.entity); return costs; }; Upgrade.prototype.Upgrade = function(template) { if (this.IsUpgrading() || !this.upgradeTemplates[template]) return false; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (!cmpPlayer) return false; let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue && cmpProductionQueue.HasQueuedProduction()) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Entity is producing. Cannot start upgrading."), "translateMessage": true }); return false; } this.expendedResources = this.GetResourceCosts(template); if (!cmpPlayer || !cmpPlayer.TrySubtractResources(this.expendedResources)) { this.expendedResources = {}; return false; } this.upgrading = template; this.SetUpgradeAnimationVariant(); // Prevent cheating this.ChangeUpgradedEntityCount(1); if (this.GetUpgradeTime(template) !== 0) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_Upgrade, "UpgradeProgress", 0, UPGRADING_PROGRESS_INTERVAL, { "upgrading": template }); } else this.UpgradeProgress(); return true; }; Upgrade.prototype.CancelUpgrade = function(owner) { if (!this.IsUpgrading()) return; let cmpPlayer = QueryPlayerIDInterface(owner, IID_Player); if (cmpPlayer) cmpPlayer.AddResources(this.expendedResources); this.expendedResources = {}; this.ChangeUpgradedEntityCount(-1); // Do not update visual actor if the animation didn't change. let choice = this.upgradeTemplates[this.upgrading]; if (choice && this.template[choice].Variant) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); } - this.upgrading = false; + delete this.upgrading; this.CancelTimer(); this.SetElapsedTime(0); }; Upgrade.prototype.GetUpgradeTime = function(templateArg) { let template = this.upgrading || templateArg; let choice = this.upgradeTemplates[template]; if (!choice) return undefined; if (!this.template[choice].Time) return 0; return ApplyValueModificationsToEntity("Upgrade/Time", +this.template[choice].Time, this.entity); }; Upgrade.prototype.GetElapsedTime = function() { return this.elapsedTime; }; Upgrade.prototype.GetProgress = function() { if (!this.IsUpgrading()) return undefined; return this.GetUpgradeTime() == 0 ? 1 : Math.min(this.elapsedTime / 1000.0 / this.GetUpgradeTime(), 1.0); }; Upgrade.prototype.SetElapsedTime = function(time) { this.elapsedTime = time; Engine.PostMessage(this.entity, MT_UpgradeProgressUpdate, null); }; Upgrade.prototype.SetUpgradeAnimationVariant = function() { let choice = this.upgradeTemplates[this.upgrading]; if (!choice || !this.template[choice].Variant) return; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (!cmpVisual) return; cmpVisual.SelectAnimation(this.template[choice].Variant, false, 1.0); }; Upgrade.prototype.UpgradeProgress = function(data, lateness) { if (this.elapsedTime/1000.0 < this.GetUpgradeTime()) { this.SetElapsedTime(this.GetElapsedTime() + UPGRADING_PROGRESS_INTERVAL + lateness); return; } this.CancelTimer(); this.completed = true; this.ChangeUpgradedEntityCount(-1); this.expendedResources = {}; let newEntity = ChangeEntityTemplate(this.entity, this.upgrading); if (newEntity) PlaySound("upgraded", newEntity); }; Engine.RegisterComponentType(IID_Upgrade, "Upgrade", Upgrade); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27007) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js (revision 27008) @@ -1,165 +1,170 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Resources = { "BuildSchema": type => { let schema = ""; for (let res of ["food", "metal", "stone", "wood"]) schema += "" + "" + "" + "" + ""; return "" + schema + ""; } }; Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/ModifiersManager.js"); // Provides `IID_ModifiersManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("Upgrade.js"); // Input (bare minimum needed for tests): let techs = { "alter_tower_upgrade_cost": { "modifications": [ { "value": "Upgrade/Cost/stone", "add": 60.0 }, { "value": "Upgrade/Cost/wood", "multiply": 0.5 }, { "value": "Upgrade/Time", "replace": 90 } ], "affects": ["Tower"] } }; let template = { "Identity": { "Classes": { '@datatype': "tokens", "_string": "Tower" }, "VisibleClasses": { '@datatype': "tokens", "_string": "" } }, "Upgrade": { "Tower": { "Cost": { "stone": "100", "wood": "50" }, "Entity": "structures/{civ}/defense_tower", "Time": "100" } } }; let civCode = "pony"; let playerID = 1; // Usually, the tech modifications would be worked out by the TechnologyManager // with assistance from globalscripts. This test is not about testing the // TechnologyManager, so the modifications (both with and without the technology // researched) are worked out before hand and placed here. let isResearched = false; let templateTechModifications = { "without": {}, "with": { "Upgrade/Cost/stone": [{ "affects": [["Tower"]], "add": 60 }], "Upgrade/Cost/wood": [{ "affects": [["Tower"]], "multiply": 0.5 }], "Upgrade/Time": [{ "affects": [["Tower"]], "replace": 90 }] } }; let entityTechModifications = { "without": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 100 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 50 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 100 } } }, "with": { 'Upgrade/Cost/stone': { "20": { "origValue": 100, "newValue": 160 } }, 'Upgrade/Cost/wood': { "20": { "origValue": 50, "newValue": 25 } }, 'Upgrade/Time': { "20": { "origValue": 100, "newValue": 90 } } } }; /** * Initialise various bits. */ // System Entities: AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": pID => 10 // Called in helpers/player.js::QueryPlayerIDInterface(), as part of Tests T2 and T5. }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { - "GetTemplate": () => template // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). + "GetTemplate": () => template, // Called in components/Upgrade.js::ChangeUpgradedEntityCount(). + "TemplateExists": (templ) => true }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetInterval": () => 1, // Called in components/Upgrade.js::Upgrade(). "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); AddMock(SYSTEM_ENTITY, IID_ModifiersManager, { "ApplyTemplateModifiers": (valueName, curValue, template, player) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. let mods = isResearched ? templateTechModifications.with : templateTechModifications.without; if (mods[valueName]) return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); return curValue; }, "ApplyModifiers": (valueName, curValue, ent) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToEntity() // as part of Tests T3, T6 and T7 below. let mods = isResearched ? entityTechModifications.with : entityTechModifications.without; return mods[valueName][ent].newValue; } }); // Init Player: AddMock(10, IID_Player, { "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). }); +AddMock(10, IID_Identity, { + "GetCiv": () => civCode +}); // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). }); AddMock(20, IID_Identity, { "GetCiv": () => civCode // Called in components/Upgrade.js::init(). }); AddMock(20, IID_ProductionQueue, { "HasQueuedProduction": () => false }); let cmpUpgrade = ConstructComponent(20, "Upgrade", template.Upgrade); cmpUpgrade.owner = playerID; +cmpUpgrade.OnOwnershipChanged({ "to": playerID }); /** * Now to start the test proper * To start with, no techs are researched... */ // T1: Check the cost of the upgrade without a player value being passed (as it would be in the structree). let parsed_template = GetTemplateDataHelper(template, null, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T2: Check the value, with a player ID (as it would be in-session). parsed_template = GetTemplateDataHelper(template, playerID, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T3: Check that the value is correct within the Update Component. TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 100 }); /** * Tell the Upgrade component to start the Upgrade, * then mark the technology that alters the upgrade cost as researched. */ cmpUpgrade.Upgrade("structures/" + civCode + "/defense_tower"); isResearched = true; // T4: Check that the player-less value hasn't increased... parsed_template = GetTemplateDataHelper(template, null, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 100, "wood": 50, "time": 100 }); // T5: ...but the player-backed value has. parsed_template = GetTemplateDataHelper(template, playerID, {}); TS_ASSERT_UNEVAL_EQUALS(parsed_template.upgrades[0].cost, { "stone": 160, "wood": 25, "time": 90 }); // T6: The upgrade component should still be using the old resource cost (but new time cost) for the upgrade in progress... TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 100, "wood": 50, "time": 90 }); // T7: ...but with the upgrade cancelled, it now uses the modified value. cmpUpgrade.CancelUpgrade(playerID); TS_ASSERT_UNEVAL_EQUALS(cmpUpgrade.GetUpgrades()[0].cost, { "stone": 160, "wood": 25, "time": 90 }); Index: ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27007) +++ ps/trunk/binaries/data/mods/public/simulation/templates/structures/han/civil_centre.xml (revision 27008) @@ -1,47 +1,47 @@ 8.0 Minister han Gōngdiàn -unlock_spies -spy_counter units/{civ}/infantry_spearman_b units/{civ}/infantry_archer_b units/{civ}/cavalry_swordsman_b - structures/han/civil_centre_court + structures/{civ}/civil_centre_court This greatly increases the health, capture resistance, and garrison capacity of this specific Civic Center. Unlock training of Heroes here and reduce its research and batch training times by half. phase_city 300 300 upgrading structures/fndn_8x8.xml structures/han/civil_centre.xml