143 lines
5.1 KiB
C++
143 lines
5.1 KiB
C++
#include "game/sim/research.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
|
|
#include "game/sim/numeric.h"
|
|
|
|
namespace sots::sim {
|
|
|
|
bool RollEdgeAvailable(const EdgeAvailability& edge, Species species, TreeBuildMode mode,
|
|
IRandom& rng) {
|
|
if (mode == TreeBuildMode::Everything) return true;
|
|
const float p = edge.Chance(species);
|
|
if (!(p > 0.f)) return false;
|
|
if (mode == TreeBuildMode::NoRoll) return true;
|
|
if (p >= 1.f) return true;
|
|
return rng.NextFloat() <= p;
|
|
}
|
|
|
|
double TechCostMultiplier(int applicableBonusTechsOwned) {
|
|
const double m = 1.0 - 0.25 * std::max(0, applicableBonusTechsOwned);
|
|
return std::max(0.25, m);
|
|
}
|
|
|
|
int TechCost(int baseCost, double multiplier) {
|
|
if (baseCost == kNoResearchCost) return kNoResearchCost;
|
|
return std::max(1, Ftol(static_cast<double>(baseCost) * multiplier));
|
|
}
|
|
|
|
namespace {
|
|
// The image's copies of two float literals, widened to double: the per-turn decay
|
|
// fraction and the "completed early" threshold. Both are (double)0.05f and (double)0.8f,
|
|
// not the exact decimals, and both sit on a truncation/compare boundary where the
|
|
// difference is observable.
|
|
constexpr double kDecayFraction = 0.05000000074505806; // (double)0.05f
|
|
constexpr double kEarlyCompletionRatio = 0.800000011920929; // (double)0.8f
|
|
|
|
// 32-bit signed multiply then truncating division by 100, exactly as the original does it
|
|
// (it wraps rather than widening, which only matters for a cost near INT_MAX).
|
|
int scale_percent(int cost, unsigned percent) {
|
|
const std::int32_t p = static_cast<std::int32_t>(static_cast<std::uint32_t>(cost) * percent);
|
|
return static_cast<int>(p / 100);
|
|
}
|
|
} // namespace
|
|
|
|
int ResearchSpendFloor(int cost) { return std::max(0, scale_percent(cost, 50u)); }
|
|
|
|
int ResearchSpendCeiling(int cost) {
|
|
return std::max(ResearchSpendFloor(cost), scale_percent(cost, 150u));
|
|
}
|
|
|
|
float ResearchCompletionOdds(int progress, int lo, int hi) {
|
|
return static_cast<float>(static_cast<double>(progress - lo) / static_cast<double>(hi));
|
|
}
|
|
|
|
ResearchStepResult ApplyResearchPoints(ResearchNode& node, int points, Species owner, IRandom& rng) {
|
|
ResearchStepResult r;
|
|
const int cost = node.cost;
|
|
const int lo = ResearchSpendFloor(cost);
|
|
const int hi = ResearchSpendCeiling(cost);
|
|
|
|
r.wasCompleteBefore = cost <= node.progress;
|
|
// Signed min with no floor at zero: the original clamps neither side.
|
|
r.spent = std::min(points, hi - node.progress);
|
|
r.overbudget = points - r.spent;
|
|
node.progress += r.spent;
|
|
const bool nowComplete = cost <= node.progress;
|
|
|
|
if (node.progress < hi) {
|
|
if (r.spent == 0) {
|
|
r.odds = 0.f;
|
|
r.roll = 1.f;
|
|
} else {
|
|
r.odds = ResearchCompletionOdds(node.progress, lo, hi);
|
|
r.roll = rng.NextFloat();
|
|
// Zuul draw a second time and keep the lower (better) roll.
|
|
if (owner == Species::Zuul) r.roll = std::min(r.roll, rng.NextFloat());
|
|
}
|
|
} else {
|
|
r.odds = 1.f;
|
|
r.roll = 0.f;
|
|
}
|
|
|
|
if (r.odds < r.roll) {
|
|
if (!r.wasCompleteBefore && nowComplete) {
|
|
r.overbudgetEvent = true;
|
|
node.flag = TechFlag::OverBudgetNotified;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
r.completed = true;
|
|
// The ratio is itself narrowed to float32 before the comparison.
|
|
const float ratio = static_cast<float>(static_cast<double>(node.progress) / static_cast<double>(cost));
|
|
if (static_cast<double>(ratio) < kEarlyCompletionRatio) {
|
|
r.completedEarly = true;
|
|
node.flag = TechFlag::CompletedEarly;
|
|
}
|
|
node.state = TechState::Researched;
|
|
return r;
|
|
}
|
|
|
|
int DecayResearchProgress(int progress, int cost) {
|
|
return std::max(0, progress - Ftol(static_cast<double>(cost) * kDecayFraction));
|
|
}
|
|
|
|
void DecayAllResearch(std::vector<ResearchNode>& nodes) {
|
|
for (ResearchNode& n : nodes) {
|
|
// The original's guard is `progress != 0`, so a negative progress decays too.
|
|
if (n.state == TechState::Available && n.progress != 0) {
|
|
n.progress = DecayResearchProgress(n.progress, n.cost);
|
|
}
|
|
}
|
|
}
|
|
|
|
ResearchTurnResult ProcessResearchTurn(std::vector<ResearchNode>& nodes,
|
|
const std::vector<ResearchAllocEntry>& alloc, Species owner,
|
|
IRandom& rng) {
|
|
ResearchTurnResult out;
|
|
out.steps.reserve(alloc.size());
|
|
for (const ResearchAllocEntry& e : alloc) {
|
|
if (e.nodeIndex < 0 || static_cast<std::size_t>(e.nodeIndex) >= nodes.size()) continue;
|
|
ResearchStepResult s = ApplyResearchPoints(nodes[e.nodeIndex], e.points, owner, rng);
|
|
out.overbudget += s.overbudget;
|
|
out.steps.push_back(s);
|
|
}
|
|
DecayAllResearch(nodes);
|
|
return out;
|
|
}
|
|
|
|
bool RollLabAccident(int oddsPercent, IRandom& rng) {
|
|
const int roll = static_cast<int>(rng.NextIntInclusive(100));
|
|
return roll < oddsPercent;
|
|
}
|
|
|
|
int LabAccidentLossPercent(double minLoss, double maxLoss, IRandom& rng) {
|
|
const double f = Clamp01(static_cast<double>(rng.NextFloat()) * (maxLoss - minLoss) + minLoss);
|
|
return static_cast<int>(std::ceil(f * 100.0));
|
|
}
|
|
|
|
} // namespace sots::sim
|