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 5999f61..2737ac2 100644 Binary files a/dist/wolfy_cost_based_army_caps.pack and b/dist/wolfy_cost_based_army_caps.pack differ 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)