Index: binaries/data/mods/public/globalscripts/Resources.js =================================================================== --- binaries/data/mods/public/globalscripts/Resources.js +++ binaries/data/mods/public/globalscripts/Resources.js @@ -7,6 +7,7 @@ this.resourceDataObj = {}; this.resourceCodes = []; this.resourceNames = {}; + this.resourceCodesByProperty = {}; for (let filename of Engine.ListDirectoryFiles("simulation/data/resources/", "*.json", false)) { @@ -30,27 +31,36 @@ this.resourceNames[data.code] = data.name; for (let subres in data.subtypes) this.resourceNames[subres] = data.subtypes[subres]; + + for (let property in data.properties) + { + if (!this.resourceCodesByProperty[data.properties[property]]) + this.resourceCodesByProperty[data.properties[property]] = []; + this.resourceCodesByProperty[data.properties[property]].push(data.code); + } } // Sort arrays by specified order - let resSort = (a, b) => - a.order < b.order ? -1 : - a.order > b.order ? +1 : 0; - - this.resourceData.sort(resSort); - this.resourceCodes.sort((a, b) => resSort( + let resDataSort = (a, b) => a.order < b.order ? -1 : a.order > b.order ? +1 : 0; + let resSort = (a, b) => resDataSort( this.resourceData.find(resource => resource.code == a), this.resourceData.find(resource => resource.code == b) - )); + ); + + this.resourceData.sort(resDataSort); + this.resourceCodes.sort(resSort); + for (let property in this.resourceCodesByProperty) + this.resourceCodesByProperty[property].sort(resSort); deepfreeze(this.resourceData); deepfreeze(this.resourceDataObj); deepfreeze(this.resourceCodes); deepfreeze(this.resourceNames); + deepfreeze(this.resourceCodesByProperty); } /** - * Returns the objects defined in the JSON files for all availbale resources, + * Returns the objects defined in the JSON files for all available resources, * ordered as defined in these files. */ Resources.prototype.GetResources = function() @@ -68,11 +78,15 @@ /** * Returns an array containing all resource codes ordered as defined in the resource files. - * For example ["food", "wood", "stone", "metal"]. + * @param {string} property - the property e.g. ("tradable", "tributable" etc.) that the resource ought to have + * @return {string[]} - data of the form [ "food", "wood", ... ] */ -Resources.prototype.GetCodes = function() +Resources.prototype.GetCodes = function(property) { - return this.resourceCodes; + if (!property) + return this.resourceCodes; + + return this.resourceCodesByProperty[property] || []; }; /** Index: binaries/data/mods/public/globalscripts/tests/test_resources.js =================================================================== --- /dev/null +++ binaries/data/mods/public/globalscripts/tests/test_resources.js @@ -0,0 +1,47 @@ +let resources = { + "res_A": { + "code": "a", + "name": "A", + "subtypes": { + "aa": "AA", + "aaa": "AAA" + }, + "order": 2, + "properties": ["prop_a", "prop_common"] + }, + "res_B": { + "code": "b", + "name": "B", + "subtypes": { + "bb": "BB", + "bbb": "BBB" + }, + "order": 1, + "properties": ["prop_b", "prop_common"] + } +}; + +Engine.ListDirectoryFiles = () => Object.keys(resources); +Engine.ReadJSONFile = (file) => resources[file]; + +let res = new Resources(); + +TS_ASSERT_EQUALS(res.GetResources().length, 2); +TS_ASSERT_EQUALS(res.GetResources()[0].code, "b"); + +TS_ASSERT_EQUALS(res.GetResource("b").order, 1); + +TS_ASSERT_UNEVAL_EQUALS(res.GetCodes(), ["b", "a"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetCodes("prop_common"), ["b", "a"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetCodes("prop_a"), ["a"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetCodes("prop_b"), ["b"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetCodes("prop_none"), []); + +TS_ASSERT_UNEVAL_EQUALS(res.GetNames(), { + "a": "A", + "aa": "AA", + "aaa": "AAA", + "b": "B", + "bb": "BB", + "bbb": "BBB" +}); Index: binaries/data/mods/public/gui/session/menu.js =================================================================== --- binaries/data/mods/public/gui/session/menu.js +++ binaries/data/mods/public/gui/session/menu.js @@ -290,7 +290,7 @@ let size = dialog.size; let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size; - let widthOffset = g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left) / 2; + let widthOffset = g_ResourceData.GetCodes("tributable").length * (tribSize.right - tribSize.left) / 2; size.left -= widthOffset; size.right += widthOffset; @@ -491,7 +491,7 @@ function diplomacyFormatTributeButtons(i, hidden) { - let resCodes = g_ResourceData.GetCodes(); + let resCodes = g_ResourceData.GetCodes("tributable"); let r = 0; for (let resCode of resCodes) { @@ -638,7 +638,8 @@ let width = size.right - size.left; let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size; - width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left); + let length = Math.max(g_ResourceData.GetCodes("tradable").length, g_ResourceData.GetCodes("barterable").length); + width += length * (tradeSize.right - tradeSize.left); size.left = -width / 2; size.right = width / 2; @@ -656,8 +657,9 @@ let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer); let button = {}; - let resCodes = g_ResourceData.GetCodes(); - let currTradeSelection = resCodes[0]; + let resTradeCodes = g_ResourceData.GetCodes("tradable"); + let resBarterCodes = g_ResourceData.GetCodes("barterable"); + let currTradeSelection = resTradeCodes[0]; let updateTradeButtons = function() { @@ -671,12 +673,13 @@ } }; - hideRemaining("tradeResources", resCodes.length); + hideRemaining("tradeResources", resTradeCodes.length); Engine.GetGUIObjectByName("tradeHelp").hidden = false; - for (let i = 0; i < resCodes.length; ++i) + + for (let i = 0; i < resBarterCodes.length; ++i) { - let resCode = resCodes[i]; + let resBarterCode = resBarterCodes[i]; let barterResource = Engine.GetGUIObjectByName("barterResource[" + i + "]"); if (!barterResource) @@ -685,11 +688,14 @@ break; } - // Barter: - barterOpenCommon(resCode, i, "barter"); + barterOpenCommon(resBarterCode, i, "barter"); setPanelObjectPosition(barterResource, i, i + 1); + } + + for (let i = 0; i < resTradeCodes.length; ++i) + { + let resTradeCode = resTradeCodes[i]; - // Trade: let tradeResource = Engine.GetGUIObjectByName("tradeResource[" + i + "]"); if (!tradeResource) { @@ -700,19 +706,19 @@ setPanelObjectPosition(tradeResource, i, i + 1); let icon = Engine.GetGUIObjectByName("tradeResourceIcon[" + i + "]"); - icon.sprite = "stretched:session/icons/resources/" + resCode + ".png"; + icon.sprite = "stretched:session/icons/resources/" + resTradeCode + ".png"; let buttonUp = Engine.GetGUIObjectByName("tradeArrowUp[" + i + "]"); let buttonDn = Engine.GetGUIObjectByName("tradeArrowDn[" + i + "]"); - button[resCode] = { + button[resTradeCode] = { "up": buttonUp, "dn": buttonDn, "label": Engine.GetGUIObjectByName("tradeResourceText[" + i + "]"), "sel": Engine.GetGUIObjectByName("tradeResourceSelection[" + i + "]") }; - proba[resCode] = proba[resCode] || 0; + proba[resTradeCode] = proba[resTradeCode] || 0; let buttonResource = Engine.GetGUIObjectByName("tradeResourceButton[" + i + "]"); buttonResource.enabled = controlsPlayer(g_ViewedPlayer); @@ -720,7 +726,7 @@ return () => { if (Engine.HotkeyIsPressed("session.fulltradeswap")) { - for (let res of resCodes) + for (let res of resTradeCodes) proba[res] = 0; proba[resource] = 100; Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); @@ -728,7 +734,7 @@ currTradeSelection = resource; updateTradeButtons(); }; - })(resCode); + })(resTradeCode); buttonUp.enabled = controlsPlayer(g_ViewedPlayer); buttonUp.onPress = (resource => { @@ -738,7 +744,7 @@ Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; - })(resCode); + })(resTradeCode); buttonDn.enabled = controlsPlayer(g_ViewedPlayer); buttonDn.onPress = (resource => { @@ -748,7 +754,7 @@ Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; - })(resCode); + })(resTradeCode); } updateTradeButtons(); @@ -765,7 +771,7 @@ function initBarterButtons() { - g_BarterSell = g_ResourceData.GetCodes()[0]; + g_BarterSell = g_ResourceData.GetCodes("barterable")[0]; } /** @@ -873,7 +879,9 @@ Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter; if (canBarter) - g_ResourceData.GetCodes().forEach((resCode, i) => { barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer); }); + g_ResourceData.GetCodes("barterable").forEach((resCode, i) => { + barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer); + }); } function getIdleLandTradersText(traderNumber) Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -84,9 +84,9 @@ "getItems": function(unitEntStates) { // If more than `rowLength` resources, don't display icons. - if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetCodes().length > this.rowLength) + if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetCodes("barterable").length > this.rowLength) return []; - return g_ResourceData.GetCodes(); + return g_ResourceData.GetCodes("barterable"); }, "setupButton": function(data) { Index: binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js +++ binaries/data/mods/public/simulation/ai/petra/diplomacyManager.js @@ -68,6 +68,7 @@ this.nextTributeUpdate = gameState.ai.elapsedTime + 30; let totalResources = gameState.getResources(); let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + let resTribCodes = Resources.GetCodes("tributable"); let mostNeeded; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { @@ -78,7 +79,7 @@ let allyPop = gameState.sharedScript.playersData[i].popCount; let tribute = {}; let toSend = false; - for (let res in allyResources) + for (let res in resTribCodes) { if (donor && availableResources[res] > 200 && allyResources[res] < 0.2 * availableResources[res]) { @@ -402,17 +403,29 @@ } else { - response = "acceptWithTribute"; - requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState)[0]; - requiredTribute.wanted = Math.max(1000, gameState.getOwnUnits().length * (requestType === "ally" ? 10 : 5)); - this.receivedDiplomacyRequests.set(player, { - "status": "waitingForTribute", - "wanted": requiredTribute.wanted, - "type": requiredTribute.type, - "warnTime": gameState.ai.elapsedTime + 60, - "sentWarning": false, - "requestType": requestType - }); + // Try to request a tribute. + // If a resource is not tributable, do not request it. + // If no resources are tributable, decline. + let tributableResources = Resources.GetCodes("tributable"); + requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState).find(res => tributableResources.indexOf(res.type) != -1); + if (requiredTribute) + { + response = "acceptWithTribute"; + requiredTribute.wanted = Math.max(1000, gameState.getOwnUnits().length * (requestType === "ally" ? 10 : 5)); + this.receivedDiplomacyRequests.set(player, { + "status": "waitingForTribute", + "wanted": requiredTribute.wanted, + "type": requiredTribute.type, + "warnTime": gameState.ai.elapsedTime + 60, + "sentWarning": false, + "requestType": requestType + }); + } + else + { + this.receivedDiplomacyRequests.set(player, { "requestType": requestType, "status": "declinedRequest" }); + response = "decline"; + } } m.chatAnswerRequestDiplomacy(gameState, player, requestType, response, requiredTribute); }; Index: binaries/data/mods/public/simulation/ai/petra/tradeManager.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/tradeManager.js +++ binaries/data/mods/public/simulation/ai/petra/tradeManager.js @@ -162,7 +162,8 @@ m.TradeManager.prototype.setTradingGoods = function(gameState) { let tradingGoods = {}; - for (let res of Resources.GetCodes()) + let resTradeCodes = Resources.GetCodes("tradable"); + for (let res of resTradeCodes) tradingGoods[res] = 0; // first, try to anticipate future needs let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); @@ -170,7 +171,7 @@ let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); let remaining = 100; let targetNum = this.Config.Economy.targetNumTraders; - for (let res in stocks) + for (let res in resTradeCodes) { if (res == "food") continue; @@ -230,7 +231,8 @@ let getBarterRate = (prices, buy, sell) => Math.round(100 * prices.sell[sell] / prices.buy[buy]); // loop through each missing resource checking if we could barter and help finishing a queue quickly. - for (let buy of Resources.GetCodes()) + let resBarterCodes = Resources.GetCodes("barterable"); + for (let buy of resBarterCodes) { // Check if our rate allows to gather it fast enough if (needs[buy] == 0 || needs[buy] < rates[buy] * 30) @@ -239,7 +241,7 @@ // Pick the best resource to barter. let bestToSell; let bestRate = 0; - for (let sell of Resources.GetCodes()) + for (let sell of resBarterCodes) { if (sell == buy) continue; @@ -291,11 +293,11 @@ } // now do contingency bartering, selling food to buy finite resources (and annoy our ennemies by increasing prices) - if (available.food < 1000 || needs.food > 0) + if (available.food < 1000 || needs.food > 0 || resBarterCodes.indexOf("food") == -1) return false; let bestToBuy; let bestChoice = 0; - for (let buy of Resources.GetCodes()) + for (let buy of resBarterCodes) { if (buy == "food") continue; Index: binaries/data/mods/public/simulation/components/Barter.js =================================================================== --- binaries/data/mods/public/simulation/components/Barter.js +++ binaries/data/mods/public/simulation/components/Barter.js @@ -29,16 +29,16 @@ Barter.prototype.Init = function() { this.priceDifferences = {}; - for (let resource of Resources.GetCodes()) + for (let resource of Resources.GetCodes("barterable")) this.priceDifferences[resource] = 0; this.restoreTimer = undefined; }; Barter.prototype.GetPrices = function(playerID) { - var prices = { "buy": {}, "sell": {} }; + let prices = { "buy": {}, "sell": {} }; let multiplier = QueryPlayerIDInterface(playerID).GetBarterMultiplier(); - for (let resource of Resources.GetCodes()) + for (let resource of Resources.GetCodes("barterable")) { let truePrice = Resources.GetResource(resource).truePrice; prices.buy[resource] = truePrice * (100 + this.CONSTANT_DIFFERENCE + this.priceDifferences[resource]) * multiplier.buy[resource] / 100; @@ -69,7 +69,7 @@ return; } - let availResources = Resources.GetCodes(); + let availResources = Resources.GetCodes("barterable"); if (availResources.indexOf(resourceToSell) == -1) { warn("ExchangeResources: incorrect resource to sell: " + uneval(resourceToSell)); @@ -135,11 +135,11 @@ Barter.prototype.ProgressTimeout = function(data) { - var needRestore = false; - for (let resource of Resources.GetCodes()) + let needRestore = false; + for (let resource of Resources.GetCodes("barterable")) { // Calculate value to restore, it should be limited to [-DIFFERENCE_RESTORE; DIFFERENCE_RESTORE] interval - var differenceRestore = Math.min(this.DIFFERENCE_RESTORE, Math.max(-this.DIFFERENCE_RESTORE, this.priceDifferences[resource])); + let differenceRestore = Math.min(this.DIFFERENCE_RESTORE, Math.max(-this.DIFFERENCE_RESTORE, this.priceDifferences[resource])); differenceRestore = -differenceRestore; this.priceDifferences[resource] += differenceRestore; // If price difference still exists then set flag to run timer again @@ -149,7 +149,7 @@ if (!needRestore) { - var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.restoreTimer); this.restoreTimer = undefined; } 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 @@ -80,20 +80,22 @@ "sell": clone(this.template.BarterMultiplier.Sell) }; - // Initial resources and trading goods probability in steps of 5 + // Initial resources let resCodes = Resources.GetCodes(); - let quotient = Math.floor(20 / resCodes.length); - let remainder = 20 % resCodes.length; - for (let i in resCodes) + for (let res of resCodes) { - let res = resCodes[i]; this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; + } + // Trading goods probability in steps of 5 + let resTradeCodes = Resources.GetCodes("tradable"); + let quotient = Math.floor(20 / resTradeCodes.length); + let remainder = 20 % resTradeCodes.length; + for (let i in resTradeCodes) this.tradingGoods.push({ - "goods": res, + "goods": resTradeCodes[i], "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); - } }; Player.prototype.SetPlayerID = function(id) @@ -420,11 +422,11 @@ Player.prototype.SetTradingGoods = function(tradingGoods) { - let resCodes = Resources.GetCodes(); + let resTradeCodes = Resources.GetCodes("tradable"); let sumProba = 0; for (let resource in tradingGoods) { - if (resCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) + if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; @@ -434,7 +436,7 @@ if (sumProba != 100) { - error("Invalid trading goods: " + uneval(tradingGoods)); + error("Invalid trading goods probability: " + uneval(sumProba)); return; } @@ -858,15 +860,16 @@ Player.prototype.TributeResource = function(player, amounts) { - var cmpPlayer = QueryPlayerIDInterface(player); + let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; + let resTribCodes = Resources.GetCodes("tributable"); for (let resCode in amounts) - if (Resources.GetCodes().indexOf(resCode) == -1 || + if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { Index: binaries/data/mods/public/simulation/data/resources/food.json =================================================================== --- binaries/data/mods/public/simulation/data/resources/food.json +++ binaries/data/mods/public/simulation/data/resources/food.json @@ -9,6 +9,7 @@ "grain": "Grain", "meat": "Meat" }, + "properties": ["barterable", "tradable", "tributable"], "truePrice": 100, "aiAnalysisInfluenceGroup": "ignore" } Index: binaries/data/mods/public/simulation/data/resources/metal.json =================================================================== --- binaries/data/mods/public/simulation/data/resources/metal.json +++ binaries/data/mods/public/simulation/data/resources/metal.json @@ -6,6 +6,7 @@ "subtypes": { "ore": "Ore" }, + "properties": ["barterable", "tradable", "tributable"], "truePrice": 100, "aiAnalysisInfluenceGroup": "sparse" } Index: binaries/data/mods/public/simulation/data/resources/stone.json =================================================================== --- binaries/data/mods/public/simulation/data/resources/stone.json +++ binaries/data/mods/public/simulation/data/resources/stone.json @@ -7,6 +7,7 @@ "rock": "Rock", "ruins": "Ruins" }, + "properties": ["barterable", "tradable", "tributable"], "truePrice": 100, "aiAnalysisInfluenceGroup": "sparse" } Index: binaries/data/mods/public/simulation/data/resources/wood.json =================================================================== --- binaries/data/mods/public/simulation/data/resources/wood.json +++ binaries/data/mods/public/simulation/data/resources/wood.json @@ -7,6 +7,7 @@ "tree": "Tree", "ruins": "Ruins" }, + "properties": ["barterable", "tradable", "tributable"], "truePrice": 100, "aiAnalysisInfluenceGroup": "abundant" } Index: source/simulation2/components/tests/test_scripts.h =================================================================== --- source/simulation2/components/tests/test_scripts.h +++ source/simulation2/components/tests/test_scripts.h @@ -56,6 +56,19 @@ TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } + void test_global_scripts() + { + VfsPaths paths; + TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); + for (const VfsPath& path : paths) + { + CSimContext context; + CComponentManager componentManager(context, g_ScriptRuntime, true); + ScriptTestSetup(componentManager.GetScriptInterface()); + load_script(componentManager.GetScriptInterface(), path); + } + } + void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js"))