#include "game/sim/research.h" #include #include #include #include #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(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(static_cast(cost) * percent); return static_cast(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(static_cast(progress - lo) / static_cast(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(static_cast(node.progress) / static_cast(cost)); if (static_cast(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(cost) * kDecayFraction)); } void DecayAllResearch(std::vector& 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& nodes, const std::vector& alloc, Species owner, IRandom& rng) { ResearchTurnResult out; out.steps.reserve(alloc.size()); for (const ResearchAllocEntry& e : alloc) { if (e.nodeIndex < 0 || static_cast(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(rng.NextIntInclusive(100)); return roll < oddsPercent; } int LabAccidentLossPercent(double minLoss, double maxLoss, IRandom& rng) { const double f = Clamp01(static_cast(rng.NextFloat()) * (maxLoss - minLoss) + minLoss); return static_cast(std::ceil(f * 100.0)); } } // namespace sots::sim