sots-engine/src/game/sim/economy.cpp

189 lines
8.6 KiB
C++

#include "game/sim/economy.h"
#include <algorithm>
#include <cmath>
#include "game/sim/numeric.h"
namespace sots::sim {
int SavingsInterest(int savings, bool ownsSystems) {
if (savings < 0 || !ownsSystems) return 0;
return Ftol(static_cast<double>(savings) * 0.01);
}
int DebtInterest(int savings) {
if (savings >= 0) return 0;
return Ftol(-static_cast<double>(savings) * 0.15);
}
int MaintenanceCost(int maintenance, double difficultyDivisor) {
const int d = Ftol(difficultyDivisor);
if (d == 0) return maintenance;
return maintenance / d;
}
int ExpenseTotal(const std::vector<ExpenseSlider>& sliders, int availableBeforeExpenses) {
constexpr int kUnlimited = 2000000000;
const int availPre = availableBeforeExpenses;
// The game multiplies the slider fraction by the available income in single
// precision, so the income is rounded to a float before the product.
const double availAsFloat = static_cast<double>(static_cast<float>(availPre));
std::int64_t minimums = 0;
std::int64_t takes = 0;
for (const ExpenseSlider& s : sliders) {
const int minC = std::max(s.minimum, 0);
int maxC = ClampT(s.maximum, 0, kUnlimited);
if (maxC == 0) maxC = kUnlimited;
const std::int64_t room = static_cast<std::int64_t>(maxC) - minC;
const std::int64_t request =
static_cast<std::int64_t>(Ftol(static_cast<double>(s.fraction) * availAsFloat)) - minC;
const std::int64_t take = std::min(std::max<std::int64_t>(request, 0), room);
minimums += minC;
takes += take;
}
const std::int64_t headroom = static_cast<std::int64_t>(availPre) - minimums;
const std::int64_t total = minimums + std::min(std::max<std::int64_t>(takes, 0), headroom);
return static_cast<int>(ClampT<std::int64_t>(total, -2147483648LL, 2147483647LL));
}
int ResearchPointsFromMoney(int researchMoney, double difficultyMult, double resMod,
double shrm, double trm, double techMult, double serverResMod,
double resScl) {
const double base = (static_cast<double>(researchMoney) / 50.0) * 1.15 * 0.5 * 0.85;
const double rp = difficultyMult * base * (resMod + shrm + trm) * techMult * serverResMod * resScl;
return Ftol(rp);
}
Budget ComputeBudget(const BudgetInputs& in, bool projected) {
Budget b;
b.savingsInterest = SavingsInterest(in.savings, in.ownsSystems);
b.debtInterest = DebtInterest(in.savings);
for (int inc : in.systemIncome) {
if (inc >= 0) b.systemIncomePositive = SaturatingAdd(b.systemIncomePositive, inc);
else b.systemIncomeNegative = SaturatingAdd(b.systemIncomeNegative, -inc);
}
b.tradeIncome = in.tradeIncome;
b.secondaryManagerIncome = in.secondaryManagerIncome;
b.shipCarriedPopIncome = in.shipCarriedPopIncome;
b.maintenance = MaintenanceCost(in.maintenance, in.maintenanceDivisor);
// Running total of the fixed lines; the slots filled later start at zero.
auto running = [&]() -> std::int64_t {
return static_cast<std::int64_t>(b.systemIncomePositive) + b.tradeIncome +
b.shipCarriedPopIncome + b.secondaryManagerIncome + b.savingsInterest +
b.bonusIncome - b.systemIncomeNegative - b.maintenance - b.researchMoneyKept -
b.debtInterest - b.construction - b.expenses - b.researchMoneyGiven -
b.savingsGiven;
};
b.expenses = ExpenseTotal(in.expenses, static_cast<int>(std::max<std::int64_t>(0, running())));
b.available = static_cast<int>(std::max<std::int64_t>(0, running()));
if (!in.isAI && b.available > 0) {
b.construction = std::min(std::max(0, in.constructionDemand), b.available);
}
const int availAfterConstruction = b.available - b.construction;
const double rate = projected ? 0.0 : in.researchRate;
b.researchMoney = std::max(0, Ftol(static_cast<double>(availAfterConstruction) * rate));
b.researchPoints = ResearchPointsFromMoney(b.researchMoney, in.researchDifficultyMult,
in.resMod, in.shrm, in.trm, in.techResearchMult,
in.serverResMod, in.resScl);
b.totalResearchPoints = std::max(0, b.researchPoints + in.tra + in.trp);
const int pct = ClampT(in.aidResearchPercent, 0, 100);
b.researchMoneyGiven = static_cast<int>(static_cast<std::int64_t>(b.researchMoney) * pct / 100);
b.researchPointsGiven = static_cast<int>(static_cast<std::int64_t>(b.totalResearchPoints) * pct / 100);
b.totalResearchPoints -= b.researchPointsGiven;
b.hasResearchAllocation = in.hasResearchTarget;
b.researchMoneyKept = b.researchMoney - b.researchMoneyGiven;
// Tech income bonus: a share of the full net so far, only when that net is positive.
const std::int64_t netBeforeBonus = running();
if (netBeforeBonus > 0) {
b.bonusIncome = std::max(0, Ftol((in.techIncomeMult - 1.0) * static_cast<double>(netBeforeBonus)));
}
// Savings aid: capped by the projected treasury after this turn (bonus included).
if (in.aidSavings != 0) {
const int netWithBonus = static_cast<int>(ClampT<std::int64_t>(running(), -2147483648LL, 2147483647LL));
const int projectedSavings = SaturatingAdd(in.savings, netWithBonus);
b.savingsGiven = std::min(std::max(projectedSavings, 0), std::max(in.aidSavings, 0));
}
b.net = static_cast<int>(ClampT<std::int64_t>(running(), -2147483648LL, 2147483647LL));
return b;
}
// ---- trade ---------------------------------------------------------------------------
int TradeRoutesSupported(double civilianPop, double imperialPop, const TuningTable& t) {
double routes = 0;
if (t.TRADE_ROUTE_REQ_CIVPOPULATION > 0) routes += std::ceil(civilianPop / t.TRADE_ROUTE_REQ_CIVPOPULATION);
if (t.TRADE_ROUTE_REQ_IMPPOPULATION > 0) routes += std::ceil(imperialPop / t.TRADE_ROUTE_REQ_IMPPOPULATION);
return std::max(1, Ftol(routes));
}
int TradeRouteGrossIncome(const TradeRouteState& route, const TuningTable& t) {
if (route.ageTurns < t.TRADE_ROUTE_STARTUP_TURNS) return t.TRADE_ROUTE_STARTUP_INCOME;
const int perFreighter[3] = {t.TRADE_ROUTE_INCOME_PERFREIGHTER_CRQ,
t.TRADE_ROUTE_INCOME_PERFREIGHTER_CR,
t.TRADE_ROUTE_INCOME_PERFREIGHTER_DE};
int capLeft = t.TRADE_ROUTE_MAX_FREIGHTERS;
std::int64_t income = t.TRADE_ROUTE_MIN_INCOME;
for (int c = 0; c < 3; ++c) {
const int n = std::min(std::max(0, route.freighters[c]), std::max(0, capLeft));
income += static_cast<std::int64_t>(n) * perFreighter[c];
capLeft -= n;
}
double v = static_cast<double>(income);
v *= 1.0 + t.STATION_BONUS_TRADE_INCOME * route.tradeStationsAtSystem;
if (route.partnerAddicted) v *= t.ADDICTION_TRADE_MOD;
return Ftol(v);
}
int TradeRouteIncome(const TradeRouteState& route, bool asOwner, double difficultyTradeMult,
const TuningTable& t) {
const double share = Clamp01(t.TRADE_ROUTE_OWNERS_SHARE);
const double part = asOwner ? share : 1.0 - share;
return Ftol(static_cast<double>(TradeRouteGrossIncome(route, t)) * part * difficultyTradeMult);
}
// ---- bankruptcy -----------------------------------------------------------------------
BankruptcyLimits ComputeBankruptcyLimits(int maxIncome, const TuningTable& t) {
constexpr int kTreasuryLimit = 2000000000;
BankruptcyLimits l;
// Elimination: the debt whose 15 %/turn interest equals the maximum income.
l.eliminationFloor = std::max(Ftol(static_cast<double>(maxIncome) / -0.15), -kTreasuryLimit);
// Protection: the tuned factor times the maximum income, never below the floor.
l.protectionLimit = std::max(-Ftol(t.BANKRUPTCY_PROTECTION_LIMIT_FACTOR * static_cast<double>(maxIncome)),
l.eliminationFloor);
return l;
}
int BankruptcyLevel(int savings, const BankruptcyLimits& limits) {
if (savings < limits.eliminationFloor) return 2;
if (savings < limits.protectionLimit) return 1;
return 0;
}
BankruptcyDecision BankruptcyStep(BankruptcyState& state, int level, int currentTurn,
const TuningTable& t) {
const BankruptcyState old = state;
if (level != state.warningLevel) {
state.warningLevel = level;
state.startTurn = level != 0 ? currentTurn : -1;
}
BankruptcyDecision d;
d.costCutting = level != 0 && old.warningLevel != 0;
d.eliminate = old.warningLevel == 2 &&
(currentTurn - old.startTurn) >= t.BANKRUPTCY_ELIMINATION_TURNS;
return d;
}
} // namespace sots::sim