Pooled Resources

This here tutorial is going to go over the basics of a Pooled Resource, why they’re awesome, and how to use them to their fullest potential. I’m going to try and not pigeon-hole readers into a specific usage of pooled resources, but I will try at all points to cover all realistic paths to go down, and all functionality that pooled resources have. Do note this tutorial will focus specifically on Warhammer II, though I may go back and add in a 3K section, or make a second tutorial.

Alright! A pooled resource is a type of game object that’s becoming more prevalent in Total War – being first introduced throughout Total War Warhammer II’s development (to my knowledge), and becoming increasingly important in Three Kingdoms where it powers many systems like population and food supplies.

A pooled resource is pretty basic, conceptually – it’s a custom resource (of whatever make or model) that is contained within a pool. In Warhammer II, pooled resource pools can only be linked to a faction, but in 3K the functionality was expanded dramatically and they can be linked to regions, armies, and more. Whatever game object “has” the pool, “has” the resource.

—- PICTURE OF EXAMPLE 1

As mentioned, pooled resources have been used to create population systems, food. In WH2, it is used for many various odd currencies that some factions have – like Infamy, Skaven food, Imperial Authority, Blood Kisses. In my own usage, I’ve created pooled resources and have made them stand for a gromril resource for the Dwarfs, and I’ve used it for both the “stored lives” mechanic and the “Necromantic Power” mechanic in Return of the Lichemaster.

—- PICTURE OF EXAMPLE 2

The uses of pooled resources are far, in big part due to how absolutely versatile and moddable they are. They can stand for basically any numerical data you want on the campaign – the only limitation being their intrinsic link to factions, rather than other game objects like armies or characters or regions.

Initial Setup

At this point, I’m assuming one of two things – you either know what you want to make and need technical direction, or don’t have any ideas about design and just want to learn the functionality so you can make some designs.

The PR (pooled resource) game object is defined in, kinda obviously, the pooled_resources table. Most of the stuff there is super obvious – keys, localisation, an icon, min/max, and whether or not AI actually thinks about it.

As well, there’s a “default_factor” and a boolean for “persistent_factors”. Which, I guess is a good segue into an important bit of data for pooled resources.

Every pooled resource has at least one “factor”. A factor is just a way to define a source of income/expenditure for each individual pooled resource. A factor, for instance, could be “Battles” or “Buildings”, and you would then link effects or use script commands referencing that factor. It’s a way to inform the player where these +/- values are coming from, and a nice way to keep track of things from the modder side as well.

VERY IMPORTANT TO REALIZE. If you have “persistent_factors” set to false, an effect bundle lasting 5 turns that give +5 PR of a specific factor will give 25 total, 5 each turn. If you have “persistent_factors” set to true, an effect bundle doing the same will give +5 total for 5 turns, and then the PR will drop down by 5 on the sixth turn, as the source of the +5 is gone.

Most PRs should be set up as not-persistent, unless there’s a specific design in mind that requires persistent factors. If a source of a factor goes away, the +value goes away with persistence set. This is very important to realize, I can’t stress that enough.

I recommend creating custom pooled_resource_factors for further control over your own work, but there’s nothing stopping you from using vanilla factors that are used for other PR’s, they won’t overlap at all.

Following the creation of factors, they get linked to a PR in pooled_resource_factor_junctions. Make sure the unique ID used makes sense and is memorable, it’s used in a handful of other spots as we go around.

At this point, it makes sense to give the PR to an actual faction, so let’s do that. The only thing needed is a campaign_group, a linked campaign_group_member, a link between the member and your desired _faction or _subculture or _culture member criteria, and a link between the campaign group and the PR in campaign_group_pooled_resources. This tells the game to allow this faction/group of factions access to the pooled resource, and on game creation, sets the PR to the set initial_amount specified in this table.

— PICTURE OF THE TABLES NEEDED ABOVE

Basically Nothing

Here in the tutorial, you basically have a number counter that does literally nothing.

