diff --git a/CMakeLists.txt b/CMakeLists.txt index fdde09bf3..421dbaf42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,7 @@ list(APPEND ALICE_INCREMENTAL_SOURCES_LIST "${PROJECT_SOURCE_DIR}/src/gui/topbar_subwindows/gui_population_window.cpp" "${PROJECT_SOURCE_DIR}/src/gui/topbar_subwindows/gui_production_window.cpp" "${PROJECT_SOURCE_DIR}/src/gui/topbar_subwindows/gui_technology_window.cpp" + "${PROJECT_SOURCE_DIR}/src/gui/topbar_subwindows/production_subwindows/gui_build_factory_window.cpp" "${PROJECT_SOURCE_DIR}/src/gui/immediate_mode.cpp" "${PROJECT_SOURCE_DIR}/src/gui/economy_viewer.cpp" "${PROJECT_SOURCE_DIR}/src/gui/unit_tooltip.cpp" diff --git a/assets/localisation/en-US/alice.csv b/assets/localisation/en-US/alice.csv index 1418d3c78..45acdbc66 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,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_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/docs/features/UserStoriesGenerated.md b/docs/features/UserStoriesGenerated.md new file mode 100644 index 000000000..b21660327 --- /dev/null +++ b/docs/features/UserStoriesGenerated.md @@ -0,0 +1,159 @@ +# User Stories +*Automatically generated file on 2026-02-24* + +## 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 + +| 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 | + +## US4. War + + +## US5. Warscore + +| 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. Special Army Orders + +| AC3 | Reset selected army orders | + +## US7. Move and Siege army order + +| AC1 | Handle | +| AC2 | Enable | + +## US8. Strategic Redeployment army order + +| AC1 | Movement finished | +| AC2 | Button to order Strategic Redeployment | +| AC3 | Toggle strategic redeployment order on B. | + +## 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 | +| 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 | + +## US14. Calculates reinforcement for a particular regiment + + +## US15. Navy supplies + + +## US16. Ship supplies + + +## US17. Ships organization regain + + +## US18. Ships repairs + + +## US31. Map + +| AC3 | If a valid province has been selected, reset selection of armies as well | + +## 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 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 tooltips and UI | +| AC7 | User can reuse existing stack by clearing it | +| 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 | +| 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. + +| AC6 | Private Investment takes top 5 factory options for contrustion | + +## 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 | + +## US101. Sieges and Occupations + +| 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)) diff --git a/src/ai/ai_economy.cpp b/src/ai/ai_economy.cpp index 783fcefe1..59fddd79c 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,121 @@ void update_factory_types_priority(sys::state& state) { }); } +// US50AC1 Each factory type is evaluated by its profitability, payback time, and a number of synergies +// 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, + 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; + + 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::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) { + 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) { + // Control is usually <100% so this increases the score + score3 = score2.divide(control, "private_investors_avoid_high_taxes"); + } + + // 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) { + 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(); + + 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) { + factory_output_consumed_by_other_factory *= 1.5f; + } + } + + 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()) { + 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) { + factory_consumes_local_potential_resource *= 1.5f; + } + + if(state.world.province_get_rgo(pid) == cid) { + factory_consumes_local_rgo_resource *= 1.5f; + } + } + + 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, @@ -133,6 +251,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 +269,26 @@ 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_probability_to_buy, filter_payback_time, effective_profit); + + // 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 > b.score; + }); + // US50AC2 The State takes top 5 factory options for construction + // US51AC6 Private Investment takes top 5 factory options for contrustion + // 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); + } } +//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 +414,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 +431,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 +441,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 +547,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 +605,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..c0927c19c 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,32 @@ 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); + +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, + 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..78072a054 --- /dev/null +++ b/src/common_types/stackedcalculation.hpp @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sys { + + // 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)) { } + + // 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) { + assert(value != 0.f); // Division by zero + return current / value; + } + } + }; + + // US49 stacked_calculation class allows uniting number calculations for backend with explanation tooltips for the UI + template + class stacked_calculation { + private: + float initialValue; + Steps steps; + + // Evaluation helpers + static float evaluate_impl(const NoStep&, float current) { + return current; + } + + template + static float evaluate_impl(const StepNode& step, float current) { + float newCurrent = step.apply(current); // compile‑time operation + return evaluate_impl(step.next, newCurrent); + } + + // 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 + auto multiply(float value, std::string_view explanation) const { + auto newSteps = StepNode(value, explanation, steps); + return stacked_calculation( + initialValue, std::move(newSteps)); + } + // 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 { + return evaluate_impl(steps, initialValue); + } + // US49AC6 User can get all steps used for calculation for tooltips and UI + std::vector getSteps() const { + std::vector result; + collect_steps(steps, result); + return result; + } + + // US49AC7 User can reuse existing stack by clearing it + auto clear() const { + return stacked_calculation(initialValue); + } + // 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/economy_viewer.cpp b/src/gui/economy_viewer.cpp index 52e3641dd..b623762ae 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 + // 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; } 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 9c8619838..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" @@ -9,6 +12,9 @@ #include "economy_production.hpp" #include "economy_factory_view.hpp" #include "economy.hpp" +#include "commands.hpp" + +// US53 Build Factory Window namespace ui { @@ -180,151 +186,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 });*/ - - /* 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(state, contents, "alice_building_conditions"); - - 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); - - 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); - } - - 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())); - - 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 }); - - // Some extra outputs for AI debugging - - text::add_line(state, contents, "alice_pop_show_details"); - - 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 d7ef4ba45..84a057fb1 100644 --- a/src/launcher/launcher_main.hpp +++ b/src/launcher/launcher_main.hpp @@ -55,6 +55,7 @@ #include "gui_population_window.cpp" #include "gui_context_window.cpp" #include "gui_factory_refit_window.cpp" +#include "gui_build_factory_window.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"