|
@@ -51,10 +51,11 @@ async function render(planetName: string, pop: Pop) {
|
|
|
|
|
|
|
|
async function _render(planetName: string, pop: Pop) {
|
|
async function _render(planetName: string, pop: Pop) {
|
|
|
const encodedPlanetName = encodeURIComponent(planetName);
|
|
const encodedPlanetName = encodeURIComponent(planetName);
|
|
|
- const [planet, siteCounts, infras]: [Planet, SiteCount[], Infrastructure[]] = await Promise.all([
|
|
|
|
|
|
|
+ const [planet, siteCounts, infras, allPrices]: [Planet, SiteCount[], Infrastructure[], Price[]] = await Promise.all([
|
|
|
cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
|
|
cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
|
|
|
cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
|
|
cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
|
|
|
- cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`)
|
|
|
|
|
|
|
+ cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`),
|
|
|
|
|
+ cachedFetchJSON('https://api.prunplanner.org/data/exchanges'),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
let lastPOPR = null;
|
|
let lastPOPR = null;
|
|
@@ -86,7 +87,10 @@ async function _render(planetName: string, pop: Pop) {
|
|
|
currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
|
|
currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // gen
|
|
|
|
|
|
|
+ const prices: Map<string, number> = new Map();
|
|
|
|
|
+ for (const price of allPrices)
|
|
|
|
|
+ if (price.exchange_code === 'IC1')
|
|
|
|
|
+ prices.set(price.ticker, price.vwap_30d);
|
|
|
|
|
|
|
|
renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
|
|
renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
|
|
|
<br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
|
|
<br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
|
|
@@ -164,11 +168,12 @@ async function _render(planetName: string, pop: Pop) {
|
|
|
|
|
|
|
|
<h2>options</h2>
|
|
<h2>options</h2>
|
|
|
current projected ${pop} happiness: ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled))}
|
|
current projected ${pop} happiness: ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled))}
|
|
|
- <table>
|
|
|
|
|
- ${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled).map((result) => {
|
|
|
|
|
|
|
+ <table class="options">
|
|
|
|
|
+ ${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, prices).map((result) => {
|
|
|
return `<tr>
|
|
return `<tr>
|
|
|
<td>${[...result.config.entries()].map(([building, fill]) => `${building}: ${fill.numMats}`).join(', ')}</td>
|
|
<td>${[...result.config.entries()].map(([building, fill]) => `${building}: ${fill.numMats}`).join(', ')}</td>
|
|
|
<td>${formatPct(result.happiness)}</td>
|
|
<td>${formatPct(result.happiness)}</td>
|
|
|
|
|
+ <td>${formatNum(result.cost)}/day</td>
|
|
|
</tr>`;
|
|
</tr>`;
|
|
|
}).join('')}
|
|
}).join('')}
|
|
|
</table>
|
|
</table>
|
|
@@ -224,12 +229,12 @@ function calcPOPIFilled(infra: Infrastructure): number | null {
|
|
|
return filled;
|
|
return filled;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill):
|
|
|
|
|
- {config: POPIFill, happiness: number, cost: number}[] {
|
|
|
|
|
|
|
+function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
|
|
|
|
|
+ prices: Map<string, number>): {config: POPIFill, happiness: number, cost: number}[] {
|
|
|
const results: {config: POPIFill, happiness: number, cost: number}[] = [];
|
|
const results: {config: POPIFill, happiness: number, cost: number}[] = [];
|
|
|
for (const config of popiFillCombinations(currentPOPIFilled)) {
|
|
for (const config of popiFillCombinations(currentPOPIFilled)) {
|
|
|
const happiness = projectedHappiness(pop, totalNeeds, siteCount, config);
|
|
const happiness = projectedHappiness(pop, totalNeeds, siteCount, config);
|
|
|
- let cost = config.values().reduce((sum, fill) => sum + fill.numMats * fill.tier, 0); // TODO
|
|
|
|
|
|
|
+ let cost = calcCost(config, prices);
|
|
|
// is any result better than this one?
|
|
// is any result better than this one?
|
|
|
if (results.some((result) => (result.happiness >= happiness && result.cost < cost) ||
|
|
if (results.some((result) => (result.happiness >= happiness && result.cost < cost) ||
|
|
|
(result.happiness > happiness && result.cost <= cost)))
|
|
(result.happiness > happiness && result.cost <= cost)))
|
|
@@ -253,7 +258,8 @@ function* popiFillCombinations(currentPOPIFilled: POPIFill): Generator<POPIFill>
|
|
|
}
|
|
}
|
|
|
const building = entries[index];
|
|
const building = entries[index];
|
|
|
const tier = currentPOPIFilled.get(building)!.tier;
|
|
const tier = currentPOPIFilled.get(building)!.tier;
|
|
|
- for (let i = 0; i <= POPI[building].max; i++) {
|
|
|
|
|
|
|
+ const maxBuildingMats = Object.keys(POPI[building].mats).length;
|
|
|
|
|
+ for (let i = 0; i <= maxBuildingMats; i++) {
|
|
|
current.set(building, {tier, numMats: i});
|
|
current.set(building, {tier, numMats: i});
|
|
|
yield* helper(index + 1, current);
|
|
yield* helper(index + 1, current);
|
|
|
}
|
|
}
|
|
@@ -265,9 +271,10 @@ function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCoun
|
|
|
let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
|
|
let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
|
|
|
'comfort': 0, 'culture': 0, 'education': 0};
|
|
'comfort': 0, 'culture': 0, 'education': 0};
|
|
|
for (const [building, fill] of popiFilled.entries()) {
|
|
for (const [building, fill] of popiFilled.entries()) {
|
|
|
- const {max, needs} = POPI[building];
|
|
|
|
|
|
|
+ const {needs} = POPI[building];
|
|
|
|
|
+ const maxBuildingMats = Object.keys(POPI[building].mats).length;
|
|
|
for (const {need, supplied} of needs) {
|
|
for (const {need, supplied} of needs) {
|
|
|
- const provided = fill.numMats / max * fill.tier * supplied;
|
|
|
|
|
|
|
+ const provided = fill.numMats / maxBuildingMats * fill.tier * supplied;
|
|
|
totalProvided[need] += provided;
|
|
totalProvided[need] += provided;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -293,6 +300,25 @@ function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCoun
|
|
|
return happiness;
|
|
return happiness;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function calcCost(config: POPIFill, prices: Map<string, number>): number {
|
|
|
|
|
+ let cost = 0;
|
|
|
|
|
+ for (const [building, fill] of config.entries()) {
|
|
|
|
|
+ const {mats} = POPI[building];
|
|
|
|
|
+ const matPrices: {ticker: string, costPerDay: number}[] = [];
|
|
|
|
|
+ for (const [mat, amount] of Object.entries(mats)) {
|
|
|
|
|
+ const matCost = prices.get(mat)!;
|
|
|
|
|
+ if (matCost === undefined)
|
|
|
|
|
+ throw new Error('no price for ' + mat);
|
|
|
|
|
+ matPrices.push({ticker: mat, costPerDay: matCost * amount});
|
|
|
|
|
+ }
|
|
|
|
|
+ matPrices.sort((a, b) => a.costPerDay - b.costPerDay);
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < fill.numMats; i++)
|
|
|
|
|
+ cost += matPrices[i].costPerDay * fill.tier;
|
|
|
|
|
+ }
|
|
|
|
|
+ return cost;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const NEEDS: Record<Pop, Record<Need, number>> = {
|
|
const NEEDS: Record<Pop, Record<Need, number>> = {
|
|
|
'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
|
|
'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
|
|
|
'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
|
|
'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
|
|
@@ -300,21 +326,63 @@ const NEEDS: Record<Pop, Record<Need, number>> = {
|
|
|
'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
|
|
'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
|
|
|
'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
|
|
'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
|
|
|
}
|
|
}
|
|
|
-const POPI: Record<POPIBuilding, {max: number, needs: {need: Need, supplied: number}[]}> = {
|
|
|
|
|
- 'SAFETY_STATION': {max: 3, needs: [{need: 'safety', supplied: 2500}]},
|
|
|
|
|
- 'SECURITY_DRONE_POST': {max: 4, needs: [{need: 'safety', supplied: 5000}]},
|
|
|
|
|
- 'EMERGENCY_CENTER': {max: 5, needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}]},
|
|
|
|
|
- 'INFIRMARY': {max: 3, needs: [{need: 'health', supplied: 2500}]},
|
|
|
|
|
- 'HOSPITAL': {max: 6, needs: [{need: 'health', supplied: 5000}]},
|
|
|
|
|
- 'WELLNESS_CENTER': {max: 6, needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}]},
|
|
|
|
|
- 'WILDLIFE_PARK': {max: 5, needs: [{need: 'comfort', supplied: 2500}]},
|
|
|
|
|
- 'ARCADES': {max: 6, needs: [{need: 'comfort', supplied: 5000}]},
|
|
|
|
|
- 'ART_CAFE': {max: 6, needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}]},
|
|
|
|
|
- 'ART_GALLERY': {max: 4, needs: [{need: 'culture', supplied: 2500}]},
|
|
|
|
|
- 'THEATER': {max: 6, needs: [{need: 'culture', supplied: 5000}]},
|
|
|
|
|
- 'PLANETARY_BROADCASTING_HUB': {max: 6, needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}]},
|
|
|
|
|
- 'LIBRARY': {max: 5, needs: [{need: 'education', supplied: 2500}]},
|
|
|
|
|
- 'UNIVERSITY': {max: 6, needs: [{need: 'education', supplied: 5000}]},
|
|
|
|
|
|
|
+const POPI: Record<POPIBuilding, {needs: {need: Need, supplied: number}[], mats: Record<string, number>}> = {
|
|
|
|
|
+ 'SAFETY_STATION': {
|
|
|
|
|
+ needs: [{need: 'safety', supplied: 2500}],
|
|
|
|
|
+ mats: {'DW': 10, 'OFF': 10, 'SUN': 2},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'SECURITY_DRONE_POST': {
|
|
|
|
|
+ needs: [{need: 'safety', supplied: 5000}],
|
|
|
|
|
+ mats: {'POW': 1, 'RAD': 0.47, 'CCD': 0.07, 'SUD': 0.07},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'EMERGENCY_CENTER': {
|
|
|
|
|
+ needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}],
|
|
|
|
|
+ mats: {'PK': 2, 'POW': 0.4, 'BND': 4, 'RED': 0.07, 'BSC': 0.07},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'INFIRMARY': {
|
|
|
|
|
+ needs: [{need: 'health', supplied: 2500}],
|
|
|
|
|
+ mats: {'OFF': 10, 'TUB': 6.67, 'STR': 0.67},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'HOSPITAL': {
|
|
|
|
|
+ needs: [{need: 'health', supplied: 5000}],
|
|
|
|
|
+ mats: {'PK': 2, 'SEQ': 0.4, 'BND': 4, 'SDR': 0.07, 'RED': 0.07, 'BSC': 0.13},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'WELLNESS_CENTER': {
|
|
|
|
|
+ needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}],
|
|
|
|
|
+ mats: {'KOM': 4, 'OLF': 2, 'DW': 6, 'DEC': 0.67, 'PFE': 2.67, 'SOI': 6.67},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'WILDLIFE_PARK': {
|
|
|
|
|
+ needs: [{need: 'comfort', supplied: 2500}],
|
|
|
|
|
+ mats: {'DW': 10, 'FOD': 6, 'PFE': 2, 'SOI': 3.33, 'DEC': 0.33},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'ARCADES': {
|
|
|
|
|
+ needs: [{need: 'comfort', supplied: 5000}],
|
|
|
|
|
+ mats: {'POW': 2, 'MHP': 2, 'OLF': 4, 'BID': 0.2, 'HOG': 0.2, 'EDC': 0.2},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'ART_CAFE': {
|
|
|
|
|
+ needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}],
|
|
|
|
|
+ mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'ART_GALLERY': {
|
|
|
|
|
+ needs: [{need: 'culture', supplied: 2500}],
|
|
|
|
|
+ mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'THEATER': {
|
|
|
|
|
+ needs: [{need: 'culture', supplied: 5000}],
|
|
|
|
|
+ mats: {'POW': 1.4, 'MHP': 2, 'HOG': 1.4, 'OLF': 4, 'BID': 0.33, 'DEC': 0.67},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'PLANETARY_BROADCASTING_HUB': {
|
|
|
|
|
+ needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}],
|
|
|
|
|
+ mats: {'OFF': 10, 'MHP': 1, 'SP': 1.33, 'AAR': 0.67, 'EDC': 0.27, 'IDC': 0.13},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'LIBRARY': {
|
|
|
|
|
+ needs: [{need: 'education', supplied: 2500}],
|
|
|
|
|
+ mats: {'MHP': 1, 'HOG': 1, 'CD': 0.33, 'DIS': 0.33, 'BID': 0.2},
|
|
|
|
|
+ },
|
|
|
|
|
+ 'UNIVERSITY': {
|
|
|
|
|
+ needs: [{need: 'education', supplied: 5000}],
|
|
|
|
|
+ mats: {'COF': 10, 'REA': 10, 'TUB': 10, 'BID': 0.33, 'HD': 0.67, 'IDC': 0.2},
|
|
|
|
|
+ },
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
|
|
type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
|
|
@@ -367,3 +435,9 @@ interface Infrastructure {
|
|
|
CurrentLevel: number;
|
|
CurrentLevel: number;
|
|
|
Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
|
|
Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+interface Price {
|
|
|
|
|
+ ticker: string
|
|
|
|
|
+ exchange_code: string
|
|
|
|
|
+ vwap_30d: number
|
|
|
|
|
+}
|