Tutorial:Pooled Resource Unit Costs: Difference between revisions

From Total War Modding
 
(42 intermediate revisions by 2 users not shown)
Line 1: Line 1:
=== Originally written by Pear ===
= Total War: Warhammer 3 Guide by Pear =
This guide and the [[:File:!units_cost_pooled_resource.pack|example mod]] are my submission for '''Mod Jam #7'''.
This guide and the [[:File:!units_cost_pooled_resource.pack|example mod]] are my submission for '''Mod Jam #7'''.


[https://tw-modding.com/wiki/Tutorial:Pooled_Resources_Updated Pooled Resources] are cool. The problem is their application is often limited. This tutorial aims to fix that by explaining how to use Pooled Resources as costs for units in the main recruitment panel and providing an example mod for reference. The method used here can be applied to a range of other things such as RoRs, lords, agents, buildings, technologies, or anything like that.
[https://tw-modding.com/wiki/Tutorial:Pooled_Resources_Updated Pooled Resources] are cool. The problem is their application is often limited. This tutorial aims to fix that by explaining how to use Pooled Resources as costs for units in the main recruitment panel and providing an [[:File:!units_cost_pooled_resource.pack|example mod]] for reference. The method used here can be applied to a range of other things such as RoRs, lords, agents, buildings, technologies, or anything like that.


There's currently nothing in the tables to make units cost Pooled Resources except for a few faction-specific things so we have to use Lua UI scripting instead. It works by checking when the player has clicked certain units, taking away the defined cost of those units, and creating some UI components to visualise all this. The process consists of two main parts:
There's currently nothing in the tables to make units cost Pooled Resources except for a few faction-specific things so we have to use Lua UI scripting instead. It works by checking when the player has clicked certain units, taking away the defined cost of those units, and creating some UI components (UICs) to visualise all this. The process consists of two main parts:


# [https://tw-modding.com/wiki/Tutorial:Pooled_Resource_Costs#%E2%9C%8B_Making_the_UI '''Making the UI''']: making UICs, enabling and disabling UICs, and setting text, tooltips, and icons.
# [https://tw-modding.com/wiki/Tutorial:Pooled_Resource_Costs#%F0%9F%8E%A8_Making_the_UI '''Making the UI''']: making UICs, enabling and disabling UICs, and setting text, tooltips, and icons.
# [https://tw-modding.com/wiki/Tutorial:Pooled_Resource_Costs#%F0%9F%8E%AE_Triggering_the_UI '''Triggering the UI''']: creating the UI at the right time, accounting for UI refreshes, and taking away / returning the Pooled Resource to the player in a multiplayer-friendly way.
# [https://tw-modding.com/wiki/Tutorial:Pooled_Resource_Costs#%F0%9F%8E%AE_Triggering_the_UI '''Triggering the UI''']: creating the UI at the right time, accounting for UI refreshes, and taking away / returning the Pooled Resource to the player in a multiplayer-friendly way.


Before we get started I'd recommend using my [[:File:!units_cost_pooled_resource.pack|example mod]] as a reference, as well as Groove Wizard's [https://github.com/chadvandy/tw_autogen Visual Studio development environment] for your scripting, and Groove's [https://steamcommunity.com/sharedfiles/filedetails/?id=2791799449 Modding Development Tools: Lua Console] to test the code in game (yes this man is a machine). This guide assumes some knowledge of RFPM and scripting.
Before we get started I'd recommend using my [[:File:!units_cost_pooled_resource.pack|example mod]] as a reference, as well as Groove Wizard's [https://github.com/chadvandy/tw_autogen Visual Studio development environment] for your scripting, and Groove's [https://steamcommunity.com/sharedfiles/filedetails/?id=2791799449 Modding Development Tools: Lua Console] to test the code in game (yes this man is a machine). This guide assumes some knowledge of RPFM and scripting.


== 🧑‍🎓 Some UI Basics ==
== 🧑‍🎓 Some UI Basics ==
Line 15: Line 15:


=== 🔍 Finding UI Components ===
=== 🔍 Finding UI Components ===
[[File:Recruitment cost component.png|right|thumb]Context Viewer Recruitment Cost UIC for Swordsmen]
[[File:Recruitment cost component.png|right|thumb|Recruitment Cost Component in the Context Viewer]]
In order to modify a part of the UI you need to find where it's located. To do this we can use the [https://tw-modding.com/wiki/Tutorial:Context_Viewer_(Warhammer_3) Context Viewer]. A quick way to do this is by having the Context Viewer open and clicking on the screen with you mouse wheel.
In order to modify a part of the UI you need to find where it's located. To do this we can use the [https://tw-modding.com/wiki/Tutorial:Context_Viewer_(Warhammer_3) Context Viewer]. A quick way to do this is by having the Context Viewer open and clicking on the screen with your mouse wheel.


For the purpose of this tutorial, try clicking the unit card you want to add a Pooled Resource to then expanding the unit card path in the Context Viewer until you find the "RecruitmentCost" UIC. Once you've found it, paste <code>CopyFullPathToClipboard()</code> in the expression tester box. This copies the path to that UIC.  
For the purpose of this tutorial, try clicking the unit card you want to add a Pooled Resource to then expanding the unit card path in the Context Viewer until you find the "RecruitmentCost" UIC. Once you've found it, paste <code>CopyFullPathToClipboard()</code> in the expression tester box. This copies the path to that UIC.  
Line 36: Line 36:


Using the paths we've previously established, the path for this new "prestige_cost" UIC we've created is therefore:
Using the paths we've previously established, the path for this new "prestige_cost" UIC we've created is therefore:
  <code>local prestige_cost_parent_uic = find_uicomponent(unit_uic, "unit_icon", "prestige_cost")</code>
  <code>local prestige_cost_parent_uic = UIComponent(recruitment_cost_uic:CopyComponent("prestige_cost"))</code>


=== ⬇️ Repositioning Components ===
=== ⬇️ Repositioning Components ===
Line 45: Line 45:
If you ''really'' want to make sure it's in the right place take a screenshot and do a bit of pixel peeping in a photo editor and count the pixels between e.g. the original recruitment cost and the upkeep cost UICs and that's how many pixels your new component should be below the recruitment cost UIC.  
If you ''really'' want to make sure it's in the right place take a screenshot and do a bit of pixel peeping in a photo editor and count the pixels between e.g. the original recruitment cost and the upkeep cost UICs and that's how many pixels your new component should be below the recruitment cost UIC.  


Using my example mod, we can see that the correct coordinates for the normal recruitment panel are:
Using my example mod, we can see that the correct coordinates for the new component in the normal recruitment panel are:
  <code>prestige_cost_parent_uic:SetDockOffset(8, 19)</code>
  <code>prestige_cost_parent_uic:SetDockOffset(8, 19)</code>
this may change depending on the faction or panel e.g. RoRs or lords.
this may change depending on the faction or panel e.g. RoRs or lords.
=== ↗️ Resizing Components ===
Resizing components may be necessary if you're adding UICs, especially resizing list clips which would otherwise cut off the new UICs you've made. To do that you need to use [https://chadvandy.github.io/tw_modding_resources/WH3/campaign/uicomponent.html#function:uicomponent:Resize Resize] like this:
<code>unit_uic:SetCanResizeHeight(true)
unit_uic:SetCanResizeWidth(true)
local width, height= unit_uic:Dimensions()
unit_uic:Resize(width, (height +  + 10), false)</code>


=== 🪟 Finding panels ===
=== 🪟 Finding panels ===
To find the name of the panel that you want to check is opened or closed using the <code>CampaignPanelOpened</code> and <code>CampaignPanelClosed</code> listeners you can check the log produced by Groove's Modding Dev Tools. In the log it will look something like this:
To find the name of the panel that you want to check is opened or closed you can check the log produced by Groove's Modding Dev Tools. In the log it will look something like this:


  <code>[ui] <97.6s>    Panel opened units_panel
  <code>[ui] <97.6s>    Panel opened units_panel
Line 57: Line 65:


== 🎨 Making the UI ==
== 🎨 Making the UI ==
This part of the guide requires a little knowledge of scripting as we'll be looking at two functions. In short, the two functions apply the UI knowledge we covered earlier (and a little extra) to create UICs, resize UICs, and set their docking points, text, and icons. Comments in the functions briefly explain what each part does so I won't go into too much detail here. If it does not make sense I'd suggest looking up the functions used in the documentation. For the purpose of the guide we will be keeping the vanilla treasury costs and making one new Pooled Resource cost which will go below the unit card.  
So let's start! First we'll be looking at two functions — <code>initialise_uics()</code> and <code>finalise_uics()</code> . In short, the two functions apply the UI knowledge we covered earlier (and a little extra) to create UICs, resize UICs, and set their docking points, text, and icons. The functions are seperated even though they do similar things because the first one has to run only when the panel is opened, whereas the second has to run every time the UI is refreshed while the panel is open. For the purpose of the guide we will be keeping the vanilla treasury costs and making one new Pooled Resource cost which will go below the unit card.  


'''IMPORTANT''': Make sure you change the '''listener and saved value names''' from the examples to avoid incompatibilities.
'''IMPORTANT''': Make sure you change the '''listener and saved value names''' from the examples to avoid incompatibilities.
Line 69: Line 77:
  }</code>
  }</code>


This table is then used in the functions <code>initialise_uics(recruitment_type)</code> and <code>finalise_uics()</code>.  
This table is then used in the functions <code>initialise_uics()</code> and <code>finalise_uics()</code>.  
 
The first function <code>initialise_uics()</code> handles resizing the parents for UICs to make sure the list clips are displaying our new components and everything else appears correctly. The first part of that runs in a loop, once for local and once for global recruitment if the UIC exists then it proceeds:
 
<code>local function initialise_uics()
    out("PR UNIT COST LOG - Initialising components.")
    local recruitment_type
    local b
    if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED") == true then
        b = 2
    else
        b = 1
    end
    for g = b, 2 do
        if g == 1 then
            recruitment_type = "local1"
        elseif g == 2 then
            recruitment_type = "global"
        end
        local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", recruitment_type)
        if recruitment_uic then</code>


The first function <code>initialise_uics(recruitment_type)</code> handles resizing the parents for UICs to make sure the list clips are displaying our new components and everything else appears correctly. The first part of that finds the UICs we'll be using:
Next it finds the UICs we'll be using and deals with a weird UI quirk with changing stances several times:


  <code>local function initialise_uics(recruitment_type)
  <code>local recruitment_docker_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker")
    local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", recruitment_type)
            local unit_list = find_uicomponent(recruitment_uic, "unit_list")
    if recruitment_uic then
            local listview_uic = find_uicomponent(unit_list, "listview")
        local recruitment_docker_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker")
            local hslider_uic = find_uicomponent(listview_uic, "hslider")
        local unit_list = find_uicomponent(recruitment_uic, "unit_list")
            local list_clip_uic = find_uicomponent(listview_uic, "list_clip")
        local listview_uic = find_uicomponent(unit_list, "listview")
            local list_box_uic = find_uicomponent(list_clip_uic, "list_box")
        local list_clip_uic = find_uicomponent(listview_uic, "list_clip")
            -- to prevent some weird quirk with UI when you switch between army stances several times
        local list_box_uic = find_uicomponent(list_clip_uic, "list_box")</code>
            if recruitment_type == "global" and cm:get_saved_value("pr_cost_encamp_clicked") ~= true then
                cm:set_saved_value("pr_cost_encamp_clicked", true)
            end</code>


The next part uses a unit from our table as a reference. If none of the units exist, the function ends.
The next part uses a unit from our table as a reference. If none of the units exist, the function ends.
Line 93: Line 123:
                 break
                 break
             end
             end
         end</code>
         end
if reference_unit ~= nil then</code>


The next part makes the UIC's we found earlier able to be resized, and resizes according to the dimensions of adding another recruitment cost UIC. There is also some fine-tuning to make sure the UI doesn't clip anything. Finally it then runs <code>finalise_uics()</code>.
The next part makes the UIC's we found earlier able to be resized, and resizes according to the dimensions of adding another recruitment cost UIC and whether the slider for multiple units exists. There is also some fine-tuning to make sure the UI doesn't clip anything. Finally it then runs <code>finalise_uics()</code>.


  <code>if reference_unit ~= nil then
  <code>local unit_uic = find_uicomponent(list_box_uic, reference_unit)
            local unit_uic = find_uicomponent(list_box_uic, reference_unit)
                local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")
            local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")
                -- dimensions for resizing components
            recruitment_docker_uic:SetCanResizeHeight(true)
                local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
            recruitment_docker_uic:SetCanResizeWidth(true)
                -- if the slider if there to scroll through multiple units we need to resize things differently
            unit_list:SetCanResizeHeight(true)
                if hslider_uic then
            unit_list:SetCanResizeWidth(true)
                    -- resizing recruitment_docker
            list_clip_uic:SetCanResizeHeight(true)
                    local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
            list_clip_uic:SetCanResizeWidth(true)
                    recruitment_docker_uic:SetCanResizeHeight(true)
            unit_uic:SetCanResizeHeight(true)
                    recruitment_docker_uic:SetCanResizeWidth(true)
            unit_uic:SetCanResizeWidth(true)
                    recruitment_docker_uic:Resize(width_rdc, (height_rdc + (height_rcc * 4)), false)  
            -- dimensions for resizing components
                    -- resizing unit_list
            local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
                    local width_lrc, height_lrc = unit_list:Dimensions()
            -- resizing recruitment_docker
                    unit_list:SetCanResizeHeight(true)
            local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
                    unit_list:SetCanResizeWidth(true)
            recruitment_docker_uic:Resize(width_rdc, (height_rdc + (height_rcc * 2)), false)  
                    unit_list:Resize(width_lrc, (height_lrc + (height_rcc * 2)), false)  
            -- resizing unit_list
                else
            local width_lrc, height_lrc = unit_list:Dimensions()
                    -- resizing recruitment_docker
            unit_list:Resize(width_lrc, (height_lrc + height_rcc), false)  
                    local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
            -- resizing list_clip
                    recruitment_docker_uic:SetCanResizeHeight(true)
            local width_lcc, height_lcc = list_clip_uic:Dimensions()
                    recruitment_docker_uic:SetCanResizeWidth(true)
            list_clip_uic:Resize(width_lcc, (height_lcc + (height_rcc * 2) + 5), false) -- the plus 5 is to make sure there's no clipping at the bottom of the upkeep cost component
                    recruitment_docker_uic:Resize(width_rdc, (height_rdc + (height_rcc * 2)), false)  
             -- handling pr cost components
                    -- resizing unit_list
            finalise_uics()
                    local width_lrc, height_lrc = unit_list:Dimensions()
                    unit_list:SetCanResizeHeight(true)
                    unit_list:SetCanResizeWidth(true)
                    unit_list:Resize(width_lrc, (height_lrc + height_rcc), false)  
                end
                -- resizing list_clip
                local width_lcc, height_lcc = list_clip_uic:Dimensions()
                list_clip_uic:SetCanResizeHeight(true)
                list_clip_uic:SetCanResizeWidth(true)
                list_clip_uic:Resize(width_lcc, (height_lcc + (height_rcc * 2) + 5), false) -- the plus 5 is to make sure there's no clipping at the bottom of the upkeep cost component
             end
         end
         end
    end
    end
  end</code>
    -- handling pr cost components
    out("PR UNIT COST LOG - finalise_uics() from initialise_uics().")
    finalise_uics()</code>


=== 👋 Finalising UICs ===
=== 👋 Finalising UICs ===
Line 131: Line 174:
  <code>local recruitment_type
  <code>local recruitment_type
     for g = 1, 2 do
     for g = 1, 2 do
        -- loops for local recruitment and global recruitment
         if g == 1 then
         if g == 1 then
             recruitment_type = "local1"
             recruitment_type = "local1"
Line 146: Line 188:
                 local prestige_cost = pr_units[i].prestige_cost
                 local prestige_cost = pr_units[i].prestige_cost
                 local listview_uic = find_uicomponent(recruitment_uic, "unit_list", "listview")
                 local listview_uic = find_uicomponent(recruitment_uic, "unit_list", "listview")
                local hslider_uic = find_uicomponent(listview_uic, "hslider")
                 local unit_uic = find_uicomponent(listview_uic, "list_clip", "list_box", pr_units[i].unit_uic)
                 local unit_uic = find_uicomponent(listview_uic, "list_clip", "list_box", pr_units[i].unit_uic)
                 if unit_uic then
                 if unit_uic then
Line 157: Line 200:
The next part resizes and repositions the UICs we found above, including the new "prestige_cost" UIC we just created:
The next part resizes and repositions the UICs we found above, including the new "prestige_cost" UIC we just created:


  <code>             -- dimensions for resizing components
  <code>                   -- dimensions for resizing components
                     local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
                     local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
                     -- resizing unit component
                     -- resizing unit component
                     local width_uc, height_uc = unit_uic:Dimensions()
                     local width_uc, height_uc = unit_uic:Dimensions()
                    unit_uic:SetCanResizeHeight(true)
                    unit_uic:SetCanResizeWidth(true)
                     unit_uic:Resize(width_uc, (height_uc + (height_rcc * 2) + 5), false)
                     unit_uic:Resize(width_uc, (height_uc + (height_rcc * 2) + 5), false)
                     -- repositioning list_view
                     -- repositioning list_view
Line 168: Line 213:
                     local prestige_cost_uic = find_uicomponent(prestige_cost_parent_uic, "Cost")
                     local prestige_cost_uic = find_uicomponent(prestige_cost_parent_uic, "Cost")
                     prestige_cost_parent_uic:SetDockOffset(8, 19)
                     prestige_cost_parent_uic:SetDockOffset(8, 19)
                     upkeep_cost_uic:SetDockOffset(8, 42)</code>
                     upkeep_cost_uic:SetDockOffset(8, 42)
                    -- repositioning the hslider_uic
                    if hslider_uic then
                        hslider_uic:SetDockOffset(8, 49)
                    end</code>


Finally, we then set the text, tooltip, and icon, disable the unit card UICs if the player doesn't have enough of the Pooled Resource, and hide the cost modified icon for ease.
Finally, we then set the text, tooltip, and icon, disable the unit card UICs if the player doesn't have enough of the Pooled Resource, and hide the cost modified icon for ease.
Line 187: Line 236:
                         if string.match(unit_uic_tooltip_gsub, left_click_loc_gsub) then
                         if string.match(unit_uic_tooltip_gsub, left_click_loc_gsub) then
                             unit_uic:SetTooltipText(unit_name.."\n\n"..cannot_recruit_loc.."\n\n"..insufficient_pr_loc, "", true)
                             unit_uic:SetTooltipText(unit_name.."\n\n"..cannot_recruit_loc.."\n\n"..insufficient_pr_loc, "", true)
                            out("PR UNIT COST LOG - Tooltip for just insufficient prestige.")
                         else
                         else
                             unit_uic:SetTooltipText(unit_uic_tooltip.."\n"..insufficient_pr_loc, "", true)  
                             unit_uic:SetTooltipText(unit_uic_tooltip.."\n"..insufficient_pr_loc, "", true)  
                            out("PR UNIT COST LOG - Tooltip for prestige plus stuff.")
                         end
                         end
                         -- disabling recruitment of unit
                         -- disabling recruitment of unit
Line 197: Line 248:
                     prestige_cost_uic:SetImagePath("ui/skins/default/prestige_bar_icon.png", 0)
                     prestige_cost_uic:SetImagePath("ui/skins/default/prestige_bar_icon.png", 0)
                     -- setting cost tooltip
                     -- setting cost tooltip
                     prestige_cost_parent_uic:SetTooltipText("Cost||This amount will be deducted from your Prestige in order to recruit this unit.", "", true)  
                     prestige_cost_parent_uic:SetTooltipText(common.get_localised_string("pear_pr_prestige_cost_tooltip"), "", true)  
                     -- setting the cost modified icon to invisible as the CCO is still for recruitment cost so make it appear when intended
                     -- setting the cost modified icon to invisible as the CCO is still for recruitment cost so make it appear when intended
                     local cost_modified_icon_uic = find_uicomponent(prestige_cost_uic, "cost_modified_icon")
                     local cost_modified_icon_uic = find_uicomponent(prestige_cost_uic, "cost_modified_icon")
                     cost_modified_icon_uic:Visible(false)
                     cost_modified_icon_uic:SetVisible(false)
                 end
                 end
             end
             end
         end
         end
     end
     end
end</code>
end</code>


== 🎮 Triggering the UI ==
== 🎮 Triggering the UI ==
To make the UI components work we need listeners! The first one is <code>PanelOpenedCampaign</code>. We can use this to tell when the main recruitment panel <code>"units_recruitment"</code> is open so we can start setting up our UI. This is important because the UI usually refreshes when certain UICs are clicked which causes the panel to re-open. For this listener to work for both local and global recruitment we need a for loop to run the set-up for both types of recruitment which will use this table:
To make the UI components work we need listeners! The first one is <code>PanelOpenedCampaign</code>. We can use this to tell when the main recruitment panel <code>"units_recruitment"</code> is open so we can start setting up our UI. This is important because the UI usually refreshes when certain UICs are clicked which causes the panel to re-open. We also need a saved value to make sure the listener doesn't continually resize the parent UI components every time something is clicked. Together the listener looks like this:
   
   
  <code>local recruitment_types = {
  <code>   core:add_listener(
{recruitment_type = "local1"},
        "EXAMPLE_PANEL_OPENED",
{recruitment_type = "global"}
        "PanelOpenedCampaign",
}</code>
        function(context)
 
            return context.string == "units_recruitment"
We also need a saved value to make sure the listener doesn't continually resize the parent UI components every time something is clicked. Together the listener looks like this:
        end,
        function()
<code>core:add_listener(
             out("PR UNIT COST LOG - units_recruitment panel opened.")
    "EXAMPLE_PANEL_OPENED",
             if cm:get_saved_value("EXAMPLE_UICS_INITIALISED") ~= true then
    "PanelOpenedCampaign",
                 cm:set_saved_value("EXAMPLE_UICS_INITIALISED", true)
    function(context)
                 initialise_uics()
        return context.string == "units_recruitment"
    end,
    function()
        for i = 1, #recruitment_types do
             local recruitment_type = recruitment_types[i].recruitment_type
             if cm:get_saved_value("EXAMPLE_INIT_1") ~= true then
                 cm:set_saved_value("EXAMPLE_INIT_1", true)
                 initialise_uics(recruitment_type)
             else
             else
                out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_units_recruitment_opened.")
                 finalise_uics()
                 finalise_uics()
                return
             end
             end
         end
         end,
    end,
        true
    true
    )</code>
)</code>


A refresh to the UI occurs when switching to the Encamp stance. This messes up the sizing of the parent panels. To account for this we need to use <code>ComponentLClickUp</code> which checks when the Encamp button is pressed as well as a saved value similar to before like this:
A refresh to the UI occurs when switching army stance. This messes up the sizing of the parent panels. To account for this we need to use <code>ComponentLClickUp</code> which checks when the stance changes occur like this:


  <code>core:add_listener(
  <code>core:add_listener(
    "EXAMPLE_UIC_CLICKED",
        "EXAMPLE_UICS_CLICKED",
    "ComponentLClickUp",
        "ComponentLClickUp",
    function(context)
        true,
        return context.string == "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE_SET_CAMP"
        function(context)
    end,
            if string.match(context.string, "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE_SET_CAMP") then
    function()
                cm:callback(
        local recruitment_type = "global"
                    function()
        cm:callback(
                        if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED") ~= true then
            function()
                            cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED", true)
                if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED_1") ~= true then
                            initialise_uics()
                    cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED_1", true)
                        else
                    initialise_uics(recruitment_type)
                            out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_finalise_uics.")
                else
                            finalise_uics()
                    finalise_uics()
                        end
                end
                    end,  
            end,  
                    0.1
            0.1 -- this delay is due to a timing issue with refreshing UI
                )
        )
            elseif string.match(context.string, "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE") then
    end,
                cm:callback(
    true
                    function()
)</code>
                        out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_finalise_uics.")
                        finalise_uics()
                    end,  
                    0.1
                )
            end</code>


This listener is '''used again later''' and combined with something else.
'''*'''This listener is '''used again later''' and combined with something else.


Now we need to use <code>PanelClosedCampaign</code> to reset our saved values when the entire <code>"units_panel"</code> closes.
Now we need to use <code>PanelClosedCampaign</code> to reset our saved values when the entire <code>"units_panel"</code> closes.
Line 274: Line 321:
         end,
         end,
         function()
         function()
             for i = 1, #recruitment_types do
             out("PR UNIT COST LOG - units_panel panel closed.")
                local recruitment_type = recruitment_types[i].recruitment_type
            cm:set_saved_value("EXAMPLE_UICS_INITIALISED", false)
                cm:set_saved_value("EXAMPLE_INIT_1", false)
             cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED", false)
            end
             cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED_1", false)
         end,
         end,
         true
         true
     )</code>
     )</code>


Next we need to use <code>ComponentLClickUp</code> and <code>UITrigger</code>. These listeners act as a pair — <code>ComponentLClickUp</code> sends a message using <code>TriggerCampaignScriptEvent</code> to the <code>UITrigger</code> to keep things multiplayer-friendly (for more info on how that works see [https://tw-modding.com/wiki/Multiplayer_Desyncs this guide).  
Next we need to use <code>ComponentLClickUp</code> and <code>UITrigger</code>. These listeners act as a pair — <code>ComponentLClickUp</code> sends a message using <code>TriggerCampaignScriptEvent</code> to the <code>UITrigger</code> to keep things multiplayer-friendly (for more info on how that works see [https://tw-modding.com/wiki/Multiplayer_Desyncs this guide]).  


What is that message you may ask? Well, when the mouse hovers over a unit card, the stats panel for the unit becomes visible. At the moment the unit card is clicked, <code>ComponentLClickUp</code> gets the name of the unit and makes a saved value for it. It checks it against our onscreen name loc provided in our table and if they match then:
What is that message you may ask? Well, when the mouse hovers over a unit card, the stats panel for the unit becomes visible. At the moment the unit card is clicked, <code>ComponentLClickUp</code> gets the name of the unit from the stats panel and makes a saved value for it. It checks it against our onscreen name loc provided in our table and if they match then


[[File:klopp-boom2.gif|250px]]
[[File:klopp-boom2.gif|250px]]
Line 305: Line 350:
     )</code>
     )</code>


This then combines with the encamp stance check into the same listener.
'''*'''This then combines with the encamp stance check into the same listener.


Finally, you have the <code>UITrigger</code> listener which receives the message. This then adds or subtracts the Pooled Resource from the player depending on whether they've recruited the unit or cancelled recruitment.
Finally, you have the <code>UITrigger</code> listener which receives the message. This then adds or subtracts the Pooled Resource from the player depending on whether they've recruited the unit or cancelled recruitment.
Line 314: Line 359:
         true,
         true,
         function(context)
         function(context)
            local faction_cqi = context:faction_cqi();
            local faction_key = cm:model():faction_for_command_queue_index(faction_cqi):name()
             for i = 1, #pr_units do
             for i = 1, #pr_units do
                 local unit_name = common.get_localised_string(pr_units[i].unit_name_loc)
                 local unit_name = common.get_localised_string(pr_units[i].unit_name_loc)
Line 321: Line 364:
                     local unit_uic = pr_units[i].unit_uic
                     local unit_uic = pr_units[i].unit_uic
                     local prestige_cost = pr_units[i].prestige_cost
                     local prestige_cost = pr_units[i].prestige_cost
                     if context:trigger() == unit_uic.."Clicked" then  
                     if context:trigger() == unit_uic.."Clicked" then
                        local faction_cqi = context:faction_cqi();
                        local faction_key = cm:model():faction_for_command_queue_index(faction_cqi):name()
                         cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", -prestige_cost)
                         cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", -prestige_cost)
                         return
                         return
                     elseif context:trigger():starts_with("Queued") then
                     elseif context:trigger():starts_with("Queued") then
                        local faction_cqi = context:faction_cqi();
                        local faction_key = cm:model():faction_for_command_queue_index(faction_cqi):name()
                         cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", prestige_cost)
                         cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", prestige_cost)
                         return
                         return

Latest revision as of 22:30, 15 July 2023

Total War: Warhammer 3 Guide by Pear

This guide and the example mod are my submission for Mod Jam #7.

Pooled Resources are cool. The problem is their application is often limited. This tutorial aims to fix that by explaining how to use Pooled Resources as costs for units in the main recruitment panel and providing an example mod for reference. The method used here can be applied to a range of other things such as RoRs, lords, agents, buildings, technologies, or anything like that.

There's currently nothing in the tables to make units cost Pooled Resources except for a few faction-specific things so we have to use Lua UI scripting instead. It works by checking when the player has clicked certain units, taking away the defined cost of those units, and creating some UI components (UICs) to visualise all this. The process consists of two main parts:

  1. Making the UI: making UICs, enabling and disabling UICs, and setting text, tooltips, and icons.
  2. Triggering the UI: creating the UI at the right time, accounting for UI refreshes, and taking away / returning the Pooled Resource to the player in a multiplayer-friendly way.

Before we get started I'd recommend using my example mod as a reference, as well as Groove Wizard's Visual Studio development environment for your scripting, and Groove's Modding Development Tools: Lua Console to test the code in game (yes this man is a machine). This guide assumes some knowledge of RPFM and scripting.

🧑‍🎓 Some UI Basics

If you're not familiar with UI modding then you may find this section useful. If you're too kool for skool, move along!

🔍 Finding UI Components

Recruitment Cost Component in the Context Viewer

In order to modify a part of the UI you need to find where it's located. To do this we can use the Context Viewer. A quick way to do this is by having the Context Viewer open and clicking on the screen with your mouse wheel.

For the purpose of this tutorial, try clicking the unit card you want to add a Pooled Resource to then expanding the unit card path in the Context Viewer until you find the "RecruitmentCost" UIC. Once you've found it, paste CopyFullPathToClipboard() in the expression tester box. This copies the path to that UIC.

When copied it will look like this: ":root:units_panel:main_units_panel:..." and so on.

Referencing my example mod, you can write the path to the recruitment cost UIC for the Swordsmen unit like this:

local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", "local1", "unit_list", "listview", "list_clip", "list_box", "wh_main_emp_inf_swordsmen_recruitable", "unit_icon", "RecruitmentCost") 

It is fine as it is however it can be split into smaller, easier-to-access parts like this:

local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", recruitment_type)
local listview_uic = find_uicomponent(recruitment_uic, "unit_list", "listview")
local unit_uic = find_uicomponent(listview_uic, "list_clip", "list_box", "wh_main_emp_inf_swordsmen_recruitable")
local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")

📷 Creating / Copying Components

For our purposes we will probably only need to create copies of pre-existing UICs. To do that you need to use uicomponent:CopyComponent() which is being used here to copy the recruitment cost UIC for the Swordsmen unit and naming the new UIC "prestige cost":

UIComponent(recruitment_cost_uic:CopyComponent("prestige_cost"))

Using the paths we've previously established, the path for this new "prestige_cost" UIC we've created is therefore:

local prestige_cost_parent_uic = UIComponent(recruitment_cost_uic:CopyComponent("prestige_cost"))

⬇️ Repositioning Components

This part is a bit more tedious. You will need to use the console (shift + f3) from Groove's Modding Dev Tools to test where the new "prestige_cost" UIC we've created should be repositioned. To do that you need to use uicomponent:SetDockOffset(). Using the code below you can test coordinates e.g. x = 0, y = 5 until the new UIC looks like it's in the right place:

local prestige_cost_parent_uic = find_uicomponent(unit_uic, "unit_icon", "prestige_cost")
prestige_cost_parent_uic:SetDockOffset(x, y)

If you really want to make sure it's in the right place take a screenshot and do a bit of pixel peeping in a photo editor and count the pixels between e.g. the original recruitment cost and the upkeep cost UICs and that's how many pixels your new component should be below the recruitment cost UIC.

Using my example mod, we can see that the correct coordinates for the new component in the normal recruitment panel are:

prestige_cost_parent_uic:SetDockOffset(8, 19)

this may change depending on the faction or panel e.g. RoRs or lords.

↗️ Resizing Components

Resizing components may be necessary if you're adding UICs, especially resizing list clips which would otherwise cut off the new UICs you've made. To do that you need to use Resize like this:

unit_uic:SetCanResizeHeight(true)
unit_uic:SetCanResizeWidth(true)
local width, height= unit_uic:Dimensions()
unit_uic:Resize(width, (height +  + 10), false)

🪟 Finding panels

To find the name of the panel that you want to check is opened or closed you can check the log produced by Groove's Modding Dev Tools. In the log it will look something like this:

[ui] <97.6s>    Panel opened units_panel
[ui] <98.8s>    Panel opened recruitment_options
[ui] <98.8s>    Panel opened units_recruitment

🎨 Making the UI

So let's start! First we'll be looking at two functions — initialise_uics() and finalise_uics() . In short, the two functions apply the UI knowledge we covered earlier (and a little extra) to create UICs, resize UICs, and set their docking points, text, and icons. The functions are seperated even though they do similar things because the first one has to run only when the panel is opened, whereas the second has to run every time the UI is refreshed while the panel is open. For the purpose of the guide we will be keeping the vanilla treasury costs and making one new Pooled Resource cost which will go below the unit card.

IMPORTANT: Make sure you change the listener and saved value names from the examples to avoid incompatibilities.

🚀 Initialising UICs

First, we need a table for the units we want to have Pooled Resource costs. It should contain the loc key for the unit's onscreen name, the unit's UIC (which is just the unit's key plus "_recruitable"), and the Pooled Resource cost. Together it should look like this:

local pr_units = {
   {unit_name_loc = "land_units_onscreen_name_wh_main_emp_inf_spearmen_1", unit_uic = "wh_main_emp_inf_spearmen_1_recruitable", prestige_cost = 18},
   {unit_name_loc = "land_units_onscreen_name_wh_main_emp_inf_swordsmen", unit_uic = "wh_main_emp_inf_swordsmen_recruitable", prestige_cost = 19}
}

This table is then used in the functions initialise_uics() and finalise_uics().

The first function initialise_uics() handles resizing the parents for UICs to make sure the list clips are displaying our new components and everything else appears correctly. The first part of that runs in a loop, once for local and once for global recruitment if the UIC exists then it proceeds:

local function initialise_uics()
   out("PR UNIT COST LOG - Initialising components.")
   local recruitment_type
   local b
   if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED") == true then
       b = 2
   else
       b = 1
   end
   for g = b, 2 do
       if g == 1 then
           recruitment_type = "local1"
       elseif g == 2 then
           recruitment_type = "global"
       end
       local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", recruitment_type)
       if recruitment_uic then

Next it finds the UICs we'll be using and deals with a weird UI quirk with changing stances several times:

local recruitment_docker_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker")
           local unit_list = find_uicomponent(recruitment_uic, "unit_list")
           local listview_uic = find_uicomponent(unit_list, "listview")
           local hslider_uic = find_uicomponent(listview_uic, "hslider")
           local list_clip_uic = find_uicomponent(listview_uic, "list_clip")
           local list_box_uic = find_uicomponent(list_clip_uic, "list_box")
           -- to prevent some weird quirk with UI when you switch between army stances several times
           if recruitment_type == "global" and cm:get_saved_value("pr_cost_encamp_clicked") ~= true then
               cm:set_saved_value("pr_cost_encamp_clicked", true)
           end

The next part uses a unit from our table as a reference. If none of the units exist, the function ends.

local reference_unit
       for i = 1, #pr_units do
           reference_unit = pr_units[i].unit_uic
           local unit_uic = find_uicomponent(list_box_uic, reference_unit)
           if unit_uic then
               break
           elseif unit_uic == false then
               reference_unit = nil
               break
           end
       end
if reference_unit ~= nil then

The next part makes the UIC's we found earlier able to be resized, and resizes according to the dimensions of adding another recruitment cost UIC and whether the slider for multiple units exists. There is also some fine-tuning to make sure the UI doesn't clip anything. Finally it then runs finalise_uics().

local unit_uic = find_uicomponent(list_box_uic, reference_unit)
               local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")
               -- dimensions for resizing components
               local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
               -- if the slider if there to scroll through multiple units we need to resize things differently
               if hslider_uic then
                   -- resizing recruitment_docker
                   local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
                   recruitment_docker_uic:SetCanResizeHeight(true)
                   recruitment_docker_uic:SetCanResizeWidth(true)
                   recruitment_docker_uic:Resize(width_rdc, (height_rdc + (height_rcc * 4)), false) 
                   -- resizing unit_list
                   local width_lrc, height_lrc = unit_list:Dimensions()
                   unit_list:SetCanResizeHeight(true)
                   unit_list:SetCanResizeWidth(true)
                   unit_list:Resize(width_lrc, (height_lrc + (height_rcc * 2)), false) 
               else
                   -- resizing recruitment_docker
                   local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
                   recruitment_docker_uic:SetCanResizeHeight(true)
                   recruitment_docker_uic:SetCanResizeWidth(true)
                   recruitment_docker_uic:Resize(width_rdc, (height_rdc + (height_rcc * 2)), false) 
                   -- resizing unit_list
                   local width_lrc, height_lrc = unit_list:Dimensions()
                   unit_list:SetCanResizeHeight(true)
                   unit_list:SetCanResizeWidth(true)
                   unit_list:Resize(width_lrc, (height_lrc + height_rcc), false) 
               end
               -- resizing list_clip
               local width_lcc, height_lcc = list_clip_uic:Dimensions()
               list_clip_uic:SetCanResizeHeight(true)
               list_clip_uic:SetCanResizeWidth(true)
               list_clip_uic:Resize(width_lcc, (height_lcc + (height_rcc * 2) + 5), false) -- the plus 5 is to make sure there's no clipping at the bottom of the upkeep cost component
           end
       end
   end
   -- handling pr cost components
   out("PR UNIT COST LOG - finalise_uics() from initialise_uics().")
   finalise_uics()

👋 Finalising UICs

The second function finalise_uics() handles the creation of our new UICs, setting the Pooled Resource costs, text, tooltips, icons, and handling anything that gets refreshed when UICs are clicked. The first part loops the whole function for local and global recruitment, if one of those doesn't exist in the UI then that part of the function doesn't run. So if your army is not encamped then it won't cause a script error by trying to run the global part of the function.

local recruitment_type
   for g = 1, 2 do
       if g == 1 then
           recruitment_type = "local1"
       elseif g == 2 then
           recruitment_type = "global"
       end
       local recruitment_uic = find_uicomponent(core:get_ui_root(), "units_panel", "main_units_panel", "recruitment_docker", "recruitment_options", "recruitment_listbox", recruitment_type)
       if recruitment_uic then

The next part finds the UICs we'll be using in the function and creates our new "prestige_cost" UIC:

for i = 1, #pr_units do
               local unit_name = common.get_localised_string(pr_units[i].unit_name_loc)
               local prestige_cost = pr_units[i].prestige_cost
               local listview_uic = find_uicomponent(recruitment_uic, "unit_list", "listview")
               local hslider_uic = find_uicomponent(listview_uic, "hslider")
               local unit_uic = find_uicomponent(listview_uic, "list_clip", "list_box", pr_units[i].unit_uic)
               if unit_uic then
                   local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")
                   local upkeep_cost_uic = find_uicomponent(unit_uic, "unit_icon", "UpkeepCost")
                   local prestige_cost_uic_check = find_uicomponent(unit_uic, "unit_icon", "prestige_cost")
                   if prestige_cost_uic_check == false then
                       UIComponent(recruitment_cost_uic:CopyComponent("prestige_cost"))
                   end

The next part resizes and repositions the UICs we found above, including the new "prestige_cost" UIC we just created:

                    -- dimensions for resizing components
                   local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
                   -- resizing unit component
                   local width_uc, height_uc = unit_uic:Dimensions()
                   unit_uic:SetCanResizeHeight(true)
                   unit_uic:SetCanResizeWidth(true)
                   unit_uic:Resize(width_uc, (height_uc + (height_rcc * 2) + 5), false)
                   -- repositioning list_view
                   listview_uic:SetDockOffset(0, -24)
                   -- repositioning cost components
                   local prestige_cost_parent_uic = find_uicomponent(unit_uic, "unit_icon", "prestige_cost")
                   local prestige_cost_uic = find_uicomponent(prestige_cost_parent_uic, "Cost")
                   prestige_cost_parent_uic:SetDockOffset(8, 19)
                   upkeep_cost_uic:SetDockOffset(8, 42)
                   -- repositioning the hslider_uic
                   if hslider_uic then
                       hslider_uic:SetDockOffset(8, 49)
                   end

Finally, we then set the text, tooltip, and icon, disable the unit card UICs if the player doesn't have enough of the Pooled Resource, and hide the cost modified icon for ease.

local faction = cm:get_faction(cm:get_local_faction_name(true))
                   local player_prestige = faction:pooled_resource_manager():resource("emp_prestige"):value()
                   if player_prestige >= prestige_cost then
                       -- setting cost text
                       prestige_cost_uic:SetStateText(tostring(prestige_cost), "")
                   else
                       -- setting cost text
                       prestige_cost_uic:SetStateText(tostring("col:red"..prestige_cost.."/col"), "")
                       local unit_uic_tooltip = unit_uic:GetTooltipText()
                       local cannot_recruit_loc = common.get_localised_string("random_localisation_strings_string_StratHudbutton_Cannot_Recruit_Unit0")
                       local insufficient_pr_loc = common.get_localised_string("pear_insufficient_pr_prestige_tooltip")
                       local unit_uic_tooltip_gsub = unit_uic_tooltip:gsub('[%W]', )
                       local left_click_loc_gsub = (common.get_localised_string("random_localisation_strings_string_StratHud_Unit_Card_Recruit_Selection")):gsub('[%W]', )
                       if string.match(unit_uic_tooltip_gsub, left_click_loc_gsub) then
                           unit_uic:SetTooltipText(unit_name.."\n\n"..cannot_recruit_loc.."\n\n"..insufficient_pr_loc, "", true)
                           out("PR UNIT COST LOG - Tooltip for just insufficient prestige.")
                       else
                           unit_uic:SetTooltipText(unit_uic_tooltip.."\n"..insufficient_pr_loc, "", true) 
                           out("PR UNIT COST LOG - Tooltip for prestige plus stuff.")
                       end
                       -- disabling recruitment of unit
                       unit_uic:SetState("inactive")
                       unit_uic:SetDisabled(true)
                   end
                   -- setting cost icon
                   prestige_cost_uic:SetImagePath("ui/skins/default/prestige_bar_icon.png", 0)
                   -- setting cost tooltip
                   prestige_cost_parent_uic:SetTooltipText(common.get_localised_string("pear_pr_prestige_cost_tooltip"), "", true) 
                   -- setting the cost modified icon to invisible as the CCO is still for recruitment cost so make it appear when intended
                   local cost_modified_icon_uic = find_uicomponent(prestige_cost_uic, "cost_modified_icon")
                   cost_modified_icon_uic:SetVisible(false)
               end
           end
       end
   end

end

🎮 Triggering the UI

To make the UI components work we need listeners! The first one is PanelOpenedCampaign. We can use this to tell when the main recruitment panel "units_recruitment" is open so we can start setting up our UI. This is important because the UI usually refreshes when certain UICs are clicked which causes the panel to re-open. We also need a saved value to make sure the listener doesn't continually resize the parent UI components every time something is clicked. Together the listener looks like this:

    core:add_listener(
       "EXAMPLE_PANEL_OPENED",
       "PanelOpenedCampaign",
       function(context)
           return context.string == "units_recruitment"
       end,
       function()
           out("PR UNIT COST LOG - units_recruitment panel opened.")
           if cm:get_saved_value("EXAMPLE_UICS_INITIALISED") ~= true then
               cm:set_saved_value("EXAMPLE_UICS_INITIALISED", true)
               initialise_uics()
           else
               out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_units_recruitment_opened.")
               finalise_uics()
           end
       end,
       true
   )

A refresh to the UI occurs when switching army stance. This messes up the sizing of the parent panels. To account for this we need to use ComponentLClickUp which checks when the stance changes occur like this:

core:add_listener(
       "EXAMPLE_UICS_CLICKED",
       "ComponentLClickUp",
       true,
       function(context)
           if string.match(context.string, "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE_SET_CAMP") then
               cm:callback(
                   function()
                       if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED") ~= true then
                           cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED", true)
                           initialise_uics()
                       else
                           out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_finalise_uics.")
                           finalise_uics()
                       end
                   end, 
                   0.1
               )
           elseif string.match(context.string, "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE") then
               cm:callback(
                   function()
                       out("PR UNIT COST LOG - finalise_uics() from pr_unit_cost_finalise_uics.")
                       finalise_uics()
                   end, 
                   0.1
               )
           end

*This listener is used again later and combined with something else.

Now we need to use PanelClosedCampaign to reset our saved values when the entire "units_panel" closes.

core:add_listener(
       "EXAMPLE_PANEL_CLOSED",
       "PanelClosedCampaign",
       function(context)
           return context.string == "units_panel"
       end,
       function()
           out("PR UNIT COST LOG - units_panel panel closed.")
           cm:set_saved_value("EXAMPLE_UICS_INITIALISED", false)
           cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED", false)
       end,
       true
   )

Next we need to use ComponentLClickUp and UITrigger. These listeners act as a pair — ComponentLClickUp sends a message using TriggerCampaignScriptEvent to the UITrigger to keep things multiplayer-friendly (for more info on how that works see this guide).

What is that message you may ask? Well, when the mouse hovers over a unit card, the stats panel for the unit becomes visible. At the moment the unit card is clicked, ComponentLClickUp gets the name of the unit from the stats panel and makes a saved value for it. It checks it against our onscreen name loc provided in our table and if they match then

Klopp-boom2.gif

           local unit_info_uic = find_uicomponent(core:get_ui_root(), "hud_campaign", "unit_info_panel_adopter", "unit_information_parent", "unit_info_panel_holder_parent", "unit_info_panel_holder", "unit_information", "info_parent", "info_panel", "tx_unit-type")
           if unit_info_uic then
               local unit_ui_name = unit_info_uic:GetStateText()
               for i = 1, #pr_units do
                   local unit_name = common.get_localised_string(pr_units[i].unit_name_loc)
                   if unit_ui_name == unit_name then
                       cm:set_saved_value("EXAMPLE_UNIT_NAME", unit_name)
                       CampaignUI.TriggerCampaignScriptEvent(cm:get_faction(cm:get_local_faction_name(true)):command_queue_index(), context.string .."Clicked")
                       return
                   end
               end
           end
       end,
       true
   )

*This then combines with the encamp stance check into the same listener.

Finally, you have the UITrigger listener which receives the message. This then adds or subtracts the Pooled Resource from the player depending on whether they've recruited the unit or cancelled recruitment.

core:add_listener(
       "pr_unit_cost_handle_pr_cost_ui_trigger",
       "UITrigger",
       true,
       function(context)
           for i = 1, #pr_units do
               local unit_name = common.get_localised_string(pr_units[i].unit_name_loc)
               if unit_name == cm:get_saved_value("EXAMPLE_UNIT_NAME") then
                   local unit_uic = pr_units[i].unit_uic
                   local prestige_cost = pr_units[i].prestige_cost
                   if context:trigger() == unit_uic.."Clicked" then
                       local faction_cqi = context:faction_cqi();
                       local faction_key = cm:model():faction_for_command_queue_index(faction_cqi):name() 
                       cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", -prestige_cost)
                       return
                   elseif context:trigger():starts_with("Queued") then
                       local faction_cqi = context:faction_cqi();
                       local faction_key = cm:model():faction_for_command_queue_index(faction_cqi):name() 
                       cm:faction_add_pooled_resource(faction_key, "emp_prestige", "events_negative", prestige_cost)
                       return
                   end
               end
           end
       end,
       true
   )

And that's it!