Lua: Dynamically creating special and unique items (Advanced Tutorial)
5 5

5 posts in this topic

Recommended Posts

So I was working on some code for our server mod, and it turned out pretty cool. Since I haven't written a tutorial in a while, I thought I'd share.

The problem: I wanted to create special upgraded items, and unique(ish) ones, without having to define the objects in the media/scripts/*.txt files, or having to mess around with distribution tables and all that nonsense.  The items also had to be able to inherit properties from the base items they were copying.

But, that being said...this tutorial isn't so much about the end goal, but the methods used to accomplish it.

 

Warning: some of the tricks used are semi-advanced and not normally done for sanity purposes.

 

It's useful to be familiar with the InventoryItem class API, and the HandWeapon subclass for this tutorial particularly the various 'get' and 'set' methods, since thats what we'll be using (in a very backwards and dynamic way!)

 

The full code (well, partially edited) for the script is here, but I'll break it down into smaller pieces below:

Spoiler

local UPGRADE_CHANCE = 10 -- 1 in # chance item is upgraded
local SPECIAL_CHANCE = 100000 -- 1 in # chance item is special

local UpgradedItems = {
    ["Base.KitchenKnife"] = {
        {
            absolute = {
                Name = "Fancy Kitchen Knife",
            },
            multiplier = {
                ConditionLowerChance = 2,
            }
        }
    },
    ["Base.Axe"] = {
        {
            absolute = {
                Name = "Hatchet",
                TwoHandWeapon = false,
                OtherHandUse = false,
            },
            multiplier = {
                MinDamage = 0.7,
                MaxDamage = 0.7,
                MaxRange = 0.7,
                MinimumSwingTime = 0.7,
                SwingTime = 0.7,
                Weight = 0.7,
                ActualWeight = 0.7,
            },
        }
    }
}

local SpecialItems = {
    ["Base.KitchenKnife"] = {
        {
            absolute = { 
                Name = "Lorena Bobbitt's Knife",
                CriticalChance = 100,
            },
            multiplier = {
                ConditionLowerChance = 10,
            }
        },
        {
            absolute = { 
                Name = "Jack the Ripper's Knife",
            },
            multiplier = {
                ConditionLowerChance = 10,
                MaxDamage = 1.5,
            },
        },
    },
    ["Base.Axe"] = {
        {
            absolute = { 
                Name = "Lizzy Borden's Axe",
                CriticalChance = 100,
            },
            multiplier = {
                ConditionLowerChance = 4,
            },
        },
        {
            absolute = {
                Name = "Paul Bunyan's Axe",
            },
            multiplier = {
                ConditionLowerChance = 4,
                TreeDamage = 4,
            },
            relative = {
                MaxDamage = 1,
                MinDamage = 1,
            },
        },
    },
    ["ORGM.ColtSAA"] = {
        {
            absolute = {
                Name = "Billy the Kidd's Colt SAA",
                HitChance = 75,
                AimingTime = 5,
            },
        },
    }
}


local upgradeItem = function(item, upgrade)
    if not item or not upgrade then return end
    if upgrade.absolute then
        for key, value in pairs(upgrade.absolute) do
            item["set" .. key](item, value)
        end
    end
    if upgrade.multiplier then
        for key, value in pairs(upgrade.multiplier) do
            item["set" ..key](item, item["get" ..key](item) * value)
        end
    end
    if upgrade.relative then
        for key, value in pairs(upgrade.relative) do
            item["set" ..key](item, item["get" ..key](item) + value)
        end
    end
end

Events.OnFillContainer.Add(function(roomName, containerType, container)
    -- see if any of our items that spawned have possible upgrades
    for itemName, upgrades in pairs(UpgradedItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end
        for i=1,items:size() do repeat
            if ZombRand(UPGRADE_CHANCE) +1 > 1 then break end
            upgradeItem(items:get(i-1), upgrades[ZombRand(#upgrades) + 1])
        until true end
    until true end
    
    -- see if any of our items that spawned have possible special versions
    for itemName, upgrades in pairs(SpecialItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end

        for i=1,items:size() do repeat
            
            if ZombRand(SPECIAL_CHANCE) +1 > 1 then break end
            local item = items:get(i-1)
            if not item then break end
            upgradeItem(item, upgrades[ZombRand(#upgrades) + 1])
            
            -- set condition to max since this is a special
            item:setCondition(item:getConditionMax())
        until true end
    until true end
end)

 

 

Now the first bit kinda speaks for itself:

local UPGRADE_CHANCE = 10 -- 1 in # chance item is upgraded
local SPECIAL_CHANCE = 100000 -- 1 in # chance item is special

The chances that a spawned item will be a 'upgraded version' or a 'special version'.

 

So first we need to create some tables holding the new stats for our upgraded items. For this example, all the items are weapons but any item will work:

local UpgradedItems = {
    ["Base.KitchenKnife"] = {
        {
            absolute = {
                Name = "Fancy Kitchen Knife",
            },
            multiplier = {
                ConditionLowerChance = 2,
            }
        }
    },
    ["Base.Axe"] = {
        {
            absolute = {
                Name = "Hatchet",
                TwoHandWeapon = false,
                OtherHandUse = false,
            },
            multiplier = {
                MinDamage = 0.7,
                MaxDamage = 0.7,
                MaxRange = 0.7,
                MinimumSwingTime = 0.7,
                SwingTime = 0.7,
                Weight = 0.7,
                ActualWeight = 0.7,
            },
        }
    }
}

local SpecialItems = {
    ["Base.KitchenKnife"] = {
        {
            absolute = { 
                Name = "Lorena Bobbitt's Knife",
                CriticalChance = 100,
            },
            multiplier = {
                ConditionLowerChance = 10,
            }
        },
        {
            absolute = { 
                Name = "Jack the Ripper's Knife",
            },
            multiplier = {
                ConditionLowerChance = 10,
                MaxDamage = 1.5,
            },
        },
    },
    ["Base.Axe"] = {
        {
            absolute = { 
                Name = "Lizzy Borden's Axe",
                CriticalChance = 100,
            },
            multiplier = {
                ConditionLowerChance = 4,
            },
        },
        {
            absolute = {
                Name = "Paul Bunyan's Axe",
            },
            multiplier = {
                ConditionLowerChance = 4,
                TreeDamage = 4,
            },
            relative = {
                MaxDamage = 1,
                MinDamage = 1,
            },
        },
    },
    ["ORGM.ColtSAA"] = {
        {
            absolute = {
                Name = "Billy the Kidd's Colt SAA",
                HitChance = 75,
                AimingTime = 5,
            },
        },
    }
}

For those UpgradedItems and SpecialItems tables, the key/value pairs are 'the full name of the item', and the value is another table. That table contains a second layer of tables: each possible upgrade.

 

Take a look at the SpecialItem one for "Paul Bunyan's Axe"

        {
            absolute = {
                Name = "Paul Bunyan's Axe",
            },
            multiplier = {
                ConditionLowerChance = 4,
                TreeDamage = 4,
            },
            relative = {
                MaxDamage = 1,
                MinDamage = 1,
            },
        },

It contains all 3 sections (the other items only contain 2 sections each), 'absolute', 'multiplier', and 'relative'.  Absolute sets a value to a specific value. Multiplier takes a items current value, and multiplies it.  Relative takes a items current value, and adds onto it.

 

Now, the function that does the actual item upgrade:

local upgradeItem = function(item, upgrade)
    if not item or not upgrade then return end
    if upgrade.absolute then
        for key, value in pairs(upgrade.absolute) do
            item["set" .. key](item, value)
        end
    end
    if upgrade.multiplier then
        for key, value in pairs(upgrade.multiplier) do
            item["set" ..key](item, item["get" ..key](item) * value)
        end
    end
    if upgrade.relative then
        for key, value in pairs(upgrade.relative) do
            item["set" ..key](item, item["get" ..key](item) + value)
        end
    end
end

This function takes 2 arguments, the item to be upgraded, and the upgrade data. This is the part that calls the InventoryItem and HandWeapon methods, but you'll notice there's no actual method calls hardcoded in there such as item:setName(value) or item:setConditionLowerChance(value)!

Instead, we're treating these items as actual lua tables, and building the method names dynamically from strings:

item["set" .. key](item, value)

so when the key is "Name" and the value is "Paul Bunyan's Axe", its checking the 'item' table for 'setName', and treating the returned value as a function, passing it 2 arguments: the item itself, and "Paul Bunyan's Axe". This is identical to:

item:setName("Paul Bunyan's Axe")

If the key is "ConditionLowerChance" and the multiplier value is 4, then this:

item["set" ..key](item, item["get" ..key](item) * value)

becomes this:

item:setConditionLowerChance(item:getConditionLowerChance() * 4)

I should mention at this point: this is a completely backwards way of doing it and generally not advisable. You can drive yourself nuts calling methods dynamically this way when you need to debug a error.  BUT it is completely valid way of dynamically deciding on method calls using strings, instead of having a massive pile of if/elseif conditions.

 

 

So now we have a function that can create our upgraded/special items from normal ones, and tables holding our upgrade data, now for the final part: deciding on when to upgrade:

Events.OnFillContainer.Add(function(roomName, containerType, container)
    -- see if any of our items that spawned have possible upgrades
    for itemName, upgrades in pairs(UpgradedItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end
        for i=1,items:size() do repeat
            if ZombRand(UPGRADE_CHANCE) +1 > 1 then break end
            upgradeItem(items:get(i-1), upgrades[ZombRand(#upgrades) + 1])
        until true end
    until true end
    
    -- see if any of our items that spawned have possible special versions
    for itemName, upgrades in pairs(SpecialItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end

        for i=1,items:size() do repeat
            
            if ZombRand(SPECIAL_CHANCE) +1 > 1 then break end
            local item = items:get(i-1)
            if not item then break end
            upgradeItem(item, upgrades[ZombRand(#upgrades) + 1])
            
            -- set condition to max since this is a special
            item:setCondition(item:getConditionMax())
        until true end
    until true end
end)

We simply hook the OnFillContainer event. This event is triggered AFTER the base files have filled out items in a container, allowing us to see what spawned.  You'll notice I use a funky way of looping through the upgrade tables here:

    for itemName, upgrades in pairs(UpgradedItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end
        ...
    until true end

That snippit above is technically a double loop, the 'for .. end', and the 'repeat ... until true' part. Why would I do such a thing? While lua doesn't have a keyword 'continue' for skipping the rest of the code and jumping to the next repeat of the loop. All you can do is 'break' out of a loop. By using this funny loop syntax, we can use 'break' as a skip: it breaks out of the 'repeat .. until ..' part, and goes straight to the part of the 'for ... end' loop. Doing this cuts down on a pile of nested 'if' statements. For example if I wrote the first 'for' loop normally:

    -- for .. do repeat .... until .. end 
    for itemName, upgrades in pairs(UpgradedItems) do repeat
        local items = container:FindAll(itemName)
        if not items then break end
        for i=1,items:size() do repeat
            if ZombRand(UPGRADE_CHANCE) +1 > 1 then break end
            upgradeItem(items:get(i-1), upgrades[ZombRand(#upgrades) + 1])
        until true end
    until true end

    -- for .. do .... end
    for itemName, upgrades in pairs(UpgradedItems) do
        local items = container:FindAll(itemName)
        if items then
            for i=1,items:size() do
                if ZombRand(UPGRADE_CHANCE) +1 == 1 then
                    upgradeItem(items:get(i-1), upgrades[ZombRand(#upgrades) + 1])
                end
            end
        end
    end

These are small loops without too many nests, so its not overly a big deal here, but its a handy trick to know that many people probably aren't aware of.

 

But there we go, dynamically created special items that spawn without editing the script files or distribution tables, and inherit values from the base items. Hopefully if actually read all of that you learned a trick or two. ;)

Plus, how can you go wrong when you see this as a characters 'favorite weapon':

 

1.thumb.jpg.bd089f263dd5a67f6cb8d66685633b53.jpg

 

:lol:

 

Share this post


Link to post
Share on other sites
Just now, Sparrow said:

Can the item sprite change as well with the upgrade?

sure, the HandWeapon class has a setWeaponSprite() method. One of the 'special items' for baseball bats (not in the tutorial above) is using it:

    {
        absolute = {
            Name = "Addy Carver's Z Whacker",
            WeaponSprite = "BaseballbatSpiked",
        },
        multiplier = {
            CriticalChance = 4,
            MaxDamage = 1.5,
            MinDamage = 1.5,
            SwingTime = 0.8,
            MinimumSwingTime = 0.8,
            ConditionLowerChance = 10,
            Weight = 0.7,
            ActualWeight = 0.7,
        }
    },

Note the original item is a normal baseball bat, not a spiked version. This replaces the sprite to use the spiked variation.

 

Share this post


Link to post
Share on other sites

I should actually post a 'somewhat' related but slightly different tutorial here (instead of making a new thread)

 

How to edit the durability of all weapons, without having to redefine the items in the media/scripts/*.txt files

This is really quite simple.

local DurabilityFixTable = {
    "Base.Axe",
    "Base.BaseballBat",
    "Base.BaseballBatNails",
    "Base.ButterKnife",
    "Base.Hammer",
    "Base.KitchenKnife",
    "Base.Poolcue",
    "Base.Screwdriver",
    "Base.Sledgehammer",
    "Base.AxeStone",
    "Base.HuntingKnife",
    "Base.IcePick",
    "Base.LetterOpener",
    "Base.Crowbar",
    -- add more extra weapons as needed....
}

for _, item in ipairs(DurabilityFixTable) do
    local scriptItem = getScriptManager():FindItem(item)
    if scriptItem then
        scriptItem:setConditionLowerChance(scriptItem:getConditionLowerChance() * 2)
    end
end

That's it!  All weapons have less chance of lowering condition (effectively twice as strong), by directly manipulating the script items.

 

Share this post


Link to post
Share on other sites

Thank you for sharing this. Now I'm wondering instead of upgrading the items,  is it possible to switch/replace the items with another vanilla items instead? Like for example when a butter knife is spawned, is it possible to make it so that there's 10% chance that it will be replaced by jar lids instead? Or when a Farming for Beginner book is spawned is it possible to make it so there is 5% chance it will be replaced by Herbalist magazine?

 

And regarding the weapons durability, is it possible to adapt it to let's say the vehicle parts durability? For example to change the durability of all vehicles when going off road or just the general durability condition like increasing the speed check from 10mph to 30mph etc.

 

I have zero knowledge of Lua.  I can read your code and guess what it does but have no idea how to write one myself. ;p

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
5 5