A logical next step would be to create some UI to start reading the pooled resource, but the concept there is a little more complicated than all that, and UI will be covered towards the end of this cycle. But I don’t want to go on without letting you fool-proof and make sure that the PR is actually setup and working properly.

Using preferably the Lua Console, or non-preferably a regular mod script, one can use a little bit of code to check if the PR exists and, if it does, how much value it has:

local faction = cm:get_faction("faction key")
if faction:has_pooled_resource("pr key") then
    print("Has the PR!")
    local pr_obj = faction:pooled_resource("pr key")
    print(pr_obj:value())
end

If it prints yes and gives you the value you set in the campaign_group_pooled_resources table, everything works!

Do Something

This next part, we’re going to give a PR some actual effects. Or, rather, I’m going to tell you how to do that, if you wanted to. We won’t do it together.

One nice thing about PRs is that they can apply an effect bundle to the faction based on the current value of the PR, and it’s instantaneously altered on any change. This effect bundle can be based on a percentage (ie. 20% full), or a current value (ie. 50 value of this PR), or in a range (ie. you know what a range is, why am I explaining this).

This has to do with another campaign group, and it’s hyper-imperative you don’t mix up this with your previous campaign group. The former one you needed to link the faction/culture/subculture to the PR access, this one is linking the PR itself to some effects. Absolutely CANNOT mix the two.

Again, make your campaign_group and campaign_group_member. Only difference is, you need to make one for effect you want. That is, if you want to have 5 different ranges for effects, you need 5 groups and members; if you want to have 100 different effects, from 1-100, then you need 100 of them.

Firstly, link every one of these members to campaign_group_member_criteria_pooled_resources. That’s telling the game what pooled resource the campaign group is referencing. Then, in campaign_group_member_criteria_numeric_ranges, define between what two values this effect should be applied. Note, they CAN be the same value, so you can do “1-1” “2-2” and so on for the effect to change at every value of PR.

Then we get to the actual effects! In campaign_group_pooled_resource_effects, link the campaign group (not member!) to the desired effect_bundle, with the effect type of either ABSOLUTE_VALUE or PERCENTAGE_OF_CAPACITY. For the least overhead, ABSOLUTE_VALUE makes the most sense 98% of the time.

At this point, we’ll get a faction effect bundle which changes automatically based on the current PR value at any given moment!

Affect Change

This is the part where we start editing the value of the pooled resource.

The only time we’ve actually touched the pooled resource amount is with that initial_amount number earlier in the database. But lucky for you, pooled resource change can come from a few sources. Namely – they can be added with effects, with script, and with resource_costs.

EFFECTS

First up, effects! This one is quite simple. All you need to do is make a new effect, and link it to your pooled resource factor using the table effect_bonus_value_pooled_resource_factor_junctions. Do note this takes the unique_id column from the pooled_resource_factor_junctions table. Depending on the bonus_value_id applied here – base_amount or percentage_multiplier_mod – you can either apply a flat sum, or a % increase of all values applied through this defined factor.

Notably, you can also apply effects via effect_bonus_value_pooled_resource_junctions, to increase the maximum amount, the minimum amount, or a global % modifier for that pooled resource (as opposed to factor-specific, like the above example).

RESOURCE COSTS

A resource_cost is a game object that informs a cost (or sometimes, a profit) attached to other game objects. Specifically, in WH2, resource_costs are used for rituals and for technology, or for post-battle captive options. They’re a one-time payment/cost for doing these actions.

After defining the resource_cost, it can be applied to the pooled_resource in resource_cost_pooled_resource_junctions and the respective rite, technology, or what have you.

Don’t fret if the payment/cost you had in mind isn’t on this small list of resource costs! For there is…

SCRIPTS

There’s only one command needed to alter pooled resources via script, and that’s the following:

cm:faction_add_pooled_resource("faction_key", "pooled_resource_key", "factor_key", quantity)

Where quantity = a +/- number representing the change you want. Do note if the change is greater than the min/max of the factor or the pooled resource, it will only go to that min/max.


And that’s basically it when it comes to the actual functionality of the pooled resource game object! At this point, we have a new conversation – user interface.

UI

The only big downside of pooled resources is their lack of built-in UI. That isn’t to say all pooled resources should be visible, but when you want visibility, it takes a bit more work. There is a db-based way of providing frontend UI, but there are flaws with it – notably, that it only allows for one pooled resource to be displayed per faction.

Thankfully for y’all, I’m going to provide some (rather lengthy) code for a UI script to setup some stuff here.

—- PICTURE OF THE UI SCRIPT IN-GAME

Below, the things you need to change are just early four variables: faction_key, pooled_resource_key, pooled_resource_icon, factors_list. For the last, you have to link the maximum value of the factor (set in pooled_resource_factors) to the factor key. This is due to a current limitation in the scripting interface, it’s the only way I can read the key of a factor object in code.

—- INCLUDE THE UI LAYOUT FILES TOO!

local function add_pr_uic()
    local parent = find_uicomponent(core:get_ui_root(), "layout", "resources_bar", "topbar_list_parent")

    local uic --: CA_UIC
  	local faction_key = "faction_key"
    local pooled_resource_key = "pooled_resource_key"
  	local pooled_resource_icon = "ui/path_to_image/example.png"
  
  	-- link max value to factor key
    local factors_list = {
      [100] = "factor_key", 
      [101] = "factor2_key"
    }

    local function create_uic()
        uic = core:get_or_create_component(pooled_resource_key.."_"..legion, "ui/vandy_lib/pooled_resources/dy_canopic_jars", parent)
        local dummy = core:get_or_create_component("script_dummy", "ui/campaign ui/script_dummy")

        dummy:Adopt(uic:Address())

        -- remove all other children of the parent bar, except for the treasury, so the new PR will be to the right of the treasury holder
        for i = 0, parent:ChildCount() - 1 do
            local child = UIComponent(parent:Find(i))
            if child:Id() ~= "treasury_holder" then
                dummy:Adopt(child:Address())
            end
        end
        
        -- add the PR component!
        parent:Adopt(uic:Address())
    
        -- give the topbar the babies back
        for i = 0, dummy:ChildCount() - 1 do
            local child = UIComponent(dummy:Find(i))
            parent:Adopt(child:Address())
        end
    
        uic:SetInteractive(true)
        uic:SetVisible(true)
    
        local uic_icon = find_uicomponent(uic, "icon")
        uic_icon:SetImagePath(pooled_resource_icon)
        
        uic:SetTooltipText('{{tt:ui/campaign ui/tooltip_pooled_resource_breakdown}}', true)
    end

    local function check_value()
        if uic then
            local pr_obj = cm:get_faction(faction_key):pooled_resource(pooled_resource_key)
            local val = pr_obj:value()
            uic:SetStateText(tostring(val))
        end
    end

    if cm:whose_turn_is_it() == faction_key then
        create_uic()
        check_value()
    end

    local function adjust_tooltip()
        local tooltip = find_uicomponent(core:get_ui_root(), "tooltip_pooled_resource_breakdown")
        tooltip:SetVisible(true)

        print_all_uicomponent_children(tooltip)
    
        local list_parent = find_uicomponent(tooltip, "list_parent")
    
        local title_uic = find_uicomponent(list_parent, "dy_heading_textbox")
        local desc_uic = find_uicomponent(list_parent, "instructions")
    
        local loc_header = "pooled_resources"
        title_uic:SetStateText(effect.get_localised_string(loc_header.."_display_name_"..pooled_resource_key))
        desc_uic:SetStateText(effect.get_localised_string(loc_header.."_description_"..pooled_resource_key))
    
        local positive_list = find_uicomponent(list_parent, "positive_list")
        positive_list:SetVisible(true)
        local positive_list_header = find_uicomponent(positive_list, "list_header")
        positive_list_header:SetStateText(effect.get_localised_string(loc_header.."_positive_factors_display_name_"..pooled_resource_key))
    
        local negative_list = find_uicomponent(list_parent, "negative_list")
        negative_list:SetVisible(true)
        local negative_list_header = find_uicomponent(negative_list, "list_header")
        negative_list_header:SetStateText(effect.get_localised_string(loc_header.."_negative_factors_display_name_"..pooled_resource_key))

        local faction_obj = cm:get_faction(faction_key)
        local pr_obj = faction_obj:pooled_resource(pooled_resource_key)
        local factors_list_obj = pr_obj:factors()
    
        local factors = {} 
        local diff = 0
    
        for i = 0, factors_list_obj:num_items() - 1 do
            local factor = factors_list_obj:item_at(i)
            for num, key in pairs(factors_list) do
                if factor:maximum_value() == num then
                    factors[key] = factor:value()
                    break
                end
            end
            diff = diff + factor:value() -- adds/subtracts to set the "Change This turn" number
        end

        local total = find_child_uicomponent(list_parent, "change_this_turn")
        local total_val = find_child_uicomponent(total, "dy_value")
        if diff < 0 then
            total_val:SetState('0')
        elseif diff == 0 then
            total_val:SetState('1')
        elseif diff > 0 then
            total_val:SetState('2')
        end

        total_val:SetStateText(tostring(diff))

        --v function(key: string, parent: CA_UIC)
        local function new_factor(key, parent)
            local uic_path = "ui/vandy_lib/pooled_resources/"
            local state = ""

            if parent:Id() == "positive_list" then
                uic_path = uic_path .. "positive_list_entry"
                state = "positive"
            elseif parent:Id() == "negative_list" then
                uic_path = uic_path .. "negative_list_entry"
                state = "negative"
            end

            local factor_list = find_uicomponent(parent, "factor_list")

            local factor_entry = core:get_or_create_component(pooled_resource_key..key, uic_path, factor_list)
            factor_list:Adopt(factor_entry:Address())

            factor_entry:SetStateText(effect.get_localised_string("pooled_resource_factors_display_name_"..state.."_"..key))

            local value = factors[key]
            local value_uic = find_uicomponent(factor_entry, "dy_value")

            if state == "positive" then
                -- defaults to grey
                value_uic:SetState('0')
                if value > 0 then
                    value_uic:SetState('1') -- make green
                elseif value < 0 then
                    value_uic:SetState('2') -- make red
                end
            elseif state == "negative" then
                value_uic:SetState('0')
            end

            value_uic:SetStateText(tostring(value))
        end

        for key, value in pairs(factors) do
            if value >= 0 then
                -- positive path
                new_factor(key, positive_list)
            elseif value < 0 then
                -- negative path
                new_factor(key, negative_list)
            end
        end
    end

    core:add_listener(
        pooled_resource_key.."_hovered_on",
        "ComponentMouseOn",
        function(context)
            return uic == UIComponent(context.component)
        end,
        function(context)
            cm:callback(function()
                adjust_tooltip()
            end, 0.1)
        end,
        true
    )

    core:add_listener(
        pooled_resource_key.."_value_changed",
        "PooledResourceEffectChangedEvent",
        function(context)
            return context:faction():name() == faction_key and context:resource():key() == pooled_resource_key and context:faction():is_human() and cm:whose_turn_is_it() == faction_key
        end,
        function(context)
            check_value()
        end,
        true
    )

    core:add_listener(
        pooled_resource_key.."_turn_start",
        "FactionTurnStart",
        function(context)
            return context:faction():name() == faction_key and context:faction():is_human()
        end,
        function(context)
            create_uic()
            check_value()
        end,
        true
    )

    core:add_listener(
        pooled_resource_key.."_faction_turn_end",
        "FactionTurnStart",
        function(context)
            return context:faction():name() ~= faction_key and cm:get_faction(faction_key):is_human()
        end,
        function(context)
            uic:SetVisible(false)
        end,
        true
    )
end

And with that, you can set your own UI on specified factions!


Overview

At this point, all the real functionalities of a pooled resource should be understood. The main thing missing is the actual possibilities. Like I said, I don’t want to be too much design here – more development. A pooled resource can be used as a counter for practically any mechanic that needs a continued, persistent numerical value.

If there are any questions or things that I’m missing, please share in the comment section below!

Leave a Reply

Close Menu