From 3cf38a1518f3602598eddb11855f87232124ce9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Sol=C3=A9?= Date: Thu, 4 Aug 2022 00:07:34 +0200 Subject: [PATCH] Update 1.2.0 (#3) - Introduced enforcement of AI cost limits via a new automatic method - This new algorithm does not require manual maintenance - It supports all units and races by default, even the mod ones - It is possible it is CPU heavy / makes AI turns longer - Requires testing - Fixed a bug that caused the Tooltip text to be hidden if Dynamic Costs were disabled - Added a new Tooltip text for Dynamic Costs if the capacity has reached its maximum amount - Technical changes: - New method `is_faction_punishable(faction)` added to the CBAC lib API - New method `is_hero(unit_key)` added to the CBAC lib API --- CHANGELOG.md | 13 ++ README.md | 17 +-- dist/wolfy_cost_based_army_caps.pack | Bin 30145 -> 34635 bytes src/script/_lib/mod/cbac.lua | 19 ++- src/script/campaign/mod/cbac-ai.lua | 152 +++++++++++++++++++++++ src/script/campaign/mod/cbac-sl.lua | 4 +- src/script/campaign/mod/cbac-tooltip.lua | 3 + 7 files changed, 193 insertions(+), 15 deletions(-) create mode 100644 src/script/campaign/mod/cbac-ai.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index e1abcb3..4b777e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,3 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved tooltip text code into a new script - Moved supply lines code into a new script - New method `is_army_punishable(military_force)` added to the CBAC lib API + +## [1.2.0] Minor Update - 04.08.2022 + +- Introduced enforcement of AI cost limits via a new automatic method + - This new algorithm does not require manual maintenance + - It supports all units and races by default, even the mod ones + - It is possible it is CPU heavy / makes AI turns longer + - Requires testing +- Fixed a bug that caused the Tooltip text to be hidden if Dynamic Costs were disabled +- Added a new Tooltip text for Dynamic Costs if the capacity has reached its maximum amount +- Technical changes: + - New method `is_faction_punishable(faction)` added to the CBAC lib API + - New method `is_hero(unit_key)` added to the CBAC lib API diff --git a/README.md b/README.md index 9c6769b..7e0ca90 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ Cost-based Army Caps mod for Total War: Warhammer III. ## Introduction -Approved port of Jadawin's [Cost-based Army Caps](https://steamcommunity.com/sharedfiles/filedetails/?id=1723390103) for +Approved port of Jadawin's [Cost-based Army Caps](https://steamcommunity.com/sharedfiles/filedetails/?id=1723390103) for Warhammer II. -This mod introduces an economic cap to the campaign armies in a very similar fashion than Multiplayer Battles do, hence -removing the viability of elite doomstacks and increasing that of basic units without being limited by types or tiers. +This mod introduces an economic cap to the campaign armies in a very similar fashion than Multiplayer Battles do, hence +removing the viability of elite doomstacks and increasing that of basic units without being limited by types or tiers. The change affects both the player and the AI, although by default the human limited will be smaller. -If the total point cost of the army exceeds the limit, it will cost 3x the normal upkeep and will be unable to replenish -until it is again under the cost limit. Worry not, limits are programmed to be dynamic by default, which means a higher +If the total point cost of the army exceeds the limit, it will cost 3x the normal upkeep and will be unable to replenish +until it is again under the cost limit. Worry not, limits are programmed to be dynamic by default, which means a higher level lord will be able to handle a more expensive army. All the values can be modified using MCT. ## Contributing @@ -35,7 +35,8 @@ Feel free to add issues or to create pull requests. Help is always welcome. ## Authors -* **Marc Solé Fonte** - *Initial work* - [msolefonte](https://github.com/msolefonte) +* **Jadawin** - *Initial work* - [jadawin](https://steamcommunity.com/profiles/76561198030772148) +* **Marc Solé Fonte** - *Conversion to Warhammer III and posterior work* - [msolefonte](https://github.com/msolefonte) ## License @@ -43,9 +44,9 @@ This project is licensed under the Apache License, Version 2.0 - see the [LICENS ## Acknowledgments -* Special thanks to **Jadawin** for his amazing work for Warhammer II and for being open about me porting the mod here. +* Special thanks to **Jadawin** for his amazing work for Warhammer II and for being open about me porting the mod here. Thank you for all your excellent mods! -* Special thanks to the **Da Modding Den** community for their knowledge and patience. This would have not not been +* Special thanks to the **Da Modding Den** community for their knowledge and patience. This would have not not been possible without them. I resist to give names because you are all amazing <3 * Special thanks to all the users that have supported the development economically. * Thanks to all the users that have participated in the beta releases, that have reported bugs and that have contributed diff --git a/dist/wolfy_cost_based_army_caps.pack b/dist/wolfy_cost_based_army_caps.pack index 5999f614c82d046e008e2b14a1275c94dea3246f..2737ac22543c634dacb6e50b4686ba9e503421cd 100644 GIT binary patch delta 5468 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 diff --git a/src/script/_lib/mod/cbac.lua b/src/script/_lib/mod/cbac.lua index 3983196..09f5927 100644 --- a/src/script/_lib/mod/cbac.lua +++ b/src/script/_lib/mod/cbac.lua @@ -11,9 +11,12 @@ local config = { upgrade_ai_armies = false, upgrade_grace_period = 20, auto_level_ai_lords = 3, - logging_enabled = false + logging_enabled = true }; local exceptions = { + free_factions = { + "wh2_dlc10_def_blood_voyage" + }, free_heroes = { "wh_dlc07_brt_cha_green_knight_0", "wh_dlc06_dwf_cha_master_engineer_ghost_0", @@ -48,8 +51,8 @@ local exceptions = { -- UTILS -- -function table.contains(table, element) - for _, value in pairs(table) do +function table.contains(tbl, element) + for _, value in pairs(tbl) do if value == element then return true; end @@ -118,7 +121,13 @@ function cbac:is_army_punishable(military_force) return true; end -function _is_hero(unit_key) +function cbac:is_faction_punisheable(faction) + return faction:name() ~= "rebels" and not faction:name():find("_intervention") + and not faction:name():find("_incursion") + and not table.contains(exceptions.free_factions, faction:name()); +end + +function cbac:is_hero(unit_key) return string.find(unit_key, "_cha_") or table.contains(exceptions.custom_heroes, unit_key); end @@ -141,7 +150,7 @@ function cbac:get_unit_cost(unit) end function cbac:get_hero_cost(unit) - if _is_hero(unit:unit_key()) and not _is_free_hero(unit:unit_key()) then + if cbac:is_hero(unit:unit_key()) and not _is_free_hero(unit:unit_key()) then return 1; end diff --git a/src/script/campaign/mod/cbac-ai.lua b/src/script/campaign/mod/cbac-ai.lua new file mode 100644 index 0000000..f8fdd2f --- /dev/null +++ b/src/script/campaign/mod/cbac-ai.lua @@ -0,0 +1,152 @@ +local cbac = core:get_static_object("cbac"); + +-- UTILS -- + +function shuffle(tbl) + for i = #tbl, 2, -1 do + local j = cm:random_number(i); + tbl[i], tbl[j] = tbl[j], tbl[i]; + end + return tbl; +end + +function get_table_keys(tbl) + cbac:log("get_table_keys " .. #tbl); + + local keys = {}; + for key, _ in pairs(tbl) do + table.insert(keys, key); + end + + return keys; +end + +-- AI -- + +local function generate_recruitment_pool(faction) + cbac:log("Generating recruitment pool"); + local recruitment_pool = {}; + + local characters = faction:character_list(); + for i = 0, characters:num_items() - 1 do + if cm:char_is_mobile_general_with_army(characters:item_at(i)) then + local unit_list = characters:item_at(i):military_force():unit_list(); + for i = 1, unit_list:num_items() - 1 do + local unit = unit_list:item_at(i); + local unit_cost = cbac:get_unit_cost(unit); + + if not cbac:is_hero(unit:unit_key()) and unit_cost > 0 then + recruitment_pool[unit:unit_key()] = unit_cost; + end + end + end + end + + cbac:log("Recruitment pool generated. Size: " .. #get_table_keys(recruitment_pool)); + return recruitment_pool; +end + +local function replace_unit(old_unit_key, new_unit_key, character_lookup) + cbac:log("Replacing " .. old_unit_key .. " with " .. new_unit_key); + cm:remove_unit_from_character(character_lookup, old_unit_key); + cm:grant_unit_to_character(character_lookup, new_unit_key); +end + +local function downgrade_unit_and_get_savings(unit_list, unit_index, recruitment_pool, character_lookup) + local unit = unit_list:item_at(unit_index); + local unit_cost = cbac:get_unit_cost(unit); + + if unit_cost > 0 then + cbac:log("Downgrading unit? " .. unit:unit_key()); + local recruitment_pool_keys = get_table_keys(recruitment_pool); + local offset = math.random(0, #recruitment_pool_keys - 1); + + for i = 0, #recruitment_pool_keys - 1 do + local rec_unit_key = recruitment_pool_keys[(i + offset) % #recruitment_pool_keys + 1]; + if recruitment_pool[rec_unit_key] < unit_cost then + replace_unit(unit:unit_key(), rec_unit_key, character_lookup); + cbac:log("Yay! Points saved: " .. unit_cost - recruitment_pool[rec_unit_key]); + return unit_cost - recruitment_pool[rec_unit_key]; + end + end + end + + cbac:log("Nay! No points saved"); + return 0; +end + +local function enforce_limit_on_ai_army(character, recruitment_pool) + local required_savings = cbac:get_army_cost(character) - cbac:get_army_limit(character); + local army_is_over_limit = true; + + local unit_list = character:military_force():unit_list(); + local unit_indices = {} + for i = 1, unit_list:num_items() - 1 do + table.insert(unit_indices, i); + end + + unit_indices = shuffle(unit_indices); + for i = 1, #unit_indices do + required_savings = required_savings - downgrade_unit_and_get_savings(unit_list, unit_indices[i], recruitment_pool, + cm:char_lookup_str(character)); + if required_savings <= 0 then + cbac:log("This army is now under the cost limit, moving on.") + army_is_over_limit = false; + break + end + end + + if army_is_over_limit then + cbac:log("Looped through all units in army, but it is still over limit. Will try again in the next turn!"); + end +end + +local function check_ai_force_army_limit(faction, character) + if cm:char_is_mobile_general_with_army(character) then + local army_cost = cbac:get_army_cost(character); + local army_limit = cbac:get_army_limit(character); + + local recruitment_pool = generate_recruitment_pool(faction); + + if army_cost > army_limit then + cbac:log("AI Army of faction " .. faction:name() .. " is over cost limit (" .. army_cost .. "/" .. army_limit .. + "). Limits will be enforced!"); + enforce_limit_on_ai_army(character, recruitment_pool); + end + end +end + +local function check_ai_faction_army_limit(faction) + cbac:log("Checking faction limit: " .. faction:name()); + if cbac:is_faction_punisheable(faction) then + cbac:log("Faction " .. faction:name() .. " is punisheable"); + local characters = faction:character_list(); + for i = 0, characters:num_items() - 1 do + check_ai_force_army_limit(faction, characters:item_at(i)); + end + end +end + +-- LISTENERS -- + +local function add_listeners() + core:add_listener( + "ArmyCostLimitsAI", + "FactionTurnStart", + function(context) + return not context:faction():is_human(); + end, + function(context) + check_ai_faction_army_limit(context:faction()); + end, + true + ); +end + +-- MAIN -- + +local function main() + add_listeners(); +end + +main(); diff --git a/src/script/campaign/mod/cbac-sl.lua b/src/script/campaign/mod/cbac-sl.lua index 27155ce..d1989c3 100644 --- a/src/script/campaign/mod/cbac-sl.lua +++ b/src/script/campaign/mod/cbac-sl.lua @@ -3,7 +3,7 @@ local cbac = core:get_static_object("cbac"); local function apply_supply_lines(faction) local total_supply_lines_factor = 0; local character_list = faction:character_list(); - for i=0, character_list:num_items() - 1 do + for i = 0, character_list:num_items() - 1 do local character = character_list:item_at(i); if cm:char_is_mobile_general_with_army(character) and not character:character_subtype("wh2_main_def_black_ark") then if cbac:is_army_punishable(character:military_force()) then @@ -21,7 +21,7 @@ local function apply_supply_lines(faction) -- Base penalty is +15% unit upkeep on VH and Legendary local base_supply_lines_penalty = 4; -- Modify it for easy difficulties - local combined_difficulty = cm:model():combined_difficulty_level() + local combined_difficulty = cm:model():combined_difficulty_level(); if combined_difficulty == -1 then -- Hard base_supply_lines_penalty = 3; elseif combined_difficulty == 0 then -- Normal diff --git a/src/script/campaign/mod/cbac-tooltip.lua b/src/script/campaign/mod/cbac-tooltip.lua index 28dc247..1ca7fc5 100644 --- a/src/script/campaign/mod/cbac-tooltip.lua +++ b/src/script/campaign/mod/cbac-tooltip.lua @@ -42,7 +42,10 @@ local function get_dynamic_cost_tt_text(character, army_limit) local next_level = math.floor((lord_rank + limit_rank) / limit_rank) * limit_rank; return "\n\nCapacity at Level " .. next_level .. ": " .. next_limit_increase + army_limit; end + return "\n\nAlready at maximum capacity!"; end + + return ""; end local function get_army_cost_tt_text(character, army_cost)