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,7 +78,7 @@ /** * Returns an array containing all resource codes ordered as defined in the resource files. - * For example ["food", "wood", "stone", "metal"]. + * @return {string[]} - Data of the form [ "food", "wood", ... ]. */ Resources.prototype.GetCodes = function() { @@ -76,6 +86,33 @@ }; /** + * Returns an array containing all barterable resource codes ordered as defined in the resource files. + * @return {string[]} - Data of the form [ "food", "wood", ... ]. + */ +Resources.prototype.GetBarterableCodes = function() +{ + return this.resourceCodesByProperty["barterable"] || []; +}; + +/** + * Returns an array containing all tradable resource codes ordered as defined in the resource files. + * @return {string[]} - Data of the form [ "food", "wood", ... ]. + */ +Resources.prototype.GetTradableCodes = function() +{ + return this.resourceCodesByProperty["tradable"] || []; +}; + +/** + * Returns an array containing all tributable resource codes ordered as defined in the resource files. + * @return {string[]} - Data of the form [ "food", "wood", ... ]. + */ +Resources.prototype.GetTributableCodes = function() +{ + return this.resourceCodesByProperty["tributable"] || []; +}; + +/** * Returns an object mapping resource codes to translatable resource names. Includes subtypes. * For example { "food": "Food", "fish": "Fish", "fruit": "Fruit", "metal": "Metal", ... } */ Index: binaries/data/mods/public/globalscripts/tests/test_resources.js =================================================================== --- /dev/null +++ binaries/data/mods/public/globalscripts/tests/test_resources.js @@ -0,0 +1,46 @@ +let resources = { + "res_A": { + "code": "a", + "name": "A", + "subtypes": { + "aa": "AA", + "aaa": "AAA" + }, + "order": 2, + "properties": ["barterable", "tributable"] + }, + "res_B": { + "code": "b", + "name": "B", + "subtypes": { + "bb": "BB", + "bbb": "BBB" + }, + "order": 1, + "properties": ["tributable"] + } +}; + +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.GetTributableCodes(), ["b", "a"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetBarterableCodes(), ["a"]); +TS_ASSERT_UNEVAL_EQUALS(res.GetTradableCodes(), []); + +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 @@ -289,10 +289,16 @@ let dialog = Engine.GetGUIObjectByName("diplomacyDialogPanel"); let size = dialog.size; - let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size; - let widthOffset = g_ResourceData.GetCodes().length * (tribSize.right - tribSize.left) / 2; - size.left -= widthOffset; - size.right += widthOffset; + let resTribCodesLength = g_ResourceData.GetTributableCodes().length; + if (resTribCodesLength) + { + let tribSize = Engine.GetGUIObjectByName("diplomacyPlayer[0]_tribute[0]").size; + let widthOffset = resTribCodesLength * (tribSize.right - tribSize.left) / 2; + size.left -= widthOffset; + size.right += widthOffset; + } + else + Engine.GetGUIObjectByName("diplomacyHeaderTribute").hidden = true; let firstRow = Engine.GetGUIObjectByName("diplomacyPlayer[0]").size; let heightOffset = (g_Players.length - 1) * (firstRow.bottom - firstRow.top) / 2; @@ -491,9 +497,9 @@ function diplomacyFormatTributeButtons(i, hidden) { - let resCodes = g_ResourceData.GetCodes(); + let resTribCodes = g_ResourceData.GetTributableCodes(); let r = 0; - for (let resCode of resCodes) + for (let resCode of resTribCodes) { let button = Engine.GetGUIObjectByName("diplomacyPlayer[" + (i - 1) + "]_tribute[" + r + "]"); if (!button) @@ -516,15 +522,15 @@ // See INPUT_MASSTRIBUTING in input.js let multiplier = 1; return function() { - let isBatchTrainPressed = Engine.HotkeyIsPressed("session.masstribute"); - if (isBatchTrainPressed) + let isMassTributePressed = Engine.HotkeyIsPressed("session.masstribute"); + if (isMassTributePressed) { inputState = INPUT_MASSTRIBUTING; multiplier += multiplier == 1 ? 4 : 5; } let amounts = {}; - for (let res of resCodes) + for (let res of resTribCodes) amounts[res] = 0; amounts[resCode] = 100 * multiplier; @@ -538,7 +544,7 @@ button.tooltip = formatTributeTooltip(i, resCode, 100); }; - if (!isBatchTrainPressed) + if (!isMassTributePressed) g_FlushTributing(); }; })(i, resCode, button); @@ -637,8 +643,15 @@ let size = dialog.size; let width = size.right - size.left; + let resTradCodesLength = g_ResourceData.GetTradableCodes().length; + Engine.GetGUIObjectByName("tradeDialogPanelTrade").hidden = !resTradCodesLength; + + let resBarterCodesLength = g_ResourceData.GetBarterableCodes().length; + Engine.GetGUIObjectByName("tradeDialogPanelBarter").hidden = !resBarterCodesLength; + let tradeSize = Engine.GetGUIObjectByName("tradeResource[0]").size; - width += g_ResourceData.GetCodes().length * (tradeSize.right - tradeSize.left); + let length = Math.max(resTradCodesLength, resBarterCodesLength); + width += length * (tradeSize.right - tradeSize.left); size.left = -width / 2; size.right = width / 2; @@ -656,8 +669,9 @@ let proba = Engine.GuiInterfaceCall("GetTradingGoods", g_ViewedPlayer); let button = {}; - let resCodes = g_ResourceData.GetCodes(); - let currTradeSelection = resCodes[0]; + let resTradeCodes = g_ResourceData.GetTradableCodes(); + let resBarterCodes = g_ResourceData.GetBarterableCodes(); + let currTradeSelection = resTradeCodes[0]; let updateTradeButtons = function() { @@ -671,12 +685,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 +700,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 +718,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 +738,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 +746,7 @@ currTradeSelection = resource; updateTradeButtons(); }; - })(resCode); + })(resTradeCode); buttonUp.enabled = controlsPlayer(g_ViewedPlayer); buttonUp.onPress = (resource => { @@ -738,7 +756,7 @@ Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; - })(resCode); + })(resTradeCode); buttonDn.enabled = controlsPlayer(g_ViewedPlayer); buttonDn.onPress = (resource => { @@ -748,7 +766,7 @@ Engine.PostNetworkCommand({ "type": "set-trading-goods", "tradingGoods": proba }); updateTradeButtons(); }; - })(resCode); + })(resTradeCode); } updateTradeButtons(); @@ -765,7 +783,7 @@ function initBarterButtons() { - g_BarterSell = g_ResourceData.GetCodes()[0]; + g_BarterSell = g_ResourceData.GetBarterableCodes()[0]; } /** @@ -873,7 +891,9 @@ Engine.GetGUIObjectByName("barterHelp").hidden = !canBarter; if (canBarter) - g_ResourceData.GetCodes().forEach((resCode, i) => { barterUpdateCommon(resCode, i, "barter", g_ViewedPlayer); }); + g_ResourceData.GetBarterableCodes().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.GetBarterableCodes().length > this.rowLength) return []; - return g_ResourceData.GetCodes(); + return g_ResourceData.GetBarterableCodes(); }, "setupButton": function(data) { Index: binaries/data/mods/public/gui/session/session.js =================================================================== --- binaries/data/mods/public/gui/session/session.js +++ binaries/data/mods/public/gui/session/session.js @@ -680,7 +680,8 @@ Engine.GetGUIObjectByName("population").hidden = !isPlayer; Engine.GetGUIObjectByName("diplomacyButton").hidden = !isPlayer; - Engine.GetGUIObjectByName("tradeButton").hidden = !isPlayer; + Engine.GetGUIObjectByName("tradeButton").hidden = !isPlayer || + (!g_ResourceData.GetTradableCodes().length && !g_ResourceData.GetBarterableCodes().length); Engine.GetGUIObjectByName("observerText").hidden = isPlayer; let alphaLabel = Engine.GetGUIObjectByName("alphaLabel"); Index: binaries/data/mods/public/gui/session/trade_window.xml =================================================================== --- binaries/data/mods/public/gui/session/trade_window.xml +++ binaries/data/mods/public/gui/session/trade_window.xml @@ -10,7 +10,7 @@ - + Barter @@ -60,7 +60,7 @@ - + Trade 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,9 @@ this.nextTributeUpdate = gameState.ai.elapsedTime + 30; let totalResources = gameState.getResources(); let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); + let resTribCodes = Resources.GetTributableCodes(); + if (!resTribCodes.length) + return; let mostNeeded; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { @@ -78,7 +81,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]) { @@ -97,8 +100,8 @@ if (this.nextTributeRequest.has(res) && gameState.ai.elapsedTime < this.nextTributeRequest.get(res)) continue; if (!mostNeeded) - mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); - for (let k = 0; k < 2; ++k) + mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes); + for (let k = 0; k < mostNeeded.length; ++k) { if (mostNeeded[k].type == res && mostNeeded[k].wanted > 0) { @@ -402,17 +405,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 resTribCodes = Resources.GetTributableCodes(); + requiredTribute = gameState.ai.HQ.pickMostNeededResources(gameState, resTribCodes)[0]; + 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); }; @@ -511,7 +526,7 @@ { this.checkEvents(gameState, events); - if (!gameState.ai.HQ.saveResources && gameState.ai.elapsedTime > this.nextTributeUpdate) + if (Resources.GetTributableCodes().length && !gameState.ai.HQ.saveResources && gameState.ai.elapsedTime > this.nextTributeUpdate) this.tributes(gameState); if (this.waitingToBetray && gameState.ai.elapsedTime > this.betrayLapseTime) Index: binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- binaries/data/mods/public/simulation/ai/petra/headquarters.js +++ binaries/data/mods/public/simulation/ai/petra/headquarters.js @@ -902,13 +902,15 @@ * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ -m.HQ.prototype.pickMostNeededResources = function(gameState) +m.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = []) { let wantedRates = this.GetWantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); + if (!allowedResources.length) + allowedResources = Resources.GetCodes(); let needed = []; - for (let res in wantedRates) + for (let res of allowedResources) needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { @@ -1290,6 +1292,10 @@ if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; + // No need for more than one market when we cannot trade. + if (!Resources.GetTradableCodes().length) + return false; + // obstruction map let obstructions = m.createObstructionMap(gameState, 0, template); let halfSize = 0; 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 @@ -161,16 +161,19 @@ m.TradeManager.prototype.setTradingGoods = function(gameState) { + let resTradeCodes = Resources.GetTradableCodes(); + if (!resTradeCodes.length) + return; let tradingGoods = {}; - for (let res of Resources.GetCodes()) + for (let res of resTradeCodes) tradingGoods[res] = 0; // first, try to anticipate future needs let stocks = gameState.ai.HQ.getTotalResourceLevel(gameState); - let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); + let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState, resTradeCodes); let wantedRates = gameState.ai.HQ.GetWantedGatherRates(gameState); let remaining = 100; let targetNum = this.Config.Economy.targetNumTraders; - for (let res in stocks) + for (let res of resTradeCodes) { if (res == "food") continue; @@ -200,7 +203,7 @@ let nextNeed = remaining - mainNeed; tradingGoods[mostNeeded[0].type] += mainNeed; - if (mostNeeded[1].wanted > 0) + if (mostNeeded[1] && mostNeeded[1].wanted > 0) tradingGoods[mostNeeded[1].type] += nextNeed; else tradingGoods[mostNeeded[0].type] += nextNeed; @@ -218,6 +221,9 @@ let barterers = gameState.getOwnEntitiesByClass("BarterMarket", true).filter(API3.Filters.isBuilt()).toEntityArray(); if (barterers.length == 0) return false; + let resBarterCodes = Resources.GetBarterableCodes(); + if (!resBarterCodes.length) + return false; // Available resources after account substraction let available = gameState.ai.queueManager.getAvailableResources(gameState); @@ -230,7 +236,7 @@ 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()) + 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 +245,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 +297,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; @@ -409,6 +415,14 @@ */ m.TradeManager.prototype.checkRoutes = function(gameState, accessIndex) { + // If we cannot trade, do not bother checking routes. + if (!Resources.GetTradableCodes().length) + { + this.tradeRoute = undefined; + this.potentialTradeRoute = undefined; + return false; + } + let market1 = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Market"), gameState.getOwnStructures()); let market2 = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Market"), gameState.getExclusiveAllyEntities()); if (market1.length + market2.length < 2) // We have to wait ... markets will be built soon @@ -620,6 +634,8 @@ m.TradeManager.prototype.isNewMarketWorth = function(expectedGain) { + if (!Resources.GetTradableCodes().length) + return false; if (expectedGain < this.minimalGain) return false; if (this.potentialTradeRoute && expectedGain < 2*this.potentialTradeRoute.gain && @@ -630,7 +646,7 @@ m.TradeManager.prototype.update = function(gameState, events, queues) { - if (gameState.ai.HQ.canBarter) + if (gameState.ai.HQ.canBarter && Resources.GetBarterableCodes().length) this.performBarter(gameState); if (this.Config.difficulty <= 1) 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.GetBarterableCodes()) 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.GetBarterableCodes()) { 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.GetBarterableCodes(); 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.GetBarterableCodes()) { // 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.GetTradableCodes(); + 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.GetTradableCodes(); 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,19 +860,20 @@ 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.GetTributableCodes(); for (let resCode in amounts) - if (Resources.GetCodes().indexOf(resCode) == -1 || + if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { - warn("Invalid tribute amounts: " + uneval(amounts)); + warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts)); return; } Index: binaries/data/mods/public/simulation/components/tests/test_Barter.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Barter.js +++ binaries/data/mods/public/simulation/components/tests/test_Barter.js @@ -10,7 +10,7 @@ const truePrice = 110; Resources = { - "GetCodes": () => ["wood", "stone", "metal"], + "GetBarterableCodes": () => ["wood", "stone", "metal"], "GetResource": (resource) => ({ "truePrice": truePrice }) }; Index: binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Player.js +++ binaries/data/mods/public/simulation/components/tests/test_Player.js @@ -1,5 +1,7 @@ Resources = { "GetCodes": () => ["food", "metal", "stone", "wood"], + "GetTradableCodes": () => ["food", "metal", "stone", "wood"], + "GetBarterableCodes": () => ["food", "metal", "stone", "wood"], "GetResource": () => ({}), "BuildSchema": (type) => { let schema = ""; 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"))