|
@@ -100,6 +100,8 @@ async function _render(planetName: string, pop: Pop) {
|
|
|
'culture': calcTotalNeeds(lastPOPR, 'culture'),
|
|
'culture': calcTotalNeeds(lastPOPR, 'culture'),
|
|
|
'education': calcTotalNeeds(lastPOPR, 'education'),
|
|
'education': calcTotalNeeds(lastPOPR, 'education'),
|
|
|
};
|
|
};
|
|
|
|
|
+ const totalPop = lastPOPR.NextPopulationPioneer + lastPOPR.NextPopulationSettler +
|
|
|
|
|
+ lastPOPR.NextPopulationTechnician + lastPOPR.NextPopulationEngineer + lastPOPR.NextPopulationScientist;
|
|
|
|
|
|
|
|
const currentPOPIFilled: POPIFill = new Map();
|
|
const currentPOPIFilled: POPIFill = new Map();
|
|
|
for (const infra of infras) {
|
|
for (const infra of infras) {
|
|
@@ -189,23 +191,32 @@ async function _render(planetName: string, pop: Pop) {
|
|
|
</tr>
|
|
</tr>
|
|
|
</table>
|
|
</table>
|
|
|
current projected ${pop.toUpperCase()} happiness:
|
|
current projected ${pop.toUpperCase()} happiness:
|
|
|
- ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled))}
|
|
|
|
|
|
|
+ ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled, null))}
|
|
|
|
|
|
|
|
<h2>options</h2>
|
|
<h2>options</h2>
|
|
|
<table class="options">
|
|
<table class="options">
|
|
|
<tr>
|
|
<tr>
|
|
|
<th>config</th>
|
|
<th>config</th>
|
|
|
<th>projected happiness</th>
|
|
<th>projected happiness</th>
|
|
|
- <th>projected migration</th>
|
|
|
|
|
|
|
+ <th>projected change</th>
|
|
|
<th>cost/day</th>
|
|
<th>cost/day</th>
|
|
|
<th>unit cost</th>
|
|
<th>unit cost</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
- ${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, prices).map((result) => {
|
|
|
|
|
|
|
+ ${paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, totalPop, prices).map((result) => {
|
|
|
let unitCost = '';
|
|
let unitCost = '';
|
|
|
if (result.cost > 0)
|
|
if (result.cost > 0)
|
|
|
unitCost = formatNum(result.cost / result.change);
|
|
unitCost = formatNum(result.cost / result.change);
|
|
|
return `<tr>
|
|
return `<tr>
|
|
|
- <td>${[...result.config.entries()].map(([building, fill]) => `${building}: ${fill.numMats}`).join('<br>')}</td>
|
|
|
|
|
|
|
+ <td>
|
|
|
|
|
+ ${[...result.config.entries()].map(([building, fill]) => {
|
|
|
|
|
+ let currentBuildingFill = currentPOPIFilled.get(building)!.numMats;
|
|
|
|
|
+ let className = '';
|
|
|
|
|
+ if (fill.numMats > currentBuildingFill) className = 'positive';
|
|
|
|
|
+ else if (fill.numMats < currentBuildingFill) className = 'negative';
|
|
|
|
|
+ return `${building}: <span class="${className}">${fill.numMats}</span>`;
|
|
|
|
|
+ }).join('<br>')}
|
|
|
|
|
+ ${result.govProgram !== null ? `<br>${result.govProgram}` : ''}
|
|
|
|
|
+ </td>
|
|
|
<td>${formatPct(result.happiness)}</td>
|
|
<td>${formatPct(result.happiness)}</td>
|
|
|
<td>${formatDelta(result.change)}</td>
|
|
<td>${formatDelta(result.change)}</td>
|
|
|
<td>${formatNum(result.cost)}</td>
|
|
<td>${formatNum(result.cost)}</td>
|
|
@@ -266,8 +277,9 @@ function calcPOPIFilled(infra: Infrastructure): number | null {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
|
|
function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
|
|
|
- currentPop: number, lowerPop: number, prices: Map<string, number>): {config: POPIFill, happiness: number, change: number, cost: number}[] {
|
|
|
|
|
- const results: {config: POPIFill, happiness: number, change: number, cost: number}[] = [];
|
|
|
|
|
|
|
+ currentPop: number, lowerPop: number, totalPop: number, prices: Map<string, number>):
|
|
|
|
|
+ {config: POPIFill, govProgram: GovProgram | null, happiness: number, change: number, cost: number}[] {
|
|
|
|
|
+ const results: {config: POPIFill, govProgram: GovProgram | null, happiness: number, change: number, cost: number}[] = [];
|
|
|
let education = 0;
|
|
let education = 0;
|
|
|
if (pop !== 'pio') {
|
|
if (pop !== 'pio') {
|
|
|
education = EDUCATION[pop];
|
|
education = EDUCATION[pop];
|
|
@@ -276,29 +288,49 @@ function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: numb
|
|
|
education += (currentPOPIFilled.get('UNIVERSITY')?.tier ?? 0) * 0.004;
|
|
education += (currentPOPIFilled.get('UNIVERSITY')?.tier ?? 0) * 0.004;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const govPrograms: (GovProgram | null)[] = [null];
|
|
|
|
|
+ govPrograms.push(...(Object.keys(GOV_PROGRAMS) as GovProgram[]));
|
|
|
|
|
+
|
|
|
for (const config of popiFillCombinations(currentPOPIFilled)) {
|
|
for (const config of popiFillCombinations(currentPOPIFilled)) {
|
|
|
- const happiness = projectedHappiness(pop, totalNeeds, siteCount, config);
|
|
|
|
|
- let change = 0;
|
|
|
|
|
- if (happiness > 0.7 && ['pio', 'set', 'tec'].includes(pop))
|
|
|
|
|
- change = currentPop * (happiness - 0.7);
|
|
|
|
|
- else if (happiness < 0.5)
|
|
|
|
|
- change = 0.8 * currentPop * (happiness - 0.5);
|
|
|
|
|
-
|
|
|
|
|
- if (pop !== 'pio')
|
|
|
|
|
- change += lowerPop * education * happiness;
|
|
|
|
|
- // TODO: education out
|
|
|
|
|
-
|
|
|
|
|
- let cost = calcCost(config, prices);
|
|
|
|
|
- // is any result better than this one?
|
|
|
|
|
- if (results.some((result) => (result.change >= change && result.cost < cost) ||
|
|
|
|
|
- (result.change > change && result.cost <= cost)))
|
|
|
|
|
- continue;
|
|
|
|
|
- // are any results worse than this one?
|
|
|
|
|
- for (let i = results.length - 1; i >= 0; i--)
|
|
|
|
|
- if ((results[i].change <= change && results[i].cost > cost) ||
|
|
|
|
|
- (results[i].change < change && results[i].cost >= cost))
|
|
|
|
|
- results.splice(i, 1);
|
|
|
|
|
- results.push({config, happiness, change, cost});
|
|
|
|
|
|
|
+ for (const govProgram of govPrograms) {
|
|
|
|
|
+ const happiness = projectedHappiness(pop, totalNeeds, siteCount, config, govProgram);
|
|
|
|
|
+ let change = 0;
|
|
|
|
|
+ if (happiness > 0.7 && ['pio', 'set', 'tec'].includes(pop))
|
|
|
|
|
+ change = currentPop * (happiness - 0.7);
|
|
|
|
|
+ else if (happiness < 0.5)
|
|
|
|
|
+ change = 0.8 * currentPop * (happiness - 0.5);
|
|
|
|
|
+ if (govProgram === 'pio immigration' && pop === 'pio') change += 500;
|
|
|
|
|
+ else if (govProgram === 'set immigration' && pop === 'set') change += 200;
|
|
|
|
|
+ else if (govProgram === 'tec immigration' && pop === 'tec') change += 100;
|
|
|
|
|
+ else if (govProgram === 'eng immigration' && pop === 'eng') change += 50;
|
|
|
|
|
+ else if (govProgram === 'sci immigration' && pop === 'sci') change += 25;
|
|
|
|
|
+
|
|
|
|
|
+ if (pop !== 'pio') {
|
|
|
|
|
+ let eduIn = lowerPop * education * happiness;
|
|
|
|
|
+ if (govProgram === 'education I') eduIn *= 1.5;
|
|
|
|
|
+ else if (govProgram === 'education II') eduIn *= 1.75;
|
|
|
|
|
+ else if (govProgram === 'education III') eduIn *= 2;
|
|
|
|
|
+ change += eduIn;
|
|
|
|
|
+ }
|
|
|
|
|
+ // TODO: education out
|
|
|
|
|
+
|
|
|
|
|
+ let cost = calcCost(config, prices);
|
|
|
|
|
+ if (govProgram !== null) {
|
|
|
|
|
+ const program = GOV_PROGRAMS[govProgram];
|
|
|
|
|
+ cost += (program.baseCost + program.popCost * totalPop) / 7;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // is any result better than this one?
|
|
|
|
|
+ if (results.some((result) => (result.change >= change && result.cost < cost) ||
|
|
|
|
|
+ (result.change > change && result.cost <= cost)))
|
|
|
|
|
+ continue;
|
|
|
|
|
+ // are any results worse than this one?
|
|
|
|
|
+ for (let i = results.length - 1; i >= 0; i--)
|
|
|
|
|
+ if ((results[i].change <= change && results[i].cost > cost) ||
|
|
|
|
|
+ (results[i].change < change && results[i].cost >= cost))
|
|
|
|
|
+ results.splice(i, 1);
|
|
|
|
|
+ results.push({config, govProgram, happiness, change, cost});
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
return results;
|
|
return results;
|
|
|
}
|
|
}
|
|
@@ -321,7 +353,8 @@ function* popiFillCombinations(currentPOPIFilled: POPIFill): Generator<POPIFill>
|
|
|
yield* helper(0, new Map());
|
|
yield* helper(0, new Map());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, popiFilled: POPIFill): number {
|
|
|
|
|
|
|
+function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, popiFilled: POPIFill,
|
|
|
|
|
+ govProgram: GovProgram | null): number {
|
|
|
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()) {
|
|
@@ -351,7 +384,14 @@ function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCoun
|
|
|
let happiness = 1 - Object.values(weights).reduce((sum, weight) => sum + weight, 0); // assume 100% life support
|
|
let happiness = 1 - Object.values(weights).reduce((sum, weight) => sum + weight, 0); // assume 100% life support
|
|
|
for (const [need, s] of satisfaction.entries())
|
|
for (const [need, s] of satisfaction.entries())
|
|
|
happiness += weights[need] * s;
|
|
happiness += weights[need] * s;
|
|
|
- return happiness;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (govProgram === 'festivities I')
|
|
|
|
|
+ happiness += 0.05;
|
|
|
|
|
+ else if (govProgram === 'festivities II')
|
|
|
|
|
+ happiness += 0.1;
|
|
|
|
|
+ else if (govProgram === 'festivities III')
|
|
|
|
|
+ happiness += 0.2;
|
|
|
|
|
+ return Math.min(happiness, 1);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function calcCost(config: POPIFill, prices: Map<string, number>): number {
|
|
function calcCost(config: POPIFill, prices: Map<string, number>): number {
|
|
@@ -444,6 +484,19 @@ const EDUCATION: Record<Exclude<Pop, 'pio'>, number> = {
|
|
|
'eng': 0.0125,
|
|
'eng': 0.0125,
|
|
|
'sci': 0.0075,
|
|
'sci': 0.0075,
|
|
|
}
|
|
}
|
|
|
|
|
+const GOV_PROGRAMS: Record<GovProgram, {baseCost: number, popCost: number}> = {
|
|
|
|
|
+ 'festivities I': {baseCost: 5000, popCost: 0.125},
|
|
|
|
|
+ 'festivities II': {baseCost: 10000, popCost: 0.25},
|
|
|
|
|
+ 'festivities III': {baseCost: 20000, popCost: 0.5},
|
|
|
|
|
+ 'education I': {baseCost: 5000, popCost: 0.15},
|
|
|
|
|
+ 'education II': {baseCost: 10000, popCost: 0.25},
|
|
|
|
|
+ 'education III': {baseCost: 20000, popCost: 0.4},
|
|
|
|
|
+ 'pio immigration': {baseCost: 10000, popCost: 0},
|
|
|
|
|
+ 'set immigration': {baseCost: 25000, popCost: 0},
|
|
|
|
|
+ 'tec immigration': {baseCost: 50000, popCost: 0},
|
|
|
|
|
+ 'eng immigration': {baseCost: 80000, popCost: 0},
|
|
|
|
|
+ 'sci immigration': {baseCost: 125000, popCost: 0},
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
|
|
type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
|
|
|
type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education';
|
|
type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education';
|
|
@@ -451,6 +504,8 @@ type POPIBuilding = 'SAFETY_STATION' | 'SECURITY_DRONE_POST' | 'EMERGENCY_CENTER
|
|
|
'WELLNESS_CENTER' | 'WILDLIFE_PARK' | 'ARCADES' | 'ART_CAFE' | 'ART_GALLERY' | 'THEATER' |
|
|
'WELLNESS_CENTER' | 'WILDLIFE_PARK' | 'ARCADES' | 'ART_CAFE' | 'ART_GALLERY' | 'THEATER' |
|
|
|
'PLANETARY_BROADCASTING_HUB' | 'LIBRARY' | 'UNIVERSITY';
|
|
'PLANETARY_BROADCASTING_HUB' | 'LIBRARY' | 'UNIVERSITY';
|
|
|
type POPIFill = Map<POPIBuilding, {tier: number, numMats: number}>;
|
|
type POPIFill = Map<POPIBuilding, {tier: number, numMats: number}>;
|
|
|
|
|
+type GovProgram = 'festivities I' | 'festivities II' | 'festivities III' | 'education I' | 'education II' | 'education III' |
|
|
|
|
|
+ 'pio immigration' | 'set immigration' | 'tec immigration' | 'eng immigration' | 'sci immigration';
|
|
|
|
|
|
|
|
interface Planet {
|
|
interface Planet {
|
|
|
PopulationReports: POPR[]
|
|
PopulationReports: POPR[]
|