Skip to content

Commit

Permalink
Update 1.2.0 (#3)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
msolefonte authored Aug 3, 2022
1 parent 63e6fd2 commit 3cf38a1
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 15 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,17 +35,18 @@ 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

This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details.

## 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
Expand Down
Binary file modified dist/wolfy_cost_based_army_caps.pack
Binary file not shown.
19 changes: 14 additions & 5 deletions src/script/_lib/mod/cbac.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
152 changes: 152 additions & 0 deletions src/script/campaign/mod/cbac-ai.lua
Original file line number Diff line number Diff line change
@@ -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();
4 changes: 2 additions & 2 deletions src/script/campaign/mod/cbac-sl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/script/campaign/mod/cbac-tooltip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 3cf38a1

Please sign in to comment.