Tutorial:Pooled Resource Unit Costs: Difference between revisions
Line 21: | Line 21: | ||
=== 🔍 Finding UI Components === | === 🔍 Finding UI Components === | ||
[[File:Recruitment cost component.png|right|thumb]] | [[File:Recruitment cost component.png|right|thumb]] | ||
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]. | 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. | ||
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. | |||
When copied it will look like this: ":root:units_panel:main_units_panel:..." and so on. | When copied it will look like this: ":root:units_panel:main_units_panel:..." and so on. | ||
Referencing my example mod, to | Referencing my example mod, you can write the path to the recruitment cost UIC like this: | ||
<code>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")</code> | <code>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")</code> | ||
Revision as of 13:16, 10 July 2023
Originally written by Pear
💬 Let's Talk Pooled Resources
Pooled Resources are cool. The problem is they're not used for very much. 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 theory used here can be applied to a range of other things such as RoRs, lords, agents, buildings, technologies, or anything really!
This guide and the example mod are also my submission for Mod Jam #7.
🧑🎓 The Theory
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).
Making something have Pooled Resource costs consists of two main parts:
- Handling the UI: creating UICs, enabling and disabling UICs, accounting for refreshes, and setting text, tooltips, and icons.
- Handling the Cost: taking away / returning the Pooled Resource to the player in a multiplayer-friendly way.
but before we get into that, here's a little bit of theory which might be useful for those who are less familiar with UI modding.
👷 Building the UI
The first step in building the UI is figuring out where you want your Pooled Resource cost to appear. Do you want to keep the vanilla treasury recruitment cost? Do you want several Pooled Resource costs? Do you want it below the unit card like normal treasury costs or on the side of the unit card like the Wood Elves' Amber resource?
🔍 Finding UI Components
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 you 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 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()
and name the new UIC e.g. "prestige cost" like this:
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 = find_uicomponent(unit_uic, "unit_icon", "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()
and your own x and y coordinates, e.g. x = 0, y = 5 like this:
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 recruitment cost UIC and the upkeep cost UIC and that's how many pixels your new component should be below the recruitment cost UIC. The correct coordinates for 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.
✋ Handling the UI
To make the UI components work we need listeners!
IMPORTANT: Make sure you change the listener and saved value names from the examples to avoid incompatibilities.
🏁 Initialising and Finalising UICs
This section controls how the UI appears, the size of UICs their text, tooltips, and icons.
Now we need to set up the 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(recruitment_type)
and finalise_uics()
.
local function initialise_uics(recruitment_type)
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
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 list_clip_uic = find_uicomponent(listview_uic, "list_clip")
local list_box_uic = find_uicomponent(list_clip_uic, "list_box")
-- gets the reference unit from the table
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
local unit_uic = find_uicomponent(list_box_uic, reference_unit)
local recruitment_cost_uic = find_uicomponent(unit_uic, "unit_icon", "RecruitmentCost")
recruitment_docker_uic:SetCanResizeHeight(true)
recruitment_docker_uic:SetCanResizeWidth(true)
unit_list:SetCanResizeHeight(true)
unit_list:SetCanResizeWidth(true)
list_clip_uic:SetCanResizeHeight(true)
list_clip_uic:SetCanResizeWidth(true)
unit_uic:SetCanResizeHeight(true)
unit_uic:SetCanResizeWidth(true)
-- dimensions for resizing components
local width_rcc, height_rcc = recruitment_cost_uic:Dimensions()
-- resizing recruitment_docker
local width_rdc, height_rdc = recruitment_docker_uic:Dimensions()
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:Resize(width_lrc, (height_lrc + height_rcc), false)
-- resizing list_clip
local width_lcc, height_lcc = list_clip_uic:Dimensions()
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
-- handling pr cost components
finalise_uics()
end
end
end
🎮 Triggering UIC Handler Functions
The first listener we need 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. To find the name of the panel you're adding Pooled Resource costs to 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
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:
local recruitment_types = {
{recruitment_type = "local1"},
{recruitment_type = "global"}
}
We also need some saved values to make sure the listener doesn't continually resize the parent UI components. Together the listener looks like this:
core:add_listener(
"EXAMPLE_PANEL_OPENED_LISTENER",
"PanelOpenedCampaign",
function(context)
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
finalise_uics()
return
end
end
end,
true
)
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 ComponentLClickUp
which checks when the Encamp button is pressed like this:
core:add_listener(
"EXAMPLE_BUTTON_CLICKED",
"ComponentLClickUp",
function(context)
return context.string == "button_MILITARY_FORCE_ACTIVE_STANCE_TYPE_SET_CAMP"
end,
function()
local recruitment_type = "global"
cm:callback(
function()
if cm:get_saved_value("EXAMPLE_ENCAMP_CLICKED_1") ~= true then
cm:set_saved_value("EXAMPLE_ENCAMP_CLICKED_1", true)
initialise_uics(recruitment_type)
else
finalise_uics()
end
end,
0.1
)
end,
true
)
- This listener is used again later and combined with something else.
Lastly we need to use PanelClosedCampaign
to reset our saved values when the whole "units_panel"
closes.
💰 Handling the Cost
Next we need to use ComponentLClickUp
and UITrigger
. These listeners act as a pair ComponentLClickUp
sends a message which is received by UITrigger
. This is required for multiplayer compatibility.
The ComponentLClickUp
listener contains 2 parts. The first part is to account for a refresh in the UI when the army's stance is changed.