Fenris_Wolf Posted February 20, 2018 Share Posted February 20, 2018 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': Dr_Cox1911, ElectricLimbo83 and Sparrow 3 Link to comment Share on other sites More sharing options...
Sparrow Posted February 20, 2018 Share Posted February 20, 2018 Can the item sprite change as well with the upgrade? Link to comment Share on other sites More sharing options...
Fenris_Wolf Posted February 20, 2018 Author Share Posted February 20, 2018 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. Sparrow 1 Link to comment Share on other sites More sharing options...
Fenris_Wolf Posted February 20, 2018 Author Share Posted February 20, 2018 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. GoodOldLeon 1 Link to comment Share on other sites More sharing options...
oi__io Posted August 5, 2018 Share Posted August 5, 2018 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 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now