UNPKG

@kitten-science/kitten-scientists

Version:

Add-on for the wonderful incremental browser game: https://kittensgame.com/web/

720 lines 34.6 kB
import { isNil, mustExist } from "@oliversalzburg/js-utils/data/nil.js"; import { Engine } from "./Engine.js"; import { TradeSettings } from "./settings/TradeSettings.js"; import { objectEntries } from "./tools/Entries.js"; import { negativeOneToInfinity, ucfirst } from "./tools/Format.js"; import { cl } from "./tools/Log.js"; export class TradeManager { _host; settings; _workshopManager; constructor(host, workshopManager, settings = new TradeSettings()) { this._host = host; this.settings = settings; this._workshopManager = workshopManager; } tick(context) { if (!this.settings.enabled) { return; } this.autoTrade(); if (this.settings.unlockRaces.enabled) { this.autoUnlock(context); } if (this.settings.buildEmbassies.enabled) { this.autoBuildEmbassies(context); } if (this.settings.feedLeviathans.enabled) { this.autoFeedElders(); } if (this.settings.tradeBlackcoin.enabled) { this.autoTradeBlackcoin(); } } autoTrade(cacheManager) { const catpower = this._workshopManager.getResource("manpower"); const gold = this._workshopManager.getResource("gold"); const sectionTrigger = this.settings.trigger; // The races we might want to trade with during this frame. const trades = []; const season = this._host.game.calendar.getCurSeason().name; // Determine how many races we will trade with this cycle. for (const trade of Object.values(this.settings.races)) { const race = this.getRace(trade.race); const trigger = Engine.evaluateSubSectionTrigger(sectionTrigger, trade.trigger); // Check if the race is enabled, in season, unlocked, and we can actually afford it. if (trigger < 0 || !trade.enabled || !trade.seasons[season].enabled || !race.unlocked || !this.singleTradePossible(sectionTrigger, catpower, gold, trade)) { continue; } // Determine which resource the race requires for trading, if any. const require = trade.require ? this._workshopManager.getResource(trade.require) : false; // Check if this trade would be profitable. const profitable = this.getProfitability(trade.race); // If the trade is set to be limited and profitable, make this trade. if (trade.limited && profitable) { trades.push(trade.race); } else if ( // If this trade is not limited, it must either not require anything, or // the required resource must be over the trigger value. // Additionally, gold must also be over the trigger value. !require || trigger <= require.value / require.maxValue) { trades.push(trade.race); } } // If no possible trades were found, bail out. if (trades.length === 0) { return; } // Figure out how much we can currently trade let maxTrades = this.getLowestTradeAmount(null, true, false); // If no trades are possible, bail out. if (maxTrades < 1) { return; } // Distribute max trades without starving any race. // This array should hold the amount of trades possible with each race. // The indices between this array and `trades` align. const maxByRace = []; // For all races we consider trading with, perform a more thorough check // if we actually can trade with them and how many times we could trade // with them. for (let tradeIndex = 0; tradeIndex < trades.length; tradeIndex++) { const race = trades[tradeIndex]; const tradeSettings = this.settings.races[race]; // Does this trade require a certain resource? const require = !tradeSettings.require ? false : this._workshopManager.getResource(tradeSettings.require); // Have the trigger conditions for this trade been met? const trigger = Engine.evaluateSubSectionTrigger(sectionTrigger, tradeSettings.trigger); const trigConditions = (!require || trigger <= require.value / require.maxValue) && trigger <= gold.value / gold.maxValue; // How many trades could we do? const tradePos = this.getLowestTradeAmount(race, tradeSettings.limited, trigConditions); // If no trades are possible, remove the race. if (tradePos < 1) { trades.splice(tradeIndex, 1); tradeIndex--; continue; } maxByRace.push(tradePos); } // If no trades are left over after this check, bail out. if (trades.length === 0) { return; } // Now let's do some trades. // This keeps track of how many trades we've done with each race. const tradesDone = {}; // While we have races left to trade with, and enough resources to trade (this // is indicated by `maxTrades`)... while (0 < trades.length && 1 <= maxTrades) { // If we can make fewer trades than we have resources available for... if (maxTrades < trades.length) { // ...find a random race to trade with const randomRaceIndex = Math.floor(Math.random() * trades.length); const tradeIndex = trades[randomRaceIndex]; // Init the counter if necessary. if (isNil(tradesDone[tradeIndex])) { tradesDone[tradeIndex] = 0; } tradesDone[tradeIndex] += 1; maxTrades -= 1; // As we are constrained on resources, don't trade with this race again. // We want to distribute the few resources we have between races. trades.splice(randomRaceIndex, 1); maxByRace.splice(randomRaceIndex, 1); continue; } // We now want to determine which race we can trade with the least amount // of times. // This likely to determine the "best" trades to perform. // The result is that we trade first with the races that we can make the // least amount of trades with, then continue to make trades with the // races we can make more trades with (although that amount could be reduced // by the time we get to them). let minTrades = Math.floor(maxTrades / trades.length); let minTradeIndex = 0; const tradeIndex = trades[minTradeIndex]; for (let tradeIndex = 0; tradeIndex < trades.length; ++tradeIndex) { if (maxByRace[tradeIndex] < minTrades) { minTrades = maxByRace[tradeIndex]; minTradeIndex = tradeIndex; } } // Store this trade in the trades we've "done". if (isNil(tradesDone[tradeIndex])) { tradesDone[tradeIndex] = 0; } tradesDone[tradeIndex] += minTrades; maxTrades -= minTrades; trades.splice(minTradeIndex, 1); maxByRace.splice(minTradeIndex, 1); } // If we found no trades to do, bail out. if (Object.values(tradesDone).length === 0) { return; } // Calculate how much we will spend and earn from trades. // This is straight forward. const tradeNet = {}; for (const [name, amount] of objectEntries(tradesDone)) { const race = this.getRace(name); const materials = this.getMaterials(name); for (const [mat, matAmount] of objectEntries(materials)) { if (!tradeNet[mat]) { tradeNet[mat] = 0; } tradeNet[mat] -= matAmount * amount; } const meanOutput = this.getAverageTrade(race); for (const [out, outValue] of objectEntries(meanOutput)) { const res = this._workshopManager.getResource(out); if (!tradeNet[out]) { tradeNet[out] = 0; } tradeNet[out] += res.maxValue > 0 ? Math.min(mustExist(meanOutput[out]) * mustExist(tradesDone[name]), Math.max(res.maxValue - res.value, 0)) : outValue * mustExist(tradesDone[name]); } } // Update the cache with the changes produced from the trades. if (!isNil(cacheManager)) { cacheManager.pushToCache({ materials: tradeNet, timeStamp: this._host.game.timer.ticksTotal, }); } // Now actually perform the calculated trades. for (const [name, count] of objectEntries(tradesDone)) { // TODO: This check should be redundant. If no trades were possible, // the entry shouldn't even exist. if (0 < count) { this.trade(name, count); } } } autoBuildEmbassies(context) { if (!this._host.game.diplomacy.races[0].embassyPrices) { return; } // Tries to calculate how many embassies for which races it can buy, // then it buys them. Code should be straight-forward. const culture = this._workshopManager.getResource("culture"); let cultureVal = 0; const trigger = this.settings.buildEmbassies.trigger; if (culture.value / culture.maxValue < negativeOneToInfinity(trigger)) { return; } cultureVal = this._workshopManager.getValueAvailable("culture"); const embassyBulk = {}; const bulkTracker = []; for (const race of this._host.game.diplomacy.races) { const name = race.name; if (name in this.settings.buildEmbassies.races === false) { continue; } const max = negativeOneToInfinity(this.settings.buildEmbassies.races[name].max); if (!this.settings.buildEmbassies.races[name].enabled || max <= race.embassyLevel || !race.unlocked) { continue; } embassyBulk[name] = { basePrice: mustExist(race.embassyPrices?.[0]).val, currentEm: race.embassyLevel, max, priceSum: 0, race: race, val: 0, }; bulkTracker.push(name); } if (bulkTracker.length === 0) { return; } while (bulkTracker.length > 0) { for (let raceIndex = 0; raceIndex < bulkTracker.length; raceIndex++) { const name = bulkTracker[raceIndex]; const emBulk = mustExist(embassyBulk[name]); if (emBulk.max <= emBulk.currentEm + emBulk.val) { bulkTracker.splice(raceIndex, 1); --raceIndex; continue; } const nextPrice = emBulk.basePrice * 1.15 ** (emBulk.currentEm + emBulk.val); if (nextPrice <= cultureVal) { cultureVal -= nextPrice; emBulk.priceSum += nextPrice; emBulk.val += 1; context.requestGameUiRefresh = true; } else { bulkTracker.splice(raceIndex, 1); --raceIndex; } } } for (const [, emBulk] of objectEntries(embassyBulk)) { if (emBulk.val === 0) { continue; } cultureVal = this._workshopManager.getValueAvailable("culture"); if (cultureVal < emBulk.priceSum) { console.warn(...cl("Something has gone horribly wrong.", emBulk.priceSum, cultureVal)); } // We don't want to invoke the embassy build action multiple times, as // that would cause lots of log messages. // Instead, we replicate the behavior of the game here and purchase in bulk. this._workshopManager.getResource("culture").value -= emBulk.priceSum; emBulk.race.embassyLevel += emBulk.val; this._host.engine.storeForSummary("embassy", emBulk.val); if (emBulk.val !== 1) { this._host.engine.iactivity("build.embassies", [emBulk.val, emBulk.race.title], "ks-build"); } else { this._host.engine.iactivity("build.embassy", [emBulk.val, emBulk.race.title], "ks-build"); } } } autoFeedElders() { const leviathanInfo = this._host.game.diplomacy.get("leviathans"); const necrocorns = this._host.game.resPool.get("necrocorn"); if (!leviathanInfo.unlocked || necrocorns.value === 0) { return; } if (1 <= necrocorns.value) { // If feeding the elders would increase their energy level towards the // cap, do it. if (leviathanInfo.energy < this._host.game.diplomacy.getMarkerCap()) { this._host.game.diplomacy.feedElders(); this._host.engine.iactivity("act.feed"); this._host.engine.storeForSummary("feed", 1); } } else { // We can reach this branch if we have partial necrocorns from resets. // The partial necrocorns will then be feed to the elders to bring us back // to even zero. if (0.25 * (1 + this._host.game.getEffect("corruptionBoostRatio")) < 1) { this._host.engine.storeForSummary("feed", necrocorns.value); this._host.game.diplomacy.feedElders(); this._host.engine.iactivity("dispose.necrocorn"); } } } autoUnlock(context) { if (!this._host.game.tabs[4].visible) { return; } // Check how many races we could reasonably unlock at this point. const maxRaces = this._host.game.diplomacy.get("leviathans").unlocked ? 8 : 7; // If we haven't unlocked that many races yet... if (this._host.game.diplomacyTab.racePanels.length < maxRaces) { // Get the currently available catpower. let manpower = this._workshopManager.getValueAvailable("manpower"); // TODO: These should be checked in reverse order. Otherwise the check for lizards // can cause the zebras to be discovered at later stages in the game. Then it // gets to the check for the zebras and doesn't explore again, as they're // already unlocked. Then it takes another iteration to unlock the other race. // Send explorers if we haven't discovered the lizards yet. if (!this._host.game.diplomacy.get("lizards").unlocked) { if (manpower >= 1000) { this._host.game.resPool.get("manpower").value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // Do exactly the same for the sharks. if (!this._host.game.diplomacy.get("sharks").unlocked) { if (manpower >= 1000) { this._host.game.resPool.get("manpower").value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // Do exactly the same for the griffins. if (!this._host.game.diplomacy.get("griffins").unlocked) { if (manpower >= 1000) { this._host.game.resPool.get("manpower").value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // For nagas, we additionally need enough culture. if (!this._host.game.diplomacy.get("nagas").unlocked && this._host.game.resPool.get("culture").value >= 1500) { if (manpower >= 1000) { this._host.game.resPool.get("manpower").value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // Zebras require us to have a ship. if (!this._host.game.diplomacy.get("zebras").unlocked && this._host.game.resPool.get("ship").value >= 1) { if (manpower >= 1000) { this._host.game.resPool.get("manpower").value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // For spiders, we need 100 ships and 125000 science. if (!this._host.game.diplomacy.get("spiders").unlocked && mustExist(this._host.game.resPool.get("ship")).value >= 100 && mustExist(this._host.game.resPool.get("science")).maxValue > 125000) { if (manpower >= 1000) { mustExist(this._host.game.resPool.get("manpower")).value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } // Dragons require nuclear fission to be researched. if (!this._host.game.diplomacy.get("dragons").unlocked && this._host.game.science.get("nuclearFission").researched) { if (manpower >= 1000) { mustExist(this._host.game.resPool.get("manpower")).value -= 1000; const unlockedRace = mustExist(this._host.game.diplomacy.unlockRandomRace()); this._host.engine.iactivity("upgrade.race", [unlockedRace.title], "ks-upgrade"); manpower -= 1000; context.requestGameUiRefresh = true; } } } } autoTradeBlackcoin() { const coinPrice = this._host.game.calendar.cryptoPrice; const relicsInitial = this._host.game.resPool.get("relic").value; const coinsInitial = this._host.game.resPool.get("blackcoin").value; let coinsExchanged = 0.0; let relicsExchanged = 0.0; // All of this code is straight-forward. Buy low, sell high. // Exchanges up to a certain threshold, in order to keep a good exchange rate, then waits // for a higher threshold before exchanging for relics. if (coinPrice < this.settings.tradeBlackcoin.buy && this.settings.tradeBlackcoin.trigger < relicsInitial) { this._host.game.diplomacy.buyBcoin(); const currentCoin = this._host.game.resPool.get("blackcoin").value; coinsExchanged = Math.round(currentCoin - coinsInitial); this._host.engine.iactivity("act.blackcoin.buy", [this._host.renderAbsolute(coinsExchanged)]); } else if (coinPrice > this.settings.tradeBlackcoin.sell && 0 < this._host.game.resPool.get("blackcoin").value) { this._host.game.diplomacy.sellBcoin(); const relicsCurrent = mustExist(this._host.game.resPool.get("relic")).value; relicsExchanged = Math.round(relicsCurrent - relicsInitial); this._host.engine.iactivity("act.blackcoin.sell", [ this._host.renderAbsolute(relicsExchanged), ]); } } /** * Trade with the given race. * * @param name The race to trade with. * @param amount How often to trade with the race. */ trade(name, amount) { const race = this.getRace(name); this._host.game.diplomacy.tradeMultiple(race, amount); this._host.engine.storeForSummary(race.title, amount, "trade"); this._host.engine.iactivity("act.trade", [this._host.renderAbsolute(amount), ucfirst(race.title)], "ks-trade"); } /** * Determine if a trade with the given race would be considered profitable. * * @param name The race to trade with. * @returns `true` if the trade is profitable; `false` otherwise. */ getProfitability(name) { const race = this.getRace(name); // This will keep track of how much we have to spend on a given trade. // Higher values are worse. let cost = 0; // Get materials required to trade with the race. const materials = this.getMaterials(name); // For each material required to trade with the race... for (const [mat, amount] of objectEntries(materials)) { // ...determine how much of the resource is produced each tick. const tick = this._workshopManager.getTickVal(this._workshopManager.getResource(mat)); // If we're not producing any of this resource, it's spice, or blueprints, // don't consider it in the calculation. if (tick === "ignore") { continue; } // If we're consuming this resource instead of producing it, // instantly consider this trade not profitable. if (tick <= 0) { return false; } // Add to the cost. // We consider all resources to be equal in the profitability calculation. // We just add the cost of the resource, divided by how much of it we're // producing. So resources that we produce a lot of, don't "cost" as much. cost += amount / tick; } // This will keep track of how much we receive from a given trade. let profit = 0; // Get resources returned from trade. const output = this.getAverageTrade(race); // For each material resulting from a trade with the race... for (const [prod, amount] of objectEntries(output)) { const resource = this._workshopManager.getResource(prod); // ...determine how much of the resource is produced each tick. const tick = this._workshopManager.getTickVal(resource); // If we're not producing any of this resource, it's spice, or blueprints, // don't consider it in the calculation. if (tick === "ignore") { continue; } // If we're consuming this resource instead of producing it, // instantly consider this trade profitable. if (tick <= 0) { return true; } profit += // For capped resources... 0 < resource.maxValue ? // ... only add as much to the profit as we can store. Math.min(amount, Math.max(resource.maxValue - resource.value, 0)) / tick : // For uncapped resources, add all of it. amount / tick; } // If the profit is higher than the cost, consider this profitable. return cost <= profit; } /** * Determine which resources and how much of them a trade with the given race results in. * * @param race The race to check. * @returns The resources returned from an average trade and their amount. */ getAverageTrade(race) { // TODO: This is a lot of magic. It's possible some of it was copied directly from the game. const standingRatio = this._host.game.getEffect("standingRatio") + this._host.game.diplomacy.calculateStandingFromPolicies(race.name, this._host.game); const raceRatio = 1 + race.energy * 0.02; const tradeRatio = 1 + this._host.game.diplomacy.getTradeRatio() + this._host.game.diplomacy.calculateTradeBonusFromPolicies(race.name, this._host.game); const failedRatio = race.standing < 0 ? race.standing + standingRatio : 0; const successRatio = 0 < failedRatio ? 1 + failedRatio : 1; const bonusRatio = 0 < race.standing ? Math.min(race.standing + standingRatio / 2, 1) : 0; const output = {}; for (const item of race.sells) { if (!this._isValidTrade(item, race)) { // Still put invalid trades into the result to not cause missing keys. output[item.name] = 0; continue; } let mean = 0; const tradeChance = race.embassyPrices ? item.chance * (1 + this._host.game.getLimitedDR(0.01 * race.embassyLevel, 0.75)) : item.chance; if (race.name === "zebras" && item.name === "titanium") { const shipCount = this._host.game.resPool.get("ship").value; const titaniumProbability = Math.min(0.15 + shipCount * 0.0035, 1); const titaniumRatio = 1 + shipCount / 50; mean = 1.5 * titaniumRatio * (successRatio * titaniumProbability); } else { const seasionRatio = !item.seasons ? 1 : 1 + item.seasons[this._host.game.calendar.getCurSeason().name]; const normBought = (successRatio - bonusRatio) * Math.min(tradeChance / 100, 1); const normBonus = bonusRatio * Math.min(tradeChance / 100, 1); mean = (normBought + 1.25 * normBonus) * item.value * raceRatio * seasionRatio * tradeRatio; } output[item.name] = mean; } const spiceChance = race.embassyPrices ? 0.35 * (1 + 0.01 * race.embassyLevel) : 0.35; const spiceTradeAmount = successRatio * Math.min(spiceChance, 1); output.spice = 25 * spiceTradeAmount + (50 * spiceTradeAmount * tradeRatio) / 2; output.blueprint = 0.1 * successRatio; return output; } /** * Determine if this trade is valid. * * @param item The tradeable item. * @param race The race to trade with. * @returns `true` if the trade is valid; `false` otherwise. */ _isValidTrade(item, race) { return ( // Do we have enough embassies to receive the item? !(item.minLevel && race.embassyLevel < item.minLevel) && // TODO: These seem a bit too magical. (this._host.game.resPool.get(item.name).unlocked || item.name === "titanium" || item.name === "uranium" || race.name === "leviathans")); } /** * Determine how many trades are at least possible. * * @param name The race to trade with. * @param _limited Is the race set to be limited. * @param _trigConditions Ignored * @returns The lowest number of trades possible with this race. */ getLowestTradeAmount(name, _limited, _trigConditions) { let amount; const materials = this.getMaterials(name); let total; for (const [resource, required] of objectEntries(materials)) { if (resource === "manpower") { let manpowerCost = required; if (this._host.game.challenges.isActive("postApocalypse")) { manpowerCost = required * (1 + this._host.game.bld.getPollutionLevel()); } total = this._workshopManager.getValueAvailable(resource) / manpowerCost; } else if (resource === "gold") { let goldCost = required; if (this._host.game.challenges.isActive("postApocalypse")) { goldCost = required * (1 + this._host.game.bld.getPollutionLevel()); } total = this._workshopManager.getValueAvailable(resource) / goldCost; } else { total = this._workshopManager.getValueAvailable(resource) / required; } // Set the amount to the lowest amount of possible trades seen yet. amount = amount === undefined || total < amount ? total : amount; } // Round the amount down and normalize to 0. amount = Math.floor(amount ?? 0); // If the lowest amount is 0, return 0. if (amount === 0) { return 0; } // If no race was specified, or this is trading with leviathans, return the // currently known, lowest amount. // The special case for leviathans indicates that the following code is buggy or // problematic, as leviathans are the last race. if (name === null || name === "leviathans") { return amount; } const race = this.getRace(name); // Loop through the items obtained by the race, and determine // which resource has the most space left. Once we've determined this, // reduce the amount by this capacity. This ensures that we continue to trade // as long as at least one resource has capacity, and we never over-trade. let highestCapacity = 0; const tradeOutput = this.getAverageTrade(race); for (const item of race.sells) { const resource = this._workshopManager.getResource(item.name); let max = 0; // No need to process resources that don't cap if (!resource.maxValue) { continue; } max = mustExist(tradeOutput[item.name]); const capacity = Math.max((resource.maxValue - resource.value) / max, 0); highestCapacity = capacity < highestCapacity ? highestCapacity : capacity; } // We must take the ceiling of capacity so that we will trade as long // as there is any room, even if it doesn't have exact space. Otherwise // we seem to starve trading altogether. highestCapacity = Math.ceil(highestCapacity); // If any of the resources resulting from a trade are already capped, we // don't want to trade with this race. if (highestCapacity === 0) { return 0; } // Now that we know the most we *should* trade for, check to ensure that // we trade for our max cost, or our max capacity, whichever is lower. // This helps us prevent trading for resources we can't store. Note that we // essentially ignore blueprints here. amount = highestCapacity < amount ? Math.max(highestCapacity - 1, 1) : amount; return Math.floor(amount); } /** * Determine the resources required to trade with the given race. * * @param race The race to check. If not specified the resources for any * trade will be returned. * @returns The resources need to trade with the race. */ getMaterials(race = null) { const materials = { gold: 15 - this._host.game.getEffect("tradeGoldDiscount"), manpower: 50 - this._host.game.getEffect("tradeCatpowerDiscount"), }; if (isNil(race)) { return materials; } const prices = this.getRace(race).buys; for (const price of prices) { materials[price.name] = price.val; } return materials; } /** * Retrieve information about the given race from the game. * * @param name The race to get the information object for. * @returns The information object for the given race. */ getRace(name) { const raceInfo = this._host.game.diplomacy.get(name); if (isNil(raceInfo)) { throw new Error(`Unable to retrieve race '${name}'`); } return raceInfo; } /** * Determine if at least a single trade can be made. * * @param trade - The trade option to check. If not specified, all races are checked. * @returns If the requested trade is possible. */ singleTradePossible(sectionTrigger, catpower, gold, trade) { const trigger = trade ? Engine.evaluateSubSectionTrigger(sectionTrigger, trade.trigger) : sectionTrigger; if (trigger < 0 && trade === undefined) { // We will have to check all potential trades individually. return true; } if (trigger < 0 && trade !== undefined) { // This will never trigger. return false; } // We should only trade if catpower and gold hit the trigger value. // Trades can additionally require specific resources. We will check for those later. if (catpower.value / catpower.maxValue < trigger || gold.value / gold.maxValue < trigger) { return false; } // Get the materials required to trade with the race. const materials = this.getMaterials(trade?.race); for (const [resource, amount] of objectEntries(materials)) { // Check if we have a sufficient amount of that resource in storage. if (this._workshopManager.getValueAvailable(resource) < amount) { return false; } } return true; } } //# sourceMappingURL=TradeManager.js.map