From e52610eee301504bac47f57d4f38b74e93bfe4cb Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 10 Feb 2026 00:56:46 +0300 Subject: [PATCH 01/10] Adjust AI constructions algorithms --- assets/localisation/en-US/alice.csv | 28 +- docs/features/UserStoriesGenerated.md | 171 ++++++++++++ src/ai/ai_economy.cpp | 248 ++++++++++++++++-- src/ai/ai_economy.hpp | 13 + src/common_types/stackedcalculation.hpp | 121 +++++++++ .../gui_build_factory_window.hpp | 57 +++- 6 files changed, 590 insertions(+), 48 deletions(-) create mode 100644 docs/features/UserStoriesGenerated.md create mode 100644 src/common_types/stackedcalculation.hpp diff --git a/assets/localisation/en-US/alice.csv b/assets/localisation/en-US/alice.csv index 1418d3c78..b953968e1 100644 --- a/assets/localisation/en-US/alice.csv +++ b/assets/localisation/en-US/alice.csv @@ -1328,15 +1328,15 @@ alice_factory_total_bonus;This sums to about ?Y$x$?W alice_slider_controls;?YLeft-Click?W to increment by ?Y$value$?W\n?YSHIFT-Left-Click?W to increment by ?Y$x$?W\n?YSHIFT-Right-Click?W to increment to maximum alice_slider_controls_2;?YLeft-Click?W to decrement by ?Y$value$?W\n?YSHIFT-Left-Click?W to decrement by ?Y$x$?W\n?YSHIFT-Right-Click?W to decrement to minimum explain_colonial_points;Secondary and Great Powers accumulate colonial points. -province_has_workers;Province has any workforce. -province_has_available_workers;Province has unemployed workforce. -nation_is_factory_type_active;Necessary technology has been researched. -nation_is_factory_type_colonies;Target factory type can be built in colonies. -payback_time;Payback time: ?Y$x$?! -construction_cost;Construction cost: ?Y$x$?! -output_value;Output value: ?Y$x$?! -input_value;Input value: ?Y$x$?! -profitability;Profitability: ?Y$x$?! +province_has_workers;Province has any workforce +province_has_available_workers;Province has unemployed workforce +nation_is_factory_type_active;Necessary technology has been researched +nation_is_factory_type_colonies;This is a colonial province and target factory type can be built in colonies. +payback_time;Payback time: ?Y$value$?! +construction_cost;Construction cost: ?Y$value$?! +output_value;Output value: ?Y$value$?! +input_value;Input value: ?Y$value$?! +profitability;Profitability: ?Y$value$?! alice_lobby_back;Back to lobby alice_lobby_back_tt_1;You're already in the fucking lobby alice_lobby_back_tt_2;Only the host may go back to lobby @@ -1951,3 +1951,13 @@ private_message_button_tooltip;Click to toggle whether this player will receive player_loaded_status;Ready player_loading_status;Loading player_oos_status;OOS +output_is_in_demand;Output is in demand: $value$ +alice_factory_type_score;AI factory type score +factory_exploits_local_potentials;Factory exploits local potentials: $value$ +factory_output_consumed_by_other_factory;Output consumed by other local factory: $value$ +factory_consumes_other_factory_output;Consumes other local factory's output: $value$ +factory_consumes_local_potential_resource;Consumes resource present in local potentials: $value$ +factory_consumes_local_rgo_resource;Consumes local RGO resource: $value$ +total_score;Total score: $value$ +private_investors_avoid_high_taxes;Private investors avoid taxation: $value$ +state_seeks_to_maximize_taxes;The State prioritizes high taxation areas: $value$ diff --git a/docs/features/UserStoriesGenerated.md b/docs/features/UserStoriesGenerated.md new file mode 100644 index 000000000..e792b2e5a --- /dev/null +++ b/docs/features/UserStoriesGenerated.md @@ -0,0 +1,171 @@ +# User Stories +*Automatically generated file on 2026-02-10* + +## US1. Regiment construction + +| AC4 | All goods costs must be built | +| AC5 | But no faster than construction_time | + +## US2. Ships construction + +| AC5 | But no faster than construction_time | + +## US3. Trade + +| AC9 | Wartime embargoes | +| AC10 | diplomatic embargos | +| AC11 | sphere joins embargo | +| AC15 | Equal | +| AC17 | if market capital controller is at war with market coastal controller is different | +| AC2 | register trade demand on transportation labor | +| AC3 | register demand on local transportation | +| AC7 | US3AC8 Ban international sea routes or international land routes based on the corresponding modifiers | +| AC8 | Ban international sea routes or international land routes based on the corresponding modifiers | +| AC12 | subject joins embargo | +| AC21 | effect of scale | +| AC5 | Trade Route Attraction from naval bases | +| AC22 | Trade Route attraction modifier | + +## US4. War and peace + + +## US5. Warscore + +| AC4 | What | +| AC5 | count 50 | +| AC1 | Army arrives to province | +| AC2 | Army siege | + +## US6. Ticking warscore + +| AC3 | Reset selected army orders | +| AC20 | Reduce accumulated ticking warscore when the side adds a new wargoal | +| AC2 | US6AC3 | +| AC12 | US6AC13 US6AC17 US6AC18 US6AC19 Ticking warscore may go into negative only after grace period ends | +| AC5 | US6AC6 Ticking warscope for make_puppet war | +| AC7 | We hold some non | +| AC8 | Battle score | +| AC10 | US6AC17 US6AC18 US6AC19 | + +## US7. Move and Siege army order + +| AC2 | Enable | +| AC1 | Handle | + +## US8. Strategic Redeployment army order + +| AC3 | Toggle strategic redeployment order on B. | +| AC2 | Button to order Strategic Redeployment | +| AC1 | Movement finished | + +## US9. Pursue and engage army order + +| AC1 | Command army to pursue the target | +| AC3 | Button to order pursue_to_engage | +| AC4 | Toggle pursue order on N. | + +## US11. Army supplies + + +## US12. Regiment supplies + + +## US13. Regiments organization regain + +| AC3 | US13AC4 US13AC5 Morale | +| AC7 | Unfulfilled supply doesn't lower max org as it makes half the game unplayable | +| AC8 | Unfilfilled supply doesn't prevent org regain as it makes half the game unplayable | +| AC6 | Max organization of the regiment is 100 | + +## US14. Calculates reinforcement for a particular regiment + + +## US15. Host settings + + +## US16. Player accounts + + +## US17. Ships organization regain + + +## US18. Ships repairs + + +## US20. De facto annexations + + +## US27. Scriptable buttons + +| AC5 | National level interactions first | +| AC1 | US27AC2 US27AC3 US27AC4 | + +## US28. Toggleable windows + +| AC5 | US28AC6 UI variable toggle buttons can have nation | +| AC2 | When clicking button with toggle_ui_key the associated UI variable is toggled True | +| AC4 | Window is shown only when UI variable in `visible_ui_key` is set to True | + +## US29. Scriptable images with dynamic frames through datamodels + +| AC2 | US29AC3 When an icon has `datamodel | +| AC3 | US29AC3 When an icon has `datamodel | +| AC4 | US29AC5 Extracted into a separate function because both icon classes | +| AC6 | US29AC7 | + +## US31. Map + +| AC3 | If a valid province has been selected, reset selection of armies as well | + +## US40. Winter + +| AC1 | Winter textures | +| AC2 | Winter siege attrition | + +## US49. StackedCalculationWithExplanations allows uniting number calculations for backend with explanation tooltips for the UI + +| AC1 | StackedCalculationWithExplanations is constructed with initial float value | +| AC2 | User can add a value with a string_view explanation of the reason | +| AC3 | User can subtract a value with a string_view explanation of the reason | +| AC4 | User can divide current value by provided value with a string_view explanation of the reason | +| AC5 | User can get resulting value from the stack | +| AC6 | User can get all steps used for calculation for tooltipsand UI | +| AC7 | User can reuse existing stack by clearing it | +| AC8 | User can reuse existing stack by reseting it to a different initial value | + +## US50. The State constructs factories + +| AC1 | Each factory type is evaluated by its profitability, payback time, and a number of synergies | +| AC2 | The State takes top 5 factory options for construction | +| AC4 | The State doesn't initiate more constructions if it doesn't have free funds | +| AC3 | The State takes one random option for construction | +| AC5 | The State doesn't evaluate the cost of the factory it constructs beyond inital analysis | + +## US51. Private investment fund builds factories + +| AC6 | Private Investment takes top 5 factory options for contrustion | +| AC1 | Investment fund doesn't build if it's over courage with current investments | +| AC2 | Colonial pops are eligible for construction of certain factories | +| AC3 | Provinces are sorted by population descending | +| AC4 | If the province an existing construction, no new construction will be run | +| AC5 | If the state has over state.defines.factories_per_state factories, no new construction will be done | +| AC7 | Private Investment takes random option out of top 5 | +| AC8 | Private Investment doesn't get afraid of expensive constructions. It initiates one and stop the following ones | + +## US52. Nation builds some random factories if it can build by itself + +| AC2 | Exclude already present factories | +| AC3 | Construction stops when state budget runs out of construction budget appetite | + +## US53. Build Factory Window + +| AC10 | Tooltip for a factory type displays if the factory type has been activated with a technology | +| AC11 | Tooltip for a factory type displays if the target state is a colony and target factory type can be built in colonies | +| AC12 | Tooltip for a factory type displays if the factory type requires potentials and target state doesn't have required potentials | +| AC13 | Tooltip for a factory type displays key economic metrics of the potential construction | +| AC14 | Tooltip for a factory type displays the score AI places to the factory type | + +## US1010. When a factory is created, do market adjustments + +| AC1 | When a factory is created, add 10 units of its output to local market to stimulate consumption and demand registration | +| AC2 | when a factory is created, increase price of its inputs to stimulate producing those | diff --git a/src/ai/ai_economy.cpp b/src/ai/ai_economy.cpp index 783fcefe1..434a8764c 100644 --- a/src/ai/ai_economy.cpp +++ b/src/ai/ai_economy.cpp @@ -13,9 +13,12 @@ #include "economy_factory_view.hpp" #include "province.hpp" #include "money.hpp" +#include namespace ai { +// US50 The State constructs factories + void update_factory_types_priority(sys::state& state) { concurrency::parallel_for(uint32_t(0), state.world.nation_size(), [&](uint32_t id) { dcon::nation_id n{ dcon::nation_id::value_base_t(id) }; @@ -119,6 +122,110 @@ void update_factory_types_priority(sys::state& state) { }); } +struct evaluated_factory_type { + dcon::factory_type_id type; + sys::StackedCalculationWithExplanations score; +}; + +// US50AC1 Each factory type is evaluated by its profitability, payback time, and a number of synergies +sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, + dcon::nation_id nid, + dcon::market_id mid, + dcon::province_id pid, dcon::factory_type_id type, + bool pop_project, + float filter_profitability, + float filter_output_demand_satisfaction, + float filter_payback_time, + float effective_profit) { + + auto wage = state.world.province_get_labor_price(pid, economy::labor::basic_education) * 2.f; + auto outputc = state.world.factory_type_get_output(type); + auto workforce = state.world.factory_type_get_base_workforce(type); + auto& inputs = state.world.factory_type_get_inputs(type); + + bool output_is_in_demand = state.world.market_get_expected_probability_to_buy(mid, outputc) < filter_output_demand_satisfaction; + auto probability_to_sell = state.world.market_get_expected_probability_to_buy(mid, outputc); + auto control = state.world.province_get_control_ratio(pid) + 0.01f; + + float cost = economy::factory_type_build_cost(state, nid, pid, type, pop_project) + 0.1f; + float output = economy::factory_type_output_cost(state, nid, mid, type) * effective_profit; + + // -50%;50% range of miscalculation + // output *= (std::remainder(rng::get_random(state, n.id.value * pid.value * type.id.value) / 100.f, 0.5f) - 0.25f); + + float input = economy::factory_type_input_cost(state, nid, mid, type) + 0.1f; + float profitability = (output - input - wage * workforce) / input; + float payback_time = cost / std::max(0.00001f, (output - input - wage * workforce)); + + auto score = sys::StackedCalculationWithExplanations(1.f); + + score.multiply(std::min(profitability, 100.f), "profitability"); + score.divide(std::clamp(payback_time, 1.f, 365.f * 100.f), "payback_time"); + + if(output_is_in_demand) { + score.multiply(10.f, "output_is_in_demand"); + } + + if(pop_project) { + score.divide(control, "private_investors_avoid_high_taxes"); + } + else { + score.multiply(control, "state_seeks_to_maximize_taxes"); + } + + // Uncomment for release + if(score.getResult() < 1) { + return score; + } + + // Increase score if there are local resouce potentials present + if(outputc.get_uses_potentials() && state.world.province_get_factory_max_size(pid, outputc) > 0) { + score.multiply(10.f, "factory_exploits_local_potentials"); + } + + // Increase score for local synergies + for(auto otherfl : state.world.province_get_factory_location(pid)) { + auto otherf = otherfl.get_factory(); + auto othertype = otherf.get_building_type(); + + for(uint32_t i = 0; i < economy::commodity_set::set_size; ++i) { + auto cid = othertype.get_inputs().commodity_type[i]; + if(!cid) + break; + + if(cid == outputc) { + score.multiply(1.5f, "factory_output_consumed_by_other_factory"); + } + } + + for(uint32_t i = 0; i < economy::commodity_set::set_size; ++i) { + auto cid = inputs.commodity_type[i]; + if(!cid) + break; + + if(cid == othertype.get_output()) { + score.multiply(1.5f, "factory_consumes_other_factory_output"); + } + } + } + + for(uint32_t i = 0; i < economy::commodity_set::set_size; ++i) { + auto cid = inputs.commodity_type[i]; + if(!cid) + break; + + if(state.world.province_get_factory_max_size(pid, cid) > 1) { + score.multiply(1.5f, "factory_consumes_local_potential_resource"); + } + + if(state.world.province_get_rgo(pid) == cid) { + score.multiply(1.5f, "factory_consumes_local_rgo_resource"); + } + } + + return score; +} + void filter_factories_disjunctive( sys::state& state, dcon::nation_id nid, @@ -133,6 +240,8 @@ void filter_factories_disjunctive( ) { assert(desired_types.empty()); + std::vector scores; + auto n = dcon::fatten(state.world, nid); auto wage = state.world.province_get_labor_price(pid, economy::labor::basic_education) * 2.f; @@ -149,33 +258,23 @@ void filter_factories_disjunctive( continue; } - auto estimated_probability_to_buy_output = economy::estimate_probability_to_buy_after_supply_increase( - state, - mid, - state.world.factory_type_get_output(type), - state.world.factory_type_get_output_amount(type) * 0.1f - ); - bool output_is_in_demand = estimated_probability_to_buy_output < filter_output_probability_to_buy; - - float cost = economy::factory_type_build_cost(state, n, pid, type, pop_project) + 0.1f; - // we add a probability to make a mistake: - // if output is equal to 1, then we can underestimate it to be 0.75 or overestimate it to be equal to 1.25 at most - // it provides a quite wide range of potential mistakes which makes the process a bit more interesting - float output = economy::factory_type_output_cost(state, n, mid, type) * effective_profit * (1.f + std::remainder(rng::get_random(state, n.id.value * pid.value * type.id.value) / 100.f, 0.5f) - 0.25f); - float input = economy::factory_type_input_cost(state, n, mid, type) + 0.1f; - float profitability = (output - input - wage * type.get_base_workforce()) / input; - float payback_time = cost / std::max(0.00001f, (output - input - wage * type.get_base_workforce())); - - if( - output_is_in_demand - || profitability > filter_profitability - || payback_time < filter_payback_time - ) { - desired_types.push_back(type.id); - } + auto score = evaluate_factory_type(state, nid, mid, pid, type, pop_project, filter_profitability, filter_output_demand_satisfaction, filter_payback_time, effective_profit); + + scores.push_back(evaluated_factory_type{ type, score }); + } + + std::sort(scores.begin(), scores.end(), [](evaluated_factory_type a, evaluated_factory_type b) { + return a.score.getResult() > b.score.getResult(); + }); + // US50AC2 The State takes top 5 factory options for construction + // US51AC6 Private Investment takes top 5 factory options for contrustion + // Previously used filtering approach with hardcoded values didn't work well for mods changing the vanilla economy + for(size_t i = 0; i < 5 && i < scores.size(); i++) { + desired_types.push_back(scores[i].type); } } +//US50AC10 This is legacy flow that is no longer called for State Constructions, refer to US52 void get_craved_factory_types(sys::state& state, dcon::nation_id nid, dcon::market_id mid, dcon::province_id pid, std::vector& desired_types, bool pop_project) { assert(desired_types.empty()); assert(economy::can_build_factory_in_colony(state, pid)); // Do not call this function if building in state is impossible in principle @@ -301,6 +400,7 @@ void build_or_upgrade_desired_factories( auto sid = state.world.province_get_state_membership(p); auto market = state.world.state_instance_get_market_from_local_market(sid); + // US50AC4 The State doesn't initiate more constructions if it doesn't have free funds if(budget - expenses_accumulator <= 0.f) return; @@ -317,6 +417,7 @@ void build_or_upgrade_desired_factories( continue; // no labor at all } + // US50AC3 The State takes one random option for construction auto type_selection = craved_types[rng::get_random(state, uint32_t(n.index() + int32_t(budget))) % craved_types.size()]; assert(type_selection); @@ -326,8 +427,91 @@ void build_or_upgrade_desired_factories( auto present_factory = retrieve_existing_factory(state, p, type_selection); auto time = state.world.factory_type_get_construction_time(type_selection); auto expected_item_cost = economy::factory_type_build_cost(state, n, p, type_selection, false) / time * days_prepaid; - if(budget - expenses_accumulator - expected_item_cost <= 0.f) + + // US50AC5 The State doesn't evaluate the cost of the factory it constructs beyond inital analysis + // Since high upfront factory costs completely prevent the AI from ever constructing it + //if(budget - expenses_accumulator - expected_item_cost <= 0.f) + // continue; + + if(present_factory) { + if(!upgrade_is_desired(state, present_factory)) { + continue; + } + if(factory_can_be_upgraded(state, n, p, type_selection)) { + new_national_upgrade(state, n, p, type_selection); + expenses_accumulator += expected_item_cost; + } continue; + } else { + // else -- try to build -- must have room + + if(have_available_slots(state, n, p, type_selection)) { + new_national_construction(state, n, p, type_selection); + expenses_accumulator += expected_item_cost; + continue; + } else { + // TODO: try to delete a factory here + } + } + } +} + +void build_or_upgrade_random_factories( + sys::state& state, + dcon::nation_id n, + std::vector& province_priority, + float budget, float& expenses_accumulator, + float filter_profitability, + float filter_output_demand_satisfaction, + float filter_payback_time +) { + float days_prepaid = 0.5f; + static std::vector craved_types; + + for(auto p : province_priority) { + auto sid = state.world.province_get_state_membership(p); + auto market = state.world.state_instance_get_market_from_local_market(sid); + + if(budget - expenses_accumulator <= 0.f) + return; + + if(!province_has_workers(state, p)) { + continue; // no labor at all + } + // US52AC2 Exclude already present factories + craved_types.clear(); + for(auto ftype : state.world.in_factory_type) { + if(!state.world.nation_get_active_building(n, ftype) && !ftype.get_is_available_from_start()) { + continue; + } + // Is particular factory type allowed to be built in colony + if(!economy::can_build_factory_type_in_colony(state, p, ftype)) { + continue; + } + for(auto fl : state.world.province_get_factory_location(p)) { + if(fl.get_factory().get_building_type() != ftype) { + craved_types.push_back(ftype); + } + } + } + + if(craved_types.empty()) { + continue; // no craved factories + } + + // Stops small AIs from building if RGO size > available workforce + // if(!province_has_available_workers(state, p)) + // continue; // no spare workers + + auto type_selection = craved_types[rng::get_random(state, uint32_t(n.index() + int32_t(budget))) % craved_types.size()]; + assert(type_selection); + + if(!can_build(state, p, type_selection)) + continue; + + auto present_factory = retrieve_existing_factory(state, p, type_selection); + auto time = state.world.factory_type_get_construction_time(type_selection); + auto expected_item_cost = economy::factory_type_build_cost(state, n, p, type_selection, false) / time * days_prepaid; if(present_factory) { if(!upgrade_is_desired(state, present_factory)) { @@ -349,6 +533,10 @@ void build_or_upgrade_desired_factories( // TODO: try to delete a factory here } } + + // US52AC3 Construction stops when state budget runs out of construction budget appetite + if(budget - expenses_accumulator - expected_item_cost <= 0.f) + break; } } @@ -403,10 +591,18 @@ void update_ai_econ_construction(sys::state& state) { // try to build insanely good factories if((rules & issue_rule::build_factory) != 0) { // -- i.e. if building is possible - build_or_upgrade_desired_factories( + // US52 Nation builds some random factories if it can build by itself + // Building random factories resolves some other hidden issues with AI factory evaluations that sometimes prevent the AI from constructing some factory types altogether + build_or_upgrade_random_factories( state, n, ordered_provinces, budget, additional_expenses, insanely_good_profitability, insanely_good_demand_supply_disbalance, insanely_good_payback_time ); + + /* build_or_upgrade_desired_factories( + state, n, ordered_provinces, budget, additional_expenses, + insanely_good_profitability, insanely_good_demand_supply_disbalance, insanely_good_payback_time + );*/ + return; } // try to upgrade factories diff --git a/src/ai/ai_economy.hpp b/src/ai/ai_economy.hpp index 7ca5a8277..a48a1a841 100644 --- a/src/ai/ai_economy.hpp +++ b/src/ai/ai_economy.hpp @@ -1,5 +1,8 @@ #pragma once #include "dcon_generated_ids.hpp" +#include "dcon_generated.hpp" +#include + namespace sys { struct state; @@ -11,6 +14,16 @@ bool province_has_available_workers(sys::state& state, dcon::province_id p); bool province_has_workers(sys::state& state, dcon::province_id p); void update_budget(sys::state& state, bool presim = false); +sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, + dcon::nation_id nid, + dcon::market_id mid, + dcon::province_id pid, dcon::factory_type_id type, + bool pop_project, + float filter_profitability, + float filter_output_demand_satisfaction, + float filter_payback_time, + float effective_profit); + void get_craved_factory_types(sys::state& state, dcon::nation_id nid, dcon::market_id mid, dcon::province_id, std::vector& desired_types, bool pop_project); void get_desired_factory_types(sys::state& state, dcon::nation_id nid, dcon::market_id mid, dcon::province_id, std::vector& desired_types, bool pop_project); void update_ai_econ_construction(sys::state& state); diff --git a/src/common_types/stackedcalculation.hpp b/src/common_types/stackedcalculation.hpp new file mode 100644 index 000000000..d5db9f572 --- /dev/null +++ b/src/common_types/stackedcalculation.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace sys { + + // US49 StackedCalculationWithExplanations allows uniting number calculations for backend with explanation tooltips for the UI + class StackedCalculationWithExplanations { + public: + // Operation types + enum class Operation { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE + }; + + // Structure to hold each calculation step + struct CalculationStep { + Operation operation; + float value; + std::string_view explanation; + + CalculationStep(Operation op, float val, std::string_view exp) + : operation(op), value(val), explanation(exp) { } + }; + + private: + float currentValue; + std::vector calculationSteps; + + public: + // US49AC1 StackedCalculationWithExplanations is constructed with initial float value + StackedCalculationWithExplanations(float initialValue = 0.0f) + : currentValue(initialValue) { + + } + + // US49AC2 User can add a value with a string_view explanation of the reason + void add(float value, const std::string_view& explanation) { + calculationSteps.emplace_back( + Operation::ADD, value, explanation + ); + } + + // US49AC3 User can subtract a value with a string_view explanation of the reason + void subtract(float value, const std::string_view& explanation) { + calculationSteps.emplace_back( + Operation::SUBTRACT, value, explanation + ); + } + + // US49AC4 User can multiply current value by provided value with a string_view explanation of the reason + void multiply(float value, const std::string_view& explanation) { + calculationSteps.emplace_back( + Operation::MULTIPLY, value, explanation + ); + } + + // US49AC4 User can divide current value by provided value with a string_view explanation of the reason + void divide(float value, const std::string_view& explanation) { + if(value == 0) { + throw std::runtime_error("Division by zero"); + } + calculationSteps.emplace_back( + Operation::DIVIDE, value, explanation + ); + } + + // US49AC5 User can get resulting value from the stack + float getResult() const { + auto val = currentValue; + for(auto step : calculationSteps) { + if(step.operation == Operation::ADD) { + val += step.value; + } else if(step.operation == Operation::SUBTRACT) { + val -= step.value; + } else if(step.operation == Operation::MULTIPLY) { + val *= step.value; + } else if(step.operation == Operation::DIVIDE) { + val /= step.value; + } + } + return val; + } + + // US49AC6 User can get all steps used for calculation for tooltipsand UI + const std::vector& getSteps() const { + return calculationSteps; + } + + // US49AC7 User can reuse existing stack by clearing it + void clear() { + calculationSteps.clear(); + } + + // US49AC8 User can reuse existing stack by reseting it to a different initial value + void reset(float newInitialValue) { + currentValue = newInitialValue; + calculationSteps.clear(); + } + + private: + // Helper to get symbol for operation + std::string_view getOperationSymbol(Operation op) const { + switch(op) { + case Operation::ADD: return "+"; + case Operation::SUBTRACT: return "-"; + case Operation::MULTIPLY: return "×"; + case Operation::DIVIDE: return "÷"; + default: return "?"; + } + } + }; + +} diff --git a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp index 9c8619838..2bcf68677 100644 --- a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp +++ b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp @@ -10,6 +10,8 @@ #include "economy_factory_view.hpp" #include "economy.hpp" +// US53 Build Factory Window + namespace ui { class factory_build_cancel_button : public generic_close_button { @@ -276,24 +278,32 @@ class factory_build_item_button : public tinted_button_element_base { } text::add_line(state, contents, "alice_factory_total_bonus", text::variable_type::x, text::fp_four_places{ sum });*/ - /* If mod uses Factory Province limits */ - auto output = state.world.factory_type_get_output(content); - if(state.world.commodity_get_uses_potentials(output)) { - auto limit = economy::calculate_state_factory_limit(state, sid, output); - - text::add_line_with_condition(state, contents, "factory_build_condition_11", 1 <= limit); - } + text::add_line_break_to_layout(state, contents); text::add_line(state, contents, "alice_building_conditions"); + // US53AC10 Tooltip for a factory type displays if the factory type has been activated with a technology + text::add_line_with_condition(state, contents, "nation_is_factory_type_active", state.world.nation_get_active_building(n, content) || state.world.factory_type_get_is_available_from_start(content), 15); auto p = state.world.state_instance_get_capital(sid); + // US53AC11 Tooltip for a factory type displays if the target state is a colony and target factory type can be built in colonies + if(state.world.province_get_is_colonial(p)) { text::add_line_with_condition(state, contents, "nation_is_factory_type_colonies", state.world.factory_type_get_can_be_built_in_colonies(content), 15); } + // US53AC12 Tooltip for a factory type displays if the factory type requires potentials and target state doesn't have required potentials + + /* If mod uses Factory Province limits */ + auto output = state.world.factory_type_get_output(content); + if(state.world.commodity_get_uses_potentials(output)) { + auto limit = economy::calculate_province_factory_limit(state, p, output); + + text::add_line_with_condition(state, contents, "factory_build_condition_11", 1 <= limit, 15); + } + auto const tax_eff = economy::tax_collection_rate(state, n, p); auto mid = state.world.state_instance_get_market_from_local_market(sid); @@ -308,15 +318,36 @@ class factory_build_item_button : public tinted_button_element_base { float profitability = (output_value - input - wage * content.get_base_workforce()) / input; float payback_time = cost / std::max(0.00001f, (output_value - input - wage * content.get_base_workforce())); - text::add_line(state, contents, "construction_cost", text::variable_type::x, text::fp_currency{ cost }); - text::add_line(state, contents, "input_value", text::variable_type::x, text::fp_currency{ input }); - text::add_line(state, contents, "output_value", text::variable_type::x, text::fp_currency{ output_value }); - text::add_line(state, contents, "profitability", text::variable_type::x, text::fp_percentage_one_place{ profitability }); - text::add_line(state, contents, "payback_time", text::variable_type::x, text::fp_two_places{ payback_time }); + // US53AC13 Tooltip for a factory type displays key economic metrics of the potential construction: construction cost, input costs, output prices, profitability, payback type + + text::add_line(state, contents, "construction_cost", text::variable_type::value, text::fp_currency{ cost }); + text::add_line(state, contents, "input_value", text::variable_type::value, text::fp_currency{ input }); + text::add_line(state, contents, "output_value", text::variable_type::value, text::fp_currency{ output_value }); + text::add_line(state, contents, "profitability", text::variable_type::value, text::fp_percentage_one_place{ profitability }); + text::add_line(state, contents, "payback_time", text::variable_type::value, text::fp_two_places{ payback_time }); // Some extra outputs for AI debugging - text::add_line(state, contents, "alice_pop_show_details"); + text::add_line_break_to_layout(state, contents); + text::add_line(state, contents, "alice_factory_type_score"); + + // US53AC14 Tooltip for a factory type displays the score AI places to the factory type + + auto factory_type_score = ai::evaluate_factory_type(state, n, mid, p, content, false, 0.3f, 0.5f, 200.f, rich_effect); + + for(auto line : factory_type_score.getSteps()) { + if(line.operation == sys::StackedCalculationWithExplanations::Operation::ADD || line.operation == sys::StackedCalculationWithExplanations::Operation::SUBTRACT) { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); + } + else if (line.operation == sys::StackedCalculationWithExplanations::Operation::DIVIDE) { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ 1 / line.value }, 15); + } + else { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); + } + } + + text::add_line(state, contents, "total_score", text::variable_type::value, text::fp_one_place{ factory_type_score.getResult() }); text::add_line_with_condition(state, contents, "province_has_workers", ai::province_has_workers(state, p)); From 9e3920f0bb6cb0cfbf858a48bc49951bd6c47e64 Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 10 Feb 2026 11:05:57 +0300 Subject: [PATCH 02/10] Fix variable name --- src/ai/ai_economy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai/ai_economy.cpp b/src/ai/ai_economy.cpp index 434a8764c..752bcefd7 100644 --- a/src/ai/ai_economy.cpp +++ b/src/ai/ai_economy.cpp @@ -258,7 +258,7 @@ void filter_factories_disjunctive( continue; } - auto score = evaluate_factory_type(state, nid, mid, pid, type, pop_project, filter_profitability, filter_output_demand_satisfaction, filter_payback_time, effective_profit); + auto score = evaluate_factory_type(state, nid, mid, pid, type, pop_project, filter_profitability, filter_output_probability_to_buy, filter_payback_time, effective_profit); scores.push_back(evaluated_factory_type{ type, score }); } From c9e4fa2247fb545f4133b54916613e02e8000a85 Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 10 Feb 2026 19:44:31 +0300 Subject: [PATCH 03/10] Document economy scene UI --- src/gui/economy_viewer.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/gui/economy_viewer.cpp b/src/gui/economy_viewer.cpp index 52e3641dd..b4ba684a6 100644 --- a/src/gui/economy_viewer.cpp +++ b/src/gui/economy_viewer.cpp @@ -383,6 +383,8 @@ void update(sys::state& state) { }); } } else if(state.selected_trade_good && state.iui_state.tab == iui::iui_tab::commodities_markets) { + // US48 Economic Scene Commodities tab + // US48AC0 Display data only if a commodity is selected if(state.iui_state.national_data) { state.world.for_each_nation([&](dcon::nation_id n) { auto exists = (state.world.nation_get_owned_province_count(n) != 0); @@ -390,19 +392,23 @@ void update(sys::state& state) { return; } switch(state.iui_state.selected_commodity_info) { + // US48AC1 On national level, when price option is selected, display median price case iui::commodity_info_mode::price: state.iui_state.per_nation_data[n.index()] = economy::median_price(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::supply: + // US48AC2 On national level, when supply option is selected, display total supply (SUM) state.iui_state.per_nation_data[n.index()] = economy::supply(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::demand: + // US48AC3 On national level, when demand option is selected, display total supply (SUM) state.iui_state.per_nation_data[n.index()] = economy::demand(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::production: + // US48AC4 On national level, when a production option is selected, display total production (SUM) state.iui_state.per_nation_data[n.index()] = std::max( 0.f, @@ -412,6 +418,7 @@ void update(sys::state& state) { break; case iui::commodity_info_mode::consumption: + // US48AC5 On national level, when a consumption option is selected, display total consumption (SUM) state.iui_state.per_nation_data[n.index()] = std::max( 0.f, @@ -421,17 +428,20 @@ void update(sys::state& state) { break; case iui::commodity_info_mode::stockpiles: + // US48AC6 On national level, when a stockpiles option is selected, display total stockpiles (SUM) state.iui_state.per_nation_data[n.index()] = economy::stockpile(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::potentials: { + // US48AC7 On national level, when a potentials option is selected, display total potentials (SUM) state.iui_state.per_nation_data[n.index()] = (float) economy::calculate_nation_factory_limit(state, n, state.selected_trade_good); break; } case iui::commodity_info_mode::balance: { + // US48AC8 On national level, when a balance option is selected, display total balance with logarithmic scale (SUM) auto supply = economy::supply(state, n, state.selected_trade_good); auto demand = economy::demand(state, n, state.selected_trade_good); auto shift = 0.001f; @@ -443,15 +453,18 @@ void update(sys::state& state) { } case iui::commodity_info_mode::trade_in: + // US48AC9 On national level, when a trade_in option is selected, display total imports volume (SUM of goods) state.iui_state.per_nation_data[n.index()] = economy::import_volume(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::trade_out: + // US48AC10 On national level, when a trade_out option is selected, display total exports volume (SUM of goods) state.iui_state.per_nation_data[n.index()] = economy::export_volume(state, n, state.selected_trade_good); break; case iui::commodity_info_mode::trade_balance: { + // US48AC11 On national level, when a trade_balance option is selected, display total trade_balance volume (SUM of goods) auto supply = economy::import_volume(state, n, state.selected_trade_good); auto demand = economy::export_volume(state, n, state.selected_trade_good); auto shift = 0.001f; @@ -468,18 +481,22 @@ void update(sys::state& state) { state.world.for_each_market([&](dcon::market_id market) { switch(state.iui_state.selected_commodity_info) { case iui::commodity_info_mode::price: + // US48AC12 On market level, when a price option is selected, display price state.iui_state.per_market_data[market.index()] = state.world.market_get_price(market, state.selected_trade_good); break; case iui::commodity_info_mode::supply: + // US48AC13 On market level, when a supply option is selected, display commodity supply state.iui_state.per_market_data[market.index()] = state.world.market_get_supply(market, state.selected_trade_good); break; case iui::commodity_info_mode::demand: + // US48AC14 On market level, when a demand option is selected, display commodity demand state.iui_state.per_market_data[market.index()] = state.world.market_get_demand(market, state.selected_trade_good); break; case iui::commodity_info_mode::production: + // US48AC15 On market level, when a production option is selected, display commodity production state.iui_state.per_market_data[market.index()] = std::max( 0.f, @@ -489,6 +506,7 @@ void update(sys::state& state) { break; case iui::commodity_info_mode::consumption: + // US48AC16 On market level, when a consumption option is selected, display commodity consumption state.iui_state.per_market_data[market.index()] = std::max( 0.f, @@ -498,11 +516,13 @@ void update(sys::state& state) { break; case iui::commodity_info_mode::stockpiles: + // US48AC17 On market level, when a stockpiles option is selected, display commodity stockpiles state.iui_state.per_market_data[market.index()] = state.world.market_get_stockpile(market, state.selected_trade_good); break; case iui::commodity_info_mode::potentials: { + // US48AC19 On market level, when a potentials option is selected, display commodity potentials auto loc = state.world.market_get_zone_from_local_market(market); state.iui_state.per_market_data[market.index()] = (float) economy::calculate_state_factory_limit(state, loc, state.selected_trade_good); cut_away_negative = true; @@ -512,6 +532,7 @@ void update(sys::state& state) { case iui::commodity_info_mode::balance: { + // US48AC20 On market level, when a balance option is selected, display commodity trade balance auto supply = state.world.market_get_supply(market, state.selected_trade_good); auto demand = state.world.market_get_demand(market, state.selected_trade_good); auto shift = 0.001f; @@ -523,15 +544,18 @@ void update(sys::state& state) { } case iui::commodity_info_mode::trade_in: + // US48AC21 On market level, when a trade_in option is selected, display commodity trade_in volume state.iui_state.per_market_data[market.index()] = economy::trade_influx(state, market, state.selected_trade_good); break; case iui::commodity_info_mode::trade_out: + // US48AC22 On market level, when a trade_out option is selected, display commodity trade_out volume state.iui_state.per_market_data[market.index()] = economy::trade_outflux(state, market, state.selected_trade_good); break; case iui::commodity_info_mode::trade_balance: { + // US48AC23 On market level, when a trade_balance option is selected, display commodity trade_balance volume (supply - demand) auto supply = economy::trade_influx(state, market, state.selected_trade_good); auto demand = economy::trade_outflux(state, market, state.selected_trade_good); auto shift = 0.001f; @@ -1250,7 +1274,7 @@ void render(sys::state& state) { for(uint8_t i = 0; i < uint8_t(iui::commodity_info_mode::total); i++) { iui::rect button_rect = { size_selector_w + 10.f, screen_size.y - 350.f + i * view_mode_height, view_mode_width, view_mode_height }; - // Don't show Potentials buttons if there are no potentials for this good + // US48AC19 On market level, display potentials option only for commodities that use resource potentials (mods-only feature) if(!economy::get_commodity_uses_potentials(state, state.selected_trade_good) && (uint8_t)iui::commodity_info_mode::potentials == i) { continue; } From f420b927efd37851b87f0bf42266770293e9a17f Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 10 Feb 2026 19:47:19 +0300 Subject: [PATCH 04/10] Fix numbering --- src/gui/economy_viewer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/economy_viewer.cpp b/src/gui/economy_viewer.cpp index b4ba684a6..b623762ae 100644 --- a/src/gui/economy_viewer.cpp +++ b/src/gui/economy_viewer.cpp @@ -1274,7 +1274,7 @@ void render(sys::state& state) { for(uint8_t i = 0; i < uint8_t(iui::commodity_info_mode::total); i++) { iui::rect button_rect = { size_selector_w + 10.f, screen_size.y - 350.f + i * view_mode_height, view_mode_width, view_mode_height }; - // US48AC19 On market level, display potentials option only for commodities that use resource potentials (mods-only feature) + // US48AC18 On market level, display potentials option only for commodities that use resource potentials (mods-only feature) if(!economy::get_commodity_uses_potentials(state, state.selected_trade_good) && (uint8_t)iui::commodity_info_mode::potentials == i) { continue; } From 006fa220f99df290ab8cb242d120ce9ed19f9c27 Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Sun, 15 Feb 2026 17:10:22 +0300 Subject: [PATCH 05/10] Implement Expression templates for stacked calculations --- CMakeLists.txt | 1 + assets/localisation/en-US/alice.csv | 6 +- src/ai/ai_economy.cpp | 74 ++++--- src/ai/ai_economy.hpp | 18 +- src/common_types/stackedcalculation.hpp | 195 ++++++++++-------- .../gui_build_factory_window.cpp | 176 ++++++++++++++++ .../gui_build_factory_window.hpp | 175 +--------------- src/launcher/launcher_main.hpp | 2 + src/main.cpp | 1 + 9 files changed, 359 insertions(+), 289 deletions(-) create mode 100644 src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 520823e0e..b2f8f8ab5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,7 @@ list(APPEND ALICE_INCREMENTAL_SOURCES_LIST "src/gui/topbar_subwindows/gui_population_window.cpp" "src/gui/topbar_subwindows/gui_production_window.cpp" "src/gui/topbar_subwindows/gui_technology_window.cpp" + "src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp" "src/gui/immediate_mode.cpp" "src/gui/economy_viewer.cpp" "src/gui/unit_tooltip.cpp" diff --git a/assets/localisation/en-US/alice.csv b/assets/localisation/en-US/alice.csv index b953968e1..45acdbc66 100644 --- a/assets/localisation/en-US/alice.csv +++ b/assets/localisation/en-US/alice.csv @@ -1951,13 +1951,17 @@ private_message_button_tooltip;Click to toggle whether this player will receive player_loaded_status;Ready player_loading_status;Loading player_oos_status;OOS -output_is_in_demand;Output is in demand: $value$ +output_is_not_in_demand;Output is not in high demand: $value$ +output_is_in_demand;Output is in high demand: $value$ alice_factory_type_score;AI factory type score factory_exploits_local_potentials;Factory exploits local potentials: $value$ +factory_doesnt_exploit_local_potentials;Factory doesn't exploit local potentials: $value$ factory_output_consumed_by_other_factory;Output consumed by other local factory: $value$ factory_consumes_other_factory_output;Consumes other local factory's output: $value$ factory_consumes_local_potential_resource;Consumes resource present in local potentials: $value$ factory_consumes_local_rgo_resource;Consumes local RGO resource: $value$ total_score;Total score: $value$ +this_is_not_a_pop_project;This is not a pop project: $value$ private_investors_avoid_high_taxes;Private investors avoid taxation: $value$ +this_is_not_a_state_project;This is not a state project: $value$ state_seeks_to_maximize_taxes;The State prioritizes high taxation areas: $value$ diff --git a/src/ai/ai_economy.cpp b/src/ai/ai_economy.cpp index 752bcefd7..59fddd79c 100644 --- a/src/ai/ai_economy.cpp +++ b/src/ai/ai_economy.cpp @@ -122,13 +122,9 @@ void update_factory_types_priority(sys::state& state) { }); } -struct evaluated_factory_type { - dcon::factory_type_id type; - sys::StackedCalculationWithExplanations score; -}; - // US50AC1 Each factory type is evaluated by its profitability, payback time, and a number of synergies -sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, +// Unprofitable factories have negative scores +factory_evaluation_stack evaluate_factory_type(sys::state& state, dcon::nation_id nid, dcon::market_id mid, dcon::province_id pid, dcon::factory_type_id type, @@ -150,40 +146,42 @@ sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, float cost = economy::factory_type_build_cost(state, nid, pid, type, pop_project) + 0.1f; float output = economy::factory_type_output_cost(state, nid, mid, type) * effective_profit; - // -50%;50% range of miscalculation - // output *= (std::remainder(rng::get_random(state, n.id.value * pid.value * type.id.value) / 100.f, 0.5f) - 0.25f); - float input = economy::factory_type_input_cost(state, nid, mid, type) + 0.1f; float profitability = (output - input - wage * workforce) / input; float payback_time = cost / std::max(0.00001f, (output - input - wage * workforce)); - auto score = sys::StackedCalculationWithExplanations(1.f); - - score.multiply(std::min(profitability, 100.f), "profitability"); - score.divide(std::clamp(payback_time, 1.f, 365.f * 100.f), "payback_time"); + auto score = sys::stacked_calculation(1.f) + .multiply(std::min(profitability, 100.f), "profitability") + .divide(std::clamp(payback_time, 1.f, 365.f * 100.f), "payback_time"); + auto score2 = score.multiply(1.f, "output_is_not_in_demand"); if(output_is_in_demand) { - score.multiply(10.f, "output_is_in_demand"); + score2 = score.multiply(10.f, "output_is_in_demand"); } + // Increase score with diminishing control if this is a pop project + auto score3 = score2.divide(1.f, "this_is_not_a_pop_project"); if(pop_project) { - score.divide(control, "private_investors_avoid_high_taxes"); - } - else { - score.multiply(control, "state_seeks_to_maximize_taxes"); + // Control is usually <100% so this increases the score + score3 = score2.divide(control, "private_investors_avoid_high_taxes"); } - // Uncomment for release - if(score.getResult() < 1) { - return score; + // Higher score for better controlled provinces if this is a state project + auto score4 = score3.multiply(1.f, "this_is_not_a_state_project"); + if(!pop_project) { + score4 = score3.multiply(control, "state_seeks_to_maximize_taxes"); } // Increase score if there are local resouce potentials present + auto score5 = score4.multiply(1.f, "factory_doesnt_exploit_local_potentials"); if(outputc.get_uses_potentials() && state.world.province_get_factory_max_size(pid, outputc) > 0) { - score.multiply(10.f, "factory_exploits_local_potentials"); + score5 = score4.multiply(10.f, "factory_exploits_local_potentials"); } // Increase score for local synergies + auto factory_output_consumed_by_other_factory = 1.f; + auto factory_consumes_other_factory_output = 1.f; + for(auto otherfl : state.world.province_get_factory_location(pid)) { auto otherf = otherfl.get_factory(); auto othertype = otherf.get_building_type(); @@ -194,7 +192,7 @@ sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, break; if(cid == outputc) { - score.multiply(1.5f, "factory_output_consumed_by_other_factory"); + factory_output_consumed_by_other_factory *= 1.5f; } } @@ -204,28 +202,41 @@ sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, break; if(cid == othertype.get_output()) { - score.multiply(1.5f, "factory_consumes_other_factory_output"); + factory_consumes_other_factory_output *= 1.f; } } } + auto factory_consumes_local_potential_resource = 1.f; + auto factory_consumes_local_rgo_resource = 1.f; + for(uint32_t i = 0; i < economy::commodity_set::set_size; ++i) { auto cid = inputs.commodity_type[i]; if(!cid) break; if(state.world.province_get_factory_max_size(pid, cid) > 1) { - score.multiply(1.5f, "factory_consumes_local_potential_resource"); + factory_consumes_local_potential_resource *= 1.5f; } if(state.world.province_get_rgo(pid) == cid) { - score.multiply(1.5f, "factory_consumes_local_rgo_resource"); + factory_consumes_local_rgo_resource *= 1.5f; } } - return score; + auto score6 = score5.multiply(factory_output_consumed_by_other_factory, "factory_output_consumed_by_other_factory") + .multiply(factory_consumes_other_factory_output, "factory_consumes_other_factory_output") + .multiply(factory_consumes_local_potential_resource, "factory_consumes_local_potential_resource") + .multiply(factory_consumes_local_rgo_resource, "factory_consumes_local_rgo_resource"); + + return score6; } +struct evaluated_factory_type { + dcon::factory_type_id type; + float score; +}; + void filter_factories_disjunctive( sys::state& state, dcon::nation_id nid, @@ -260,15 +271,18 @@ void filter_factories_disjunctive( auto score = evaluate_factory_type(state, nid, mid, pid, type, pop_project, filter_profitability, filter_output_probability_to_buy, filter_payback_time, effective_profit); - scores.push_back(evaluated_factory_type{ type, score }); + // Build only profitable factories + if(score.getResult() > 0.f) { + scores.push_back(evaluated_factory_type{ type, score.getResult() }); + } } std::sort(scores.begin(), scores.end(), [](evaluated_factory_type a, evaluated_factory_type b) { - return a.score.getResult() > b.score.getResult(); + return a.score > b.score; }); // US50AC2 The State takes top 5 factory options for construction // US51AC6 Private Investment takes top 5 factory options for contrustion - // Previously used filtering approach with hardcoded values didn't work well for mods changing the vanilla economy + // Previously used approach that had filters with hardcoded values didn't work well for mods changing the vanilla economy for(size_t i = 0; i < 5 && i < scores.size(); i++) { desired_types.push_back(scores[i].type); } diff --git a/src/ai/ai_economy.hpp b/src/ai/ai_economy.hpp index a48a1a841..c0927c19c 100644 --- a/src/ai/ai_economy.hpp +++ b/src/ai/ai_economy.hpp @@ -14,7 +14,23 @@ bool province_has_available_workers(sys::state& state, dcon::province_id p); bool province_has_workers(sys::state& state, dcon::province_id p); void update_budget(sys::state& state, bool presim = false); -sys::StackedCalculationWithExplanations evaluate_factory_type(sys::state& state, + +using factory_evaluation_stack = decltype( + std::declval>() + .multiply(1.f, "profitability") + .divide(1.f, "payback_time") + .multiply(1.f, "output_is_in_demand") + .divide(1.f, "private_investors_avoid_high_taxes") + .multiply(1.f, "state_seeks_to_maximize_taxes") + .multiply(10.f, "factory_exploits_local_potentials") + .multiply(1.f, "factory_output_consumed_by_other_factory") + .multiply(1.f, "factory_consumes_other_factory_output") + .multiply(1.f, "factory_consumes_local_potential_resource") + .multiply(1.f, "factory_consumes_local_rgo_resource") +); + +// Returns score AI (nation or private investors) places to the factory type construction in the given market with explanation localisation keys. Filters are redundant for now +factory_evaluation_stack evaluate_factory_type(sys::state& state, dcon::nation_id nid, dcon::market_id mid, dcon::province_id pid, dcon::factory_type_id type, diff --git a/src/common_types/stackedcalculation.hpp b/src/common_types/stackedcalculation.hpp index d5db9f572..ceb0daf67 100644 --- a/src/common_types/stackedcalculation.hpp +++ b/src/common_types/stackedcalculation.hpp @@ -6,116 +6,145 @@ #include #include #include +#include +#include namespace sys { - // US49 StackedCalculationWithExplanations allows uniting number calculations for backend with explanation tooltips for the UI - class StackedCalculationWithExplanations { - public: - // Operation types - enum class Operation { - ADD, - SUBTRACT, - MULTIPLY, - DIVIDE - }; - - // Structure to hold each calculation step - struct CalculationStep { - Operation operation; - float value; - std::string_view explanation; - - CalculationStep(Operation op, float val, std::string_view exp) - : operation(op), value(val), explanation(exp) { } - }; + // Operation types + enum class Operation { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE + }; + + // Structure to hold each calculation step + struct CalculationStep { + Operation operation; + float value; + std::string_view explanation; + + CalculationStep(Operation op, float val, std::string_view exp) + : operation(op), value(val), explanation(exp) { } + }; + + // ------------------------------------------------------------------- + // Step nodes – each node represents one operation and stores the rest + // of the steps as a nested structure. + // ------------------------------------------------------------------- + + // Empty step list + struct NoStep { }; + + // A single step node holds the operation type as a non-type template parameter, + // the operand value, the explanation, and the remaining steps. + template + struct StepNode { + float value; + std::string_view explanation; + Next next; + + StepNode(float v, std::string_view exp, Next n) + : value(v), explanation(exp), next(std::move(n)) { } + }; + // US49 stacked_calculation class allows uniting number calculations for backend with explanation tooltips for the UI + template + class stacked_calculation { private: - float currentValue; - std::vector calculationSteps; + float initialValue; + Steps steps; - public: - // US49AC1 StackedCalculationWithExplanations is constructed with initial float value - StackedCalculationWithExplanations(float initialValue = 0.0f) - : currentValue(initialValue) { + // Apply a single operation to a value + static float apply(Operation op, float current, float val) { + switch(op) { + case Operation::ADD: return current + val; + case Operation::SUBTRACT: return current - val; + case Operation::MULTIPLY: return current * val; + case Operation::DIVIDE: + if(val == 0.0f) throw std::runtime_error("Division by zero"); + return current / val; + } + return current; // unreachable + } + // Evaluation helpers + static float evaluate_impl(const NoStep&, float current) { + return current; } - // US49AC2 User can add a value with a string_view explanation of the reason - void add(float value, const std::string_view& explanation) { - calculationSteps.emplace_back( - Operation::ADD, value, explanation - ); + template + static float evaluate_impl(const StepNode& step, float current) { + float newCurrent = apply(Op, current, step.value); + return evaluate_impl(step.next, newCurrent); } - // US49AC3 User can subtract a value with a string_view explanation of the reason - void subtract(float value, const std::string_view& explanation) { - calculationSteps.emplace_back( - Operation::SUBTRACT, value, explanation - ); + // First collect all previous steps, then add the current one (so the order is preserved). + static void collect_steps(const NoStep&, std::vector&) { } + + template + static void collect_steps(const StepNode& step, + std::vector& out) { + collect_steps(step.next, out); + out.emplace_back(Op, step.value, step.explanation); } + public: + // US49AC1 stacked_calculation is constructed with initial float value (default 0.0f) and no steps + explicit stacked_calculation(float init = 0.0f) + : initialValue(init), steps() { } + + // Internal constructor used when appending a step. + stacked_calculation(float init, Steps s) + : initialValue(init), steps(std::move(s)) { } + + // US49AC2 User can add a value with a string_view explanation of the reason + auto add(float value, std::string_view explanation) const { + auto newSteps = StepNode(value, explanation, steps); + return stacked_calculation( + initialValue, std::move(newSteps)); + } + // US49AC3 User can subtract a value with a string_view explanation of the reason + auto subtract(float value, std::string_view explanation) const { + auto newSteps = StepNode(value, explanation, steps); + return stacked_calculation( + initialValue, std::move(newSteps)); + } // US49AC4 User can multiply current value by provided value with a string_view explanation of the reason - void multiply(float value, const std::string_view& explanation) { - calculationSteps.emplace_back( - Operation::MULTIPLY, value, explanation - ); + auto multiply(float value, std::string_view explanation) const { + auto newSteps = StepNode(value, explanation, steps); + return stacked_calculation( + initialValue, std::move(newSteps)); } - - // US49AC4 User can divide current value by provided value with a string_view explanation of the reason - void divide(float value, const std::string_view& explanation) { - if(value == 0) { - throw std::runtime_error("Division by zero"); - } - calculationSteps.emplace_back( - Operation::DIVIDE, value, explanation - ); + // US49AC5 User can divide current value by provided value with a string_view explanation of the reason + auto divide(float value, std::string_view explanation) const { + // Division‑by‑zero check is deferred until evaluation (lazy). + auto newSteps = StepNode(value, explanation, steps); + return stacked_calculation( + initialValue, std::move(newSteps)); } // US49AC5 User can get resulting value from the stack float getResult() const { - auto val = currentValue; - for(auto step : calculationSteps) { - if(step.operation == Operation::ADD) { - val += step.value; - } else if(step.operation == Operation::SUBTRACT) { - val -= step.value; - } else if(step.operation == Operation::MULTIPLY) { - val *= step.value; - } else if(step.operation == Operation::DIVIDE) { - val /= step.value; - } - } - return val; + return evaluate_impl(steps, initialValue); } - // US49AC6 User can get all steps used for calculation for tooltipsand UI - const std::vector& getSteps() const { - return calculationSteps; + std::vector getSteps() const { + std::vector result; + collect_steps(steps, result); + return result; } // US49AC7 User can reuse existing stack by clearing it - void clear() { - calculationSteps.clear(); + auto clear() const { + return stacked_calculation(initialValue); } - // US49AC8 User can reuse existing stack by reseting it to a different initial value - void reset(float newInitialValue) { - currentValue = newInitialValue; - calculationSteps.clear(); - } - - private: - // Helper to get symbol for operation - std::string_view getOperationSymbol(Operation op) const { - switch(op) { - case Operation::ADD: return "+"; - case Operation::SUBTRACT: return "-"; - case Operation::MULTIPLY: return "×"; - case Operation::DIVIDE: return "÷"; - default: return "?"; - } + auto reset(float newInitialValue) const { + return stacked_calculation(newInitialValue); } }; + } diff --git a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp new file mode 100644 index 000000000..5cd0868d2 --- /dev/null +++ b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp @@ -0,0 +1,176 @@ +#include "gui_build_factory_window.hpp" +#include "economy_government.hpp" +#include "ai_economy.hpp" + +namespace ui { + + void factory_build_item_button::update_tooltip(sys::state& state, int32_t x, int32_t y, text::columnar_layout& contents) noexcept { + if(retrieve(state, parent)) { + text::add_line(state, contents, "alice_recommended_build"); + } + + auto content = dcon::fatten(state.world, retrieve(state, parent)); + + text::add_line(state, contents, "factory_tier", text::variable_type::x, text::int_wholenum{ state.world.factory_type_get_factory_tier(content) }); + + // + auto sid = retrieve(state, parent); + auto n = state.world.state_ownership_get_nation(state.world.state_instance_get_state_ownership(sid)); + // + text::add_line(state, contents, "alice_factory_base_workforce", text::variable_type::x, state.world.factory_type_get_base_workforce(content)); + + // List factory type inputs + text::add_line(state, contents, "alice_factory_inputs"); + + auto s = retrieve(state, parent); + + auto const& iset = state.world.factory_type_get_inputs(content); + for(uint32_t i = 0; i < economy::commodity_set::set_size; i++) { + if(iset.commodity_type[i] && iset.commodity_amounts[i] > 0.0f) { + auto amount = iset.commodity_amounts[i]; + auto cid = iset.commodity_type[i]; + auto price = economy::price(state, s, cid); + + auto box = text::open_layout_box(contents, 0); + + // Commodity icon + std::string padding = cid.index() < 10 ? "0" : ""; + std::string description = "@$" + padding + std::to_string(cid.index()); + text::add_unparsed_text_to_layout_box(state, contents, box, description); + // Text + text::substitution_map m; + text::add_to_substitution_map(m, text::variable_type::name, state.world.commodity_get_name(cid)); + text::add_to_substitution_map(m, text::variable_type::val, text::fp_currency{ price }); + text::add_to_substitution_map(m, text::variable_type::need, text::fp_four_places{ amount }); + text::add_to_substitution_map(m, text::variable_type::cost, text::fp_currency{ price * amount }); + text::localised_format_box(state, contents, box, "alice_factory_input_item", m); + text::close_layout_box(contents, box); + } + } + + text::add_line_break_to_layout(state, contents); + + // List factory type construction costs + text::add_line(state, contents, "alice_construction_cost"); + auto const& cset = state.world.factory_type_get_construction_costs(content); + for(uint32_t i = 0; i < economy::commodity_set::set_size; i++) { + if(cset.commodity_type[i] && cset.commodity_amounts[i] > 0.0f) { + auto amount = cset.commodity_amounts[i]; + auto cid = cset.commodity_type[i]; + auto price = economy::price(state, s, cid); + + // Commodity icon + auto box = text::open_layout_box(contents, 0); + std::string padding = cid.index() < 10 ? "0" : ""; + std::string description = "@$" + padding + std::to_string(cid.index()); + text::add_unparsed_text_to_layout_box(state, contents, box, description); + + text::substitution_map m; + text::add_to_substitution_map(m, text::variable_type::name, state.world.commodity_get_name(cid)); + text::add_to_substitution_map(m, text::variable_type::val, text::fp_currency{ price }); + text::add_to_substitution_map(m, text::variable_type::need, text::fp_four_places{ amount }); + text::add_to_substitution_map(m, text::variable_type::cost, text::fp_currency{ price * amount }); + text::localised_format_box(state, contents, box, "alice_factory_input_item", m); + text::close_layout_box(contents, box); + } + } + /*text::add_line_break_to_layout(state, contents); + // These bonuses are not applied in PA + float sum = 0.f; + if(auto b1 = state.world.factory_type_get_bonus_1_trigger(content); b1) { + text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_1_amount(content) }); + if(trigger::evaluate(state, b1, trigger::to_generic(sid), trigger::to_generic(n), 0)) { + sum -= state.world.factory_type_get_bonus_1_amount(content); + } + ui::trigger_description(state, contents, b1, trigger::to_generic(sid), trigger::to_generic(n), 0); + } + if(auto b2 = state.world.factory_type_get_bonus_2_trigger(content); b2) { + text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_2_amount(content) }); + if(trigger::evaluate(state, b2, trigger::to_generic(sid), trigger::to_generic(n), 0)) { + sum -= state.world.factory_type_get_bonus_2_amount(content); + } + ui::trigger_description(state, contents, b2, trigger::to_generic(sid), trigger::to_generic(n), 0); + } + if(auto b3 = state.world.factory_type_get_bonus_3_trigger(content); b3) { + text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_3_amount(content) }); + if(trigger::evaluate(state, b3, trigger::to_generic(sid), trigger::to_generic(n), 0)) { + sum -= state.world.factory_type_get_bonus_3_amount(content); + } + ui::trigger_description(state, contents, b3, trigger::to_generic(sid), trigger::to_generic(n), 0); + } + text::add_line(state, contents, "alice_factory_total_bonus", text::variable_type::x, text::fp_four_places{ sum });*/ + + text::add_line_break_to_layout(state, contents); + + text::add_line(state, contents, "alice_building_conditions"); + + // US53AC10 Tooltip for a factory type displays if the factory type has been activated with a technology + + text::add_line_with_condition(state, contents, "nation_is_factory_type_active", state.world.nation_get_active_building(n, content) || state.world.factory_type_get_is_available_from_start(content), 15); + + auto p = state.world.state_instance_get_capital(sid); + + // US53AC11 Tooltip for a factory type displays if the target state is a colony and target factory type can be built in colonies + + if(state.world.province_get_is_colonial(p)) { + text::add_line_with_condition(state, contents, "nation_is_factory_type_colonies", state.world.factory_type_get_can_be_built_in_colonies(content), 15); + } + + // US53AC12 Tooltip for a factory type displays if the factory type requires potentials and target state doesn't have required potentials + + /* If mod uses Factory Province limits */ + auto output = state.world.factory_type_get_output(content); + if(state.world.commodity_get_uses_potentials(output)) { + auto limit = economy::calculate_province_factory_limit(state, p, output); + + text::add_line_with_condition(state, contents, "factory_build_condition_11", 1 <= limit, 15); + } + + auto const tax_eff = economy::tax_collection_rate(state, n, p); + + auto mid = state.world.state_instance_get_market_from_local_market(sid); + auto market_demand_satisfaction = state.world.market_get_expected_probability_to_sell(mid, output); + + auto wage = state.world.province_get_labor_price(p, economy::labor::basic_education) * 2.f; + auto const rich_effect = (1.0f - tax_eff * float(state.world.nation_get_rich_tax(n)) / 100.0f); + + float cost = economy::factory_type_build_cost(state, n, p, content, false) + 0.1f; + float output_value = economy::factory_type_output_cost(state, n, mid, content) * rich_effect; + float input = economy::factory_type_input_cost(state, n, mid, content) + 0.1f; + float profitability = (output_value - input - wage * content.get_base_workforce()) / input; + float payback_time = cost / std::max(0.00001f, (output_value - input - wage * content.get_base_workforce())); + + // US53AC13 Tooltip for a factory type displays key economic metrics of the potential construction: construction cost, input costs, output prices, profitability, payback type + + text::add_line(state, contents, "construction_cost", text::variable_type::value, text::fp_currency{ cost }); + text::add_line(state, contents, "input_value", text::variable_type::value, text::fp_currency{ input }); + text::add_line(state, contents, "output_value", text::variable_type::value, text::fp_currency{ output_value }); + text::add_line(state, contents, "profitability", text::variable_type::value, text::fp_percentage_one_place{ profitability }); + text::add_line(state, contents, "payback_time", text::variable_type::value, text::fp_two_places{ payback_time }); + + // Some extra outputs for AI debugging + + text::add_line_break_to_layout(state, contents); + text::add_line(state, contents, "alice_factory_type_score"); + + // US53AC14 Tooltip for a factory type displays the score AI places to the factory type + + auto factory_type_score = ai::evaluate_factory_type(state, n, mid, p, content, false, 0.3f, 0.5f, 200.f, rich_effect); + + for(auto line : factory_type_score.getSteps()) { + if(line.operation == sys::Operation::ADD || line.operation == sys::Operation::SUBTRACT) { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); + } else if(line.operation == sys::Operation::DIVIDE) { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ 1 / line.value }, 15); + } else { + text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); + } + } + + text::add_line(state, contents, "total_score", text::variable_type::value, text::fp_one_place{ factory_type_score.getResult() }); + + text::add_line_with_condition(state, contents, "province_has_workers", ai::province_has_workers(state, p)); + text::add_line_with_condition(state, contents, "province_has_available_workers", ai::province_has_available_workers(state, p)); + } + +} diff --git a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp index 2bcf68677..602bb0e63 100644 --- a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp +++ b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp @@ -182,180 +182,7 @@ class factory_build_item_button : public tinted_button_element_base { tooltip_behavior has_tooltip(sys::state& state) noexcept override { return tooltip_behavior::variable_tooltip; } - void update_tooltip(sys::state& state, int32_t x, int32_t y, text::columnar_layout& contents) noexcept override { - if(retrieve(state, parent)) { - text::add_line(state, contents, "alice_recommended_build"); - } - - auto content = dcon::fatten(state.world, retrieve(state, parent)); - - text::add_line(state, contents, "factory_tier", text::variable_type::x, text::int_wholenum{ state.world.factory_type_get_factory_tier(content) }); - - // - auto sid = retrieve(state, parent); - auto n = state.world.state_ownership_get_nation(state.world.state_instance_get_state_ownership(sid)); - // - text::add_line(state, contents, "alice_factory_base_workforce", text::variable_type::x, state.world.factory_type_get_base_workforce(content)); - - // List factory type inputs - text::add_line(state, contents, "alice_factory_inputs"); - - auto s = retrieve(state, parent); - - auto const& iset = state.world.factory_type_get_inputs(content); - for(uint32_t i = 0; i < economy::commodity_set::set_size; i++) { - if(iset.commodity_type[i] && iset.commodity_amounts[i] > 0.0f) { - auto amount = iset.commodity_amounts[i]; - auto cid = iset.commodity_type[i]; - auto price = economy::price(state, s, cid); - - auto box = text::open_layout_box(contents, 0); - - // Commodity icon - std::string padding = cid.index() < 10 ? "0" : ""; - std::string description = "@$" + padding + std::to_string(cid.index()); - text::add_unparsed_text_to_layout_box(state, contents, box, description); - // Text - text::substitution_map m; - text::add_to_substitution_map(m, text::variable_type::name, state.world.commodity_get_name(cid)); - text::add_to_substitution_map(m, text::variable_type::val, text::fp_currency{ price }); - text::add_to_substitution_map(m, text::variable_type::need, text::fp_four_places{ amount }); - text::add_to_substitution_map(m, text::variable_type::cost, text::fp_currency{ price * amount }); - text::localised_format_box(state, contents, box, "alice_factory_input_item", m); - text::close_layout_box(contents, box); - } - } - - text::add_line_break_to_layout(state, contents); - - // List factory type construction costs - text::add_line(state, contents, "alice_construction_cost"); - auto const& cset = state.world.factory_type_get_construction_costs(content); - for(uint32_t i = 0; i < economy::commodity_set::set_size; i++) { - if(cset.commodity_type[i] && cset.commodity_amounts[i] > 0.0f) { - auto amount = cset.commodity_amounts[i]; - auto cid = cset.commodity_type[i]; - auto price = economy::price(state, s, cid); - - // Commodity icon - auto box = text::open_layout_box(contents, 0); - std::string padding = cid.index() < 10 ? "0" : ""; - std::string description = "@$" + padding + std::to_string(cid.index()); - text::add_unparsed_text_to_layout_box(state, contents, box, description); - - text::substitution_map m; - text::add_to_substitution_map(m, text::variable_type::name, state.world.commodity_get_name(cid)); - text::add_to_substitution_map(m, text::variable_type::val, text::fp_currency{ price }); - text::add_to_substitution_map(m, text::variable_type::need, text::fp_four_places{ amount }); - text::add_to_substitution_map(m, text::variable_type::cost, text::fp_currency{ price * amount }); - text::localised_format_box(state, contents, box, "alice_factory_input_item", m); - text::close_layout_box(contents, box); - } - } - /*text::add_line_break_to_layout(state, contents); - // These bonuses are not applied in PA - float sum = 0.f; - if(auto b1 = state.world.factory_type_get_bonus_1_trigger(content); b1) { - text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_1_amount(content) }); - if(trigger::evaluate(state, b1, trigger::to_generic(sid), trigger::to_generic(n), 0)) { - sum -= state.world.factory_type_get_bonus_1_amount(content); - } - ui::trigger_description(state, contents, b1, trigger::to_generic(sid), trigger::to_generic(n), 0); - } - if(auto b2 = state.world.factory_type_get_bonus_2_trigger(content); b2) { - text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_2_amount(content) }); - if(trigger::evaluate(state, b2, trigger::to_generic(sid), trigger::to_generic(n), 0)) { - sum -= state.world.factory_type_get_bonus_2_amount(content); - } - ui::trigger_description(state, contents, b2, trigger::to_generic(sid), trigger::to_generic(n), 0); - } - if(auto b3 = state.world.factory_type_get_bonus_3_trigger(content); b3) { - text::add_line(state, contents, "alice_factory_bonus", text::variable_type::x, text::fp_four_places{ state.world.factory_type_get_bonus_3_amount(content) }); - if(trigger::evaluate(state, b3, trigger::to_generic(sid), trigger::to_generic(n), 0)) { - sum -= state.world.factory_type_get_bonus_3_amount(content); - } - ui::trigger_description(state, contents, b3, trigger::to_generic(sid), trigger::to_generic(n), 0); - } - text::add_line(state, contents, "alice_factory_total_bonus", text::variable_type::x, text::fp_four_places{ sum });*/ - - text::add_line_break_to_layout(state, contents); - - text::add_line(state, contents, "alice_building_conditions"); - - // US53AC10 Tooltip for a factory type displays if the factory type has been activated with a technology - - text::add_line_with_condition(state, contents, "nation_is_factory_type_active", state.world.nation_get_active_building(n, content) || state.world.factory_type_get_is_available_from_start(content), 15); - - auto p = state.world.state_instance_get_capital(sid); - - // US53AC11 Tooltip for a factory type displays if the target state is a colony and target factory type can be built in colonies - - if(state.world.province_get_is_colonial(p)) { - text::add_line_with_condition(state, contents, "nation_is_factory_type_colonies", state.world.factory_type_get_can_be_built_in_colonies(content), 15); - } - - // US53AC12 Tooltip for a factory type displays if the factory type requires potentials and target state doesn't have required potentials - - /* If mod uses Factory Province limits */ - auto output = state.world.factory_type_get_output(content); - if(state.world.commodity_get_uses_potentials(output)) { - auto limit = economy::calculate_province_factory_limit(state, p, output); - - text::add_line_with_condition(state, contents, "factory_build_condition_11", 1 <= limit, 15); - } - - auto const tax_eff = economy::tax_collection_rate(state, n, p); - - auto mid = state.world.state_instance_get_market_from_local_market(sid); - auto market_demand_satisfaction = state.world.market_get_expected_probability_to_sell(mid, output); - - auto wage = state.world.province_get_labor_price(p, economy::labor::basic_education) * 2.f; - auto const rich_effect = (1.0f - tax_eff * float(state.world.nation_get_rich_tax(n)) / 100.0f); - - float cost = economy::factory_type_build_cost(state, n, p, content, false) + 0.1f; - float output_value = economy::factory_type_output_cost(state, n, mid, content) * rich_effect; - float input = economy::factory_type_input_cost(state, n, mid, content) + 0.1f; - float profitability = (output_value - input - wage * content.get_base_workforce()) / input; - float payback_time = cost / std::max(0.00001f, (output_value - input - wage * content.get_base_workforce())); - - // US53AC13 Tooltip for a factory type displays key economic metrics of the potential construction: construction cost, input costs, output prices, profitability, payback type - - text::add_line(state, contents, "construction_cost", text::variable_type::value, text::fp_currency{ cost }); - text::add_line(state, contents, "input_value", text::variable_type::value, text::fp_currency{ input }); - text::add_line(state, contents, "output_value", text::variable_type::value, text::fp_currency{ output_value }); - text::add_line(state, contents, "profitability", text::variable_type::value, text::fp_percentage_one_place{ profitability }); - text::add_line(state, contents, "payback_time", text::variable_type::value, text::fp_two_places{ payback_time }); - - // Some extra outputs for AI debugging - - text::add_line_break_to_layout(state, contents); - text::add_line(state, contents, "alice_factory_type_score"); - - // US53AC14 Tooltip for a factory type displays the score AI places to the factory type - - auto factory_type_score = ai::evaluate_factory_type(state, n, mid, p, content, false, 0.3f, 0.5f, 200.f, rich_effect); - - for(auto line : factory_type_score.getSteps()) { - if(line.operation == sys::StackedCalculationWithExplanations::Operation::ADD || line.operation == sys::StackedCalculationWithExplanations::Operation::SUBTRACT) { - text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); - } - else if (line.operation == sys::StackedCalculationWithExplanations::Operation::DIVIDE) { - text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ 1 / line.value }, 15); - } - else { - text::add_line(state, contents, line.explanation, text::variable_type::value, text::fp_one_place{ line.value }, 15); - } - } - - text::add_line(state, contents, "total_score", text::variable_type::value, text::fp_one_place{ factory_type_score.getResult() }); - - text::add_line_with_condition(state, contents, "province_has_workers", ai::province_has_workers(state, p)); - - if(state.cheat_data.ui_debug_mode) { - text::add_line(state, contents, "alice_building_id", text::variable_type::val, content.id.value); - text::add_line(state, contents, "alice_province_id", text::variable_type::val, p.id.value); - } - } + void update_tooltip(sys::state& state, int32_t x, int32_t y, text::columnar_layout& contents) noexcept override; }; class factory_build_item : public listbox_row_element_base { diff --git a/src/launcher/launcher_main.hpp b/src/launcher/launcher_main.hpp index 87ee1b812..2eb10bd83 100644 --- a/src/launcher/launcher_main.hpp +++ b/src/launcher/launcher_main.hpp @@ -54,6 +54,8 @@ #include "gui_population_window.cpp" #include "gui_context_window.cpp" #include "gui_factory_refit_window.cpp" +#include "gui_build_factory_window.cpp" +#include "gui_scripted_elements.cpp" #include "province_tiles.cpp" #include "immediate_mode.cpp" #include "economy_viewer.cpp" diff --git a/src/main.cpp b/src/main.cpp index aebb1b682..f06ebb781 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -43,6 +43,7 @@ #include "events.cpp" #include "gui_graphics.cpp" #include "gui_common_elements.cpp" +#include "gui_build_factory_window.cpp" #include "widgets/table.cpp" #include "gui_trigger_tooltips.cpp" #include "gui_effect_tooltips.cpp" From d37fc20962c01d4806fa0472cf4f3a2b5a7cdd44 Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Sun, 15 Feb 2026 17:24:43 +0300 Subject: [PATCH 06/10] Fix build --- src/launcher/launcher_main.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/launcher/launcher_main.hpp b/src/launcher/launcher_main.hpp index 2eb10bd83..ca83ca424 100644 --- a/src/launcher/launcher_main.hpp +++ b/src/launcher/launcher_main.hpp @@ -55,7 +55,6 @@ #include "gui_context_window.cpp" #include "gui_factory_refit_window.cpp" #include "gui_build_factory_window.cpp" -#include "gui_scripted_elements.cpp" #include "province_tiles.cpp" #include "immediate_mode.cpp" #include "economy_viewer.cpp" From 5f4594ca0b26d7897b65b757a93824cef92fc7bf Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Mon, 16 Feb 2026 13:16:49 +0300 Subject: [PATCH 07/10] Compile-time templated operations in stacked_calculation --- src/common_types/stackedcalculation.hpp | 34 +++++++++---------- .../gui_build_factory_window.hpp | 1 + 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/common_types/stackedcalculation.hpp b/src/common_types/stackedcalculation.hpp index ceb0daf67..cb084810f 100644 --- a/src/common_types/stackedcalculation.hpp +++ b/src/common_types/stackedcalculation.hpp @@ -47,6 +47,20 @@ namespace sys { StepNode(float v, std::string_view exp, Next n) : value(v), explanation(exp), next(std::move(n)) { } + + // Apply the operation to the current value – fully compile‑time resolved. + float apply(float current) const { + if constexpr(Op == Operation::ADD) { + return current + value; + } else if constexpr(Op == Operation::SUBTRACT) { + return current - value; + } else if constexpr(Op == Operation::MULTIPLY) { + return current * value; + } else if constexpr(Op == Operation::DIVIDE) { + if(value == 0.0f) throw std::runtime_error("Division by zero"); + return current / value; + } + } }; // US49 stacked_calculation class allows uniting number calculations for backend with explanation tooltips for the UI @@ -56,19 +70,6 @@ namespace sys { float initialValue; Steps steps; - // Apply a single operation to a value - static float apply(Operation op, float current, float val) { - switch(op) { - case Operation::ADD: return current + val; - case Operation::SUBTRACT: return current - val; - case Operation::MULTIPLY: return current * val; - case Operation::DIVIDE: - if(val == 0.0f) throw std::runtime_error("Division by zero"); - return current / val; - } - return current; // unreachable - } - // Evaluation helpers static float evaluate_impl(const NoStep&, float current) { return current; @@ -76,7 +77,7 @@ namespace sys { template static float evaluate_impl(const StepNode& step, float current) { - float newCurrent = apply(Op, current, step.value); + float newCurrent = step.apply(current); // compile‑time operation return evaluate_impl(step.next, newCurrent); } @@ -129,7 +130,7 @@ namespace sys { float getResult() const { return evaluate_impl(steps, initialValue); } - // US49AC6 User can get all steps used for calculation for tooltipsand UI + // US49AC6 User can get all steps used for calculation for tooltips and UI std::vector getSteps() const { std::vector result; collect_steps(steps, result); @@ -140,11 +141,10 @@ namespace sys { auto clear() const { return stacked_calculation(initialValue); } - // US49AC8 User can reuse existing stack by reseting it to a different initial value + // US49AC8 User can reuse existing stack by resetting it to a different initial value auto reset(float newInitialValue) const { return stacked_calculation(newInitialValue); } }; - } diff --git a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp index 602bb0e63..39bc1f25f 100644 --- a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp +++ b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp @@ -9,6 +9,7 @@ #include "economy_production.hpp" #include "economy_factory_view.hpp" #include "economy.hpp" +#include "commands.hpp" // US53 Build Factory Window From 34ca63acbc05c8cef616e64198c827f7c9493ad7 Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 24 Feb 2026 13:23:53 +0300 Subject: [PATCH 08/10] assert for division by zero --- src/common_types/stackedcalculation.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common_types/stackedcalculation.hpp b/src/common_types/stackedcalculation.hpp index cb084810f..78072a054 100644 --- a/src/common_types/stackedcalculation.hpp +++ b/src/common_types/stackedcalculation.hpp @@ -57,7 +57,7 @@ namespace sys { } else if constexpr(Op == Operation::MULTIPLY) { return current * value; } else if constexpr(Op == Operation::DIVIDE) { - if(value == 0.0f) throw std::runtime_error("Division by zero"); + assert(value != 0.f); // Division by zero return current / value; } } From 359e1108d95c345ef3e9ab27faf1b8783e40b06e Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Tue, 24 Feb 2026 13:30:30 +0300 Subject: [PATCH 09/10] User Story docs clean up --- docs/features/UserStoriesGenerated.md | 126 ++++++++++++-------------- docs/features/generate_usdocs.py | 124 +++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 docs/features/generate_usdocs.py diff --git a/docs/features/UserStoriesGenerated.md b/docs/features/UserStoriesGenerated.md index e792b2e5a..b21660327 100644 --- a/docs/features/UserStoriesGenerated.md +++ b/docs/features/UserStoriesGenerated.md @@ -1,5 +1,5 @@ # User Stories -*Automatically generated file on 2026-02-10* +*Automatically generated file on 2026-02-24* ## US1. Regiment construction @@ -12,51 +12,42 @@ ## US3. Trade -| AC9 | Wartime embargoes | -| AC10 | diplomatic embargos | -| AC11 | sphere joins embargo | -| AC15 | Equal | -| AC17 | if market capital controller is at war with market coastal controller is different | | AC2 | register trade demand on transportation labor | | AC3 | register demand on local transportation | | AC7 | US3AC8 Ban international sea routes or international land routes based on the corresponding modifiers | | AC8 | Ban international sea routes or international land routes based on the corresponding modifiers | +| AC9 | Wartime embargoes | +| AC10 | diplomatic embargos | +| AC11 | sphere joins embargo | | AC12 | subject joins embargo | +| AC15 | Equal | +| AC17 | if market capital controller is at war with market coastal controller is different | | AC21 | effect of scale | -| AC5 | Trade Route Attraction from naval bases | -| AC22 | Trade Route attraction modifier | -## US4. War and peace +## US4. War ## US5. Warscore -| AC4 | What | -| AC5 | count 50 | | AC1 | Army arrives to province | | AC2 | Army siege | +| AC4 | What % of the province score should be counted towards occupation. [0.0f | +| AC5 | count 50% of occupation score for wars declared after targetted war | -## US6. Ticking warscore +## US6. Special Army Orders | AC3 | Reset selected army orders | -| AC20 | Reduce accumulated ticking warscore when the side adds a new wargoal | -| AC2 | US6AC3 | -| AC12 | US6AC13 US6AC17 US6AC18 US6AC19 Ticking warscore may go into negative only after grace period ends | -| AC5 | US6AC6 Ticking warscope for make_puppet war | -| AC7 | We hold some non | -| AC8 | Battle score | -| AC10 | US6AC17 US6AC18 US6AC19 | ## US7. Move and Siege army order -| AC2 | Enable | | AC1 | Handle | +| AC2 | Enable | ## US8. Strategic Redeployment army order -| AC3 | Toggle strategic redeployment order on B. | -| AC2 | Button to order Strategic Redeployment | | AC1 | Movement finished | +| AC2 | Button to order Strategic Redeployment | +| AC3 | Toggle strategic redeployment order on B. | ## US9. Pursue and engage army order @@ -73,17 +64,17 @@ ## US13. Regiments organization regain | AC3 | US13AC4 US13AC5 Morale | +| AC6 | Max organization of the regiment is 100% | | AC7 | Unfulfilled supply doesn't lower max org as it makes half the game unplayable | | AC8 | Unfilfilled supply doesn't prevent org regain as it makes half the game unplayable | -| AC6 | Max organization of the regiment is 100 | ## US14. Calculates reinforcement for a particular regiment -## US15. Host settings +## US15. Navy supplies -## US16. Player accounts +## US16. Ship supplies ## US17. Ships organization regain @@ -92,65 +83,60 @@ ## US18. Ships repairs -## US20. De facto annexations - - -## US27. Scriptable buttons - -| AC5 | National level interactions first | -| AC1 | US27AC2 US27AC3 US27AC4 | - -## US28. Toggleable windows - -| AC5 | US28AC6 UI variable toggle buttons can have nation | -| AC2 | When clicking button with toggle_ui_key the associated UI variable is toggled True | -| AC4 | Window is shown only when UI variable in `visible_ui_key` is set to True | - -## US29. Scriptable images with dynamic frames through datamodels - -| AC2 | US29AC3 When an icon has `datamodel | -| AC3 | US29AC3 When an icon has `datamodel | -| AC4 | US29AC5 Extracted into a separate function because both icon classes | -| AC6 | US29AC7 | - ## US31. Map | AC3 | If a valid province has been selected, reset selection of armies as well | -## US40. Winter - -| AC1 | Winter textures | -| AC2 | Winter siege attrition | - -## US49. StackedCalculationWithExplanations allows uniting number calculations for backend with explanation tooltips for the UI - -| AC1 | StackedCalculationWithExplanations is constructed with initial float value | +## US48. Economic Scene Commodities tab + +| AC0 | Display data only if a commodity is selected | +| AC1 | On national level, when price option is selected, display median price | +| AC2 | On national level, when supply option is selected, display total supply | +| AC3 | On national level, when demand option is selected, display total supply | +| AC4 | On national level, when a production option is selected, display total production | +| AC5 | On national level, when a consumption option is selected, display total consumption | +| AC6 | On national level, when a stockpiles option is selected, display total stockpiles | +| AC7 | On national level, when a potentials option is selected, display total potentials | +| AC8 | On national level, when a balance option is selected, display total balance with logarithmic scale | +| AC9 | On national level, when a trade_in option is selected, display total imports volume | +| AC10 | On national level, when a trade_out option is selected, display total exports volume | +| AC11 | On national level, when a trade_balance option is selected, display total trade_balance volume | +| AC12 | On market level, when a price option is selected, display price | +| AC13 | On market level, when a supply option is selected, display commodity supply | +| AC14 | On market level, when a demand option is selected, display commodity demand | +| AC15 | On market level, when a production option is selected, display commodity production | +| AC16 | On market level, when a consumption option is selected, display commodity consumption | +| AC17 | On market level, when a stockpiles option is selected, display commodity stockpiles | +| AC18 | On market level, display potentials option only for commodities that use resource potentials | +| AC19 | On market level, when a potentials option is selected, display commodity potentials | +| AC20 | On market level, when a balance option is selected, display commodity trade balance | +| AC21 | On market level, when a trade_in option is selected, display commodity trade_in volume | +| AC22 | On market level, when a trade_out option is selected, display commodity trade_out volume | +| AC23 | On market level, when a trade_balance option is selected, display commodity trade_balance volume | + +## US49. stacked_calculation class allows uniting number calculations for backend with explanation tooltips for the UI + +| AC1 | stacked_calculation is constructed with initial float value | | AC2 | User can add a value with a string_view explanation of the reason | | AC3 | User can subtract a value with a string_view explanation of the reason | -| AC4 | User can divide current value by provided value with a string_view explanation of the reason | +| AC4 | User can multiply current value by provided value with a string_view explanation of the reason | | AC5 | User can get resulting value from the stack | -| AC6 | User can get all steps used for calculation for tooltipsand UI | +| AC6 | User can get all steps used for calculation for tooltips and UI | | AC7 | User can reuse existing stack by clearing it | -| AC8 | User can reuse existing stack by reseting it to a different initial value | +| AC8 | User can reuse existing stack by resetting it to a different initial value | ## US50. The State constructs factories | AC1 | Each factory type is evaluated by its profitability, payback time, and a number of synergies | | AC2 | The State takes top 5 factory options for construction | -| AC4 | The State doesn't initiate more constructions if it doesn't have free funds | | AC3 | The State takes one random option for construction | +| AC4 | The State doesn't initiate more constructions if it doesn't have free funds | | AC5 | The State doesn't evaluate the cost of the factory it constructs beyond inital analysis | +| AC10 | This is legacy flow that is no longer called for State Constructions, refer to US52 | -## US51. Private investment fund builds factories +## US51. | AC6 | Private Investment takes top 5 factory options for contrustion | -| AC1 | Investment fund doesn't build if it's over courage with current investments | -| AC2 | Colonial pops are eligible for construction of certain factories | -| AC3 | Provinces are sorted by population descending | -| AC4 | If the province an existing construction, no new construction will be run | -| AC5 | If the state has over state.defines.factories_per_state factories, no new construction will be done | -| AC7 | Private Investment takes random option out of top 5 | -| AC8 | Private Investment doesn't get afraid of expensive constructions. It initiates one and stop the following ones | ## US52. Nation builds some random factories if it can build by itself @@ -165,7 +151,9 @@ | AC13 | Tooltip for a factory type displays key economic metrics of the potential construction | | AC14 | Tooltip for a factory type displays the score AI places to the factory type | -## US1010. When a factory is created, do market adjustments +## US101. Sieges and Occupations -| AC1 | When a factory is created, add 10 units of its output to local market to stimulate consumption and demand registration | -| AC2 | when a factory is created, increase price of its inputs to stimulate producing those | +| AC2 | Forts reduce siege speed by alice_fort_siege_slowdown factor | +| AC3 | Forts increase hostile siege attrition by state.defines.alice_fort_siege_attrition_per_level per level | +| AC4 | Calculate victory points that a province P is worth in the nation N. Used for warscore, occupation rate. | +| AC5 | defines the general algorithm for getting the effective fort level with said amount of total strength of units who are enemies with the fort controller, | diff --git a/docs/features/generate_usdocs.py b/docs/features/generate_usdocs.py new file mode 100644 index 000000000..699405712 --- /dev/null +++ b/docs/features/generate_usdocs.py @@ -0,0 +1,124 @@ +import asyncio +import os +from datetime import datetime +import re + +# This script parses all source files of the game and generates a single concise User Stories MD document from comments marks +# Use // US[id] to declare a User Story +# Use // US[id]AC[id] to declare an Acceptance Criteria + +# Run the file with CWD pointing to the Project Alice repository root +# From VS Code: With Project Folder opened, Right Mouse Button -> Run Python File in terminal or F5 for debug start + +cwd = os.getcwd() +outputfpath = os.path.join(cwd, "docs/features/UserStoriesGenerated.md") + +ignore_folders = [".git"] +ignorefiles = ["UserStoriesGenerated.md"] +allowed_extensions = ["txt", "py", "cs", "h", "hpp", "cpp", "c", "gd", "md"] + +data = {} + +def EMPTY_US(): + return { "name":"", "acceptance_criteria": {} } + +def add_us(usnumber, name): + usid = int(usnumber) + if usid in data: + data[usid]["name"] = name + else: + data[usid] = EMPTY_US() + data[usid]["name"] = name + +def add_ac(usnumber, acnumber, name): + usid = int(usnumber) + acid = int(acnumber) + if usid in data: + data[usid]["acceptance_criteria"][acid] = name + else: + data[usid] = EMPTY_US() + data[usid]["acceptance_criteria"][acid] = name + +def compile_data(): + res = "# User Stories\n" + res += "*Automatically generated file on " + datetime.today().strftime('%Y-%m-%d') + "*\n" + + uslist = sorted(data.keys()) + + for usid in uslist: + us = data[usid] + res += f"\n## US{usid}. {us["name"]}\n\n" + + aclist = sorted(us["acceptance_criteria"].keys()) + + for acid in aclist: + ac = us["acceptance_criteria"][acid] + res += f"| AC{acid} | {ac} |\n" + return res + +async def process_folder(path): + fileslist = os.listdir(path) + + async with asyncio.TaskGroup() as tg: + for k in fileslist: + if k in ignore_folders: + continue + if k in ignorefiles: + continue + if (os.path.isfile(os.path.join(path, k))): + extstr = k.split(".")[-1] + if extstr not in allowed_extensions: + continue + try: + process_file(path, k) + except Exception as e: + print(e) + else: + newpath = os.path.join(path, k) + tg.create_task(process_folder(newpath)) + +def process_file(folderpath, fname): + fpath = os.path.join(folderpath, fname) + + print("Processing file" + fpath) + + f = open(fpath) + + while True: + line = f.readline() + if not line: + break + + usmatch = re.search("(//|;|#)[ ]*US([0-9]+)[ .]([a-zA-z0-9.,' %]+)", line) + if usmatch: + print(line) + commenttype = usmatch.group(1) + usnumber = usmatch.group(2) + usname = usmatch.group(3) + + print(commenttype, usnumber, usname) + add_us(usnumber, usname) + + acmatch = re.search("(//|;|#)[ ]*US([0-9]+)AC([0-9]+)[ .]([a-zA-z0-9.,' %]+)", line) + if acmatch: + print(line) + commenttype = acmatch.group(1) + usnumber = acmatch.group(2) + acnumber = acmatch.group(3) + acname = acmatch.group(4) + + print(commenttype, usnumber, acnumber, acname) + add_ac(usnumber, acnumber, acname) + + f.close() + +start_time = datetime.now() + +r = asyncio.run(process_folder(cwd)) + +output = compile_data() +with open(outputfpath, "w", encoding="utf-8") as f: + f.write(output) + +time_elapsed = datetime.now() - start_time +print('Extras Time elapsed (hh:mm:ss.ms) {}'.format(time_elapsed)) From a546b5133a45f848f86b2bb2b7a2831e233c638f Mon Sep 17 00:00:00 2001 From: Paul Nakonechnyy Date: Sat, 28 Feb 2026 21:18:56 +0300 Subject: [PATCH 10/10] Fix incremental build --- .../production_subwindows/gui_build_factory_window.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp index 39bc1f25f..e4b0e7faa 100644 --- a/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp +++ b/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.hpp @@ -1,5 +1,8 @@ #pragma once +#include "system_state.hpp" +#include "gui_listbox_templates.hpp" +#include "gui_common_elements.hpp" #include "gui_element_types.hpp" #include "gui_production_enum.hpp" #include "ai_economy.hpp"