#include "game/sim/economy.h" #include #include #include "game/sim/numeric.h" namespace sots::sim { int SavingsInterest(int savings, bool ownsSystems) { if (savings < 0 || !ownsSystems) return 0; return Ftol(static_cast(savings) * 0.01); } int DebtInterest(int savings) { if (savings >= 0) return 0; return Ftol(-static_cast(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& 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(static_cast(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(maxC) - minC; const std::int64_t request = static_cast(Ftol(static_cast(s.fraction) * availAsFloat)) - minC; const std::int64_t take = std::min(std::max(request, 0), room); minimums += minC; takes += take; } const std::int64_t headroom = static_cast(availPre) - minimums; const std::int64_t total = minimums + std::min(std::max(takes, 0), headroom); return static_cast(ClampT(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(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(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(std::max(0, running()))); b.available = static_cast(std::max(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(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(static_cast(b.researchMoney) * pct / 100); b.researchPointsGiven = static_cast(static_cast(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(netBeforeBonus))); } // Savings aid: capped by the projected treasury after this turn (bonus included). if (in.aidSavings != 0) { const int netWithBonus = static_cast(ClampT(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(ClampT(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(n) * perFreighter[c]; capLeft -= n; } double v = static_cast(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(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(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(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