diff --git a/data/resources.js b/data/resources.js index ddbff606..472295f9 100644 --- a/data/resources.js +++ b/data/resources.js @@ -528,8 +528,8 @@ window.FMG.data.resourceModels = { Any_forest: 'biome(5, 6, 7, 8, 9)', Temperate_and_boreal_forests: 'biome(6, 8, 9)', Hills: 'minHeight(40) || (minHeight(30) && nth(10))', - Mountains: 'minHeight(60) || (minHeight(40) && nth(10))', - Mountains_and_wetlands: 'minHeight(60) || (biome(12) && nth(8))', + Mountains: 'minHeight(60) || (minHeight(20) && nth(10))', + Mountains_and_wetlands: 'minHeight(60) || (biome(12) && nth(7)) || (minHeight(20) && nth(10))', Headwaters: 'river() && minHeight(40)', More_habitable: 'minHabitability(20) && habitability()', Marine_and_rivers: 'shore(-1) && (type("ocean", "freshwater", "salt") || (river() && shore(1, 2)))', diff --git a/index.html b/index.html index 279bc44a..97bd4748 100644 --- a/index.html +++ b/index.html @@ -2019,7 +2019,7 @@ - + diff --git a/main.js b/main.js index 1bd85405..c784b792 100644 --- a/main.js +++ b/main.js @@ -1445,7 +1445,6 @@ function rankCells() { const flMax = d3.max(cells.fl) + d3.max(cells.conf); // to normalize flux const areaMean = d3.mean(cells.area); // to adjust population by cell area const getResValue = (i) => (cells.resource[i] ? Resources.get(cells.resource[i])?.value : 0); // get bonus resource scope - const resBonuses = []; const POP_BALANCER = 1.5; // to ballance population to desired number for (const i of cells.i) { @@ -1476,20 +1475,12 @@ function rankCells() { const cellRes = getResValue(i); const neibRes = d3.mean(cells.c[i].map((c) => getResValue(c))); const resBonus = (cellRes ? cellRes + 10 : 0) + neibRes; - resBonuses.push(resBonus); // cell rural population is suitability adjusted by cell area cells.pop[i] = s > 0 ? (s * POP_BALANCER * cells.area[i]) / areaMean : 0; cells.s[i] = s + resBonus; - - // debug.append('text').attr('x', cells.p[i][0]).attr('y', cells.p[i][1]).text(cells.s[i]); } - console.log(resBonuses); - console.log(d3.max(resBonuses)); - console.log(d3.mean(resBonuses)); - console.log(d3.median(resBonuses.map((v) => rn(v)))); - TIME && console.timeEnd('rankCells'); } diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 8efbdb71..371581dd 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -229,12 +229,12 @@ window.BurgsAndStates = (function () { const {cells, burgs} = pack; for (const burg of burgs) { - if (!burg.i || burg.removed) continue; - if (newburg && newburg.i !== burg.i) continue; - const {cell, state, pop, capital} = burg; + const {i, removed, cell, state, pop, capital, tradeCenter} = burg; + if (!i || removed) continue; + if (newburg && newburg.i !== i) continue; burg.citadel = capital || (pop > 50 && P(0.75)) || P(0.5) ? 1 : 0; - burg.plaza = pop > 50 || (pop > 30 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.25) ? 1 : 0; + burg.plaza = tradeCenter === i; burg.walls = capital || pop > 30 || (pop > 20 && P(0.75)) || (pop > 10 && P(0.5)) || P(0.2) ? 1 : 0; burg.shanty = pop > 30 || (pop > 20 && P(0.75)) || (burg.walls && P(0.75)) ? 1 : 0; const religion = cells.religion[cell]; diff --git a/modules/production-generator.js b/modules/production-generator.js index 61c846b8..56da2fa1 100644 --- a/modules/production-generator.js +++ b/modules/production-generator.js @@ -102,11 +102,11 @@ window.Production = (function () { queue.queue({...occupation, basePriority: newBasePriority, priority: newPriority}); } + burg.produced = {}; for (const resourceId in productionPull) { - const production = Math.ceil(productionPull[resourceId]); - productionPull[resourceId] = production; + const production = productionPull[resourceId]; + burg.produced[resourceId] = Math.ceil(production); } - burg.production = productionPull; } }; diff --git a/modules/trade-generator.js b/modules/trade-generator.js index 4cdf1780..89944532 100644 --- a/modules/trade-generator.js +++ b/modules/trade-generator.js @@ -2,23 +2,95 @@ window.Trade = (function () { const defineCenters = () => { - const {cells} = pack; + TIME && console.time('defineCenters'); + pack.trade = {centers: [], deals: []}; + const {burgs, trade} = pack; + + // min distance between trade centers + let minSpacing = (((graphWidth + graphHeight) * 2) / burgs.length ** 0.7) | 0; + + const tradeScore = burgs.map(({i, removed, capital, port, population, produced}) => { + if (!i || removed) return {i: 0, score: 0}; + const totalProduction = d3.sum(Object.values(produced)); + let score = Math.round(totalProduction - population); + if (capital) score *= 2; + if (port) score *= 3; + return {i, score}; + }); + + const candidatesSorted = tradeScore.sort((a, b) => b.score - a.score); + const centersTree = d3.quadtree(); + + for (const {i} of candidatesSorted) { + if (!i) continue; + const {x, y} = burgs[i]; + + const tradeCenter = centersTree.find(x, y, minSpacing); + if (tradeCenter) { + const centerBurg = tradeCenter[2]; + burgs[i].tradeCenter = centerBurg; + + const {x: x2, y: y2} = burgs[centerBurg]; + debug.append('line').attr('x1', x).attr('y1', y).attr('x2', x2).attr('y2', y2).attr('stroke', 'black').attr('stroke-width', 0.2); + } else { + trade.centers.push({i: trade.centers.length, burg: i, x, y}); + centersTree.add([x, y, i]); + burgs[i].tradeCenter = i; + } + + minSpacing += 1; + } + + for (const {i, score} of candidatesSorted) { + if (!i) continue; + const {x, y} = burgs[i]; + debug.append('text').attr('x', x).attr('y', y).style('font-size', 4).text(score); + } + + TIME && console.timeEnd('defineCenters'); }; const exportGoods = () => { - for (const burg of pack.burgs) { - if (!burg.i || burg.removed) continue; - const {population, production: resourcePool} = burg; - const localUsage = Math.ceil(population); + const {burgs, states, trade} = pack; + const DEFAULT_TRANSPORT_DIST = (graphWidth + graphHeight) / 20; - const surplus = {}; - for (const resourceId in resourcePool) { - const production = resourcePool[resourceId]; - const extraProduction = production - localUsage; - if (extraProduction > 0) surplus[resourceId] = extraProduction; + for (const tradeCenter of trade.centers) { + const {i: centerId, burg: centerBurg, x: x0, y: y0} = tradeCenter; + const goods = {}; + + for (const burg of burgs) { + const {i, removed, tradeCenter, produced, population, state, x, y} = burg; + if (!i || removed || tradeCenter !== centerBurg) continue; + const consumption = Math.ceil(population); + + const distance = Math.hypot(x - x0, y - y0); + const transportFee = (distance / DEFAULT_TRANSPORT_DIST) ** 0.8 || 0.02; + const salesTax = states[state].salesTax || 0.1; + + for (const resourceId in produced) { + const production = produced[resourceId]; + const quantity = production - consumption; + if (quantity < 1) continue; + + const {value} = Resources.get(+resourceId); + + const basePrice = value * quantity; + const transportCost = rn((value * quantity) ** 0.5 * transportFee, 2); + const netPrice = basePrice - transportCost; + + const stateIncome = rn(netPrice * salesTax, 2); + const burgIncome = rn(netPrice - stateIncome, 2); + + if (burgIncome < 1 || burgIncome < basePrice / 4) continue; + + trade.deals.push({resourceId: +resourceId, quantity, exporter: i, tradeCenter: centerId, basePrice, transportCost, stateIncome, burgIncome}); + + if (!goods[resourceId]) goods[resourceId] = quantity; + else goods[resourceId] += quantity; + } } - burg.export = surplus; + tradeCenter.goods = goods; } }; diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js index 72d8d31e..809f00f6 100644 --- a/modules/ui/burg-editor.js +++ b/modules/ui/burg-editor.js @@ -98,8 +98,10 @@ function editBurg(id) { else document.getElementById('burgShanty').classList.add('inactive'); // economics block - document.getElementById('burgProduction').innerHTML = getProduction(b.production); - document.getElementById('burgExport').innerHTML = getProduction(b.export); + document.getElementById('burgProduction').innerHTML = getProduction(b.produced); + const deals = pack.trade.deals; + document.getElementById('burgExport').innerHTML = getExport(deals.filter((deal) => deal.exporter === b.i)); + document.getElementById('burgImport').innerHTML = ''; //toggle lock updateBurgLockIcon(); @@ -119,12 +121,12 @@ function editBurg(id) { document.getElementById('burgEmblem').setAttribute('href', '#' + coaID); } - function getProduction(resources) { + function getProduction(pool) { let html = ''; - for (const resourceId in resources) { + for (const resourceId in pool) { const {name, unit, icon} = Resources.get(+resourceId); - const production = resources[resourceId]; + const production = pool[resourceId]; const unitName = production > 1 ? unit + 's' : unit; html += ` @@ -136,6 +138,24 @@ function editBurg(id) { return html; } + function getExport(dealsArray) { + if (!dealsArray.length) return 'no'; + + const totalIncome = d3.sum(dealsArray.map((deal) => deal.burgIncome)); + const exported = dealsArray.map((deal) => { + const {resourceId, quantity, burgIncome} = deal; + const {name, unit, icon} = Resources.get(resourceId); + const unitName = quantity > 1 ? unit + 's' : unit; + + return ` + + ${quantity} + `; + }); + + return `${totalIncome}: ${exported.join('')}`; + } + // [-1; 31] °C, source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature function getTemperatureLikeness(temperature) { if (temperature < -15) return 'nowhere in the real-world';