Modding:Tutorial/Custom Items
From DoomRL Wiki
In the following tutorial you will learn the basics of item objects. Unique to item objects is the object's type, which carries with it a number of different prototype keys, engine hooks, and propertes. We will learn about each item type and what can be done with them to fit your particular item needs. In some cases items will appear roughly indistinguishable from cells: the defining factor here is that they can both exist on the same tile, whereas two cells or two items cannot.
Contents |
Base Prototype
Although this was more-or-less explained in the Game Objects tutorial, we will further explore each key in the base prototype.
Items{ name = "an item", --required field id = "generic", --defaults to 'name' sprite = 0, --required field; always set to 0 for now overlay = 0, --defaults to 0 color = WHITE, --defaults to LIGHTGRAY level = 1, --required field weight = 1000, --required field flags = {}, --default depends on item type set = "", --defaults to "" firstmsg = "You got an item!", --defaults to "" color_id = "generic", --defaults to 'id' res_bullet = 0, --defaults to 0 res_melee = 0, --defaults to 0 res_shrapnel = 0, --defaults to 0 res_acid = 0, --defaults to 0 res_fire = 0, --defaults to 0 res_plasma = 0, --defaults to 0 type = ITEMTYPE_NONE --required field ascii = "?" --default depends on item type }
- name is what the item appears to be in-game (e.g. using the 'look' command).
- id is the item identifier, to be used in lua whenever you want to call the item prototype.
- sprite is what will eventually be the graphical tile of the item.
- overlay, like sprite, is based on there being graphical tiles in DoomRL. You can ignore this key entirely for the time being.
- color is one of 16 (4-bit) colors that you can use to distinguish the item on the map. For items that can be picked up, this is also the color of the item's name in your inventory/equipment screens.
- level is the minimum level that the item can appear on, for random item generation purposes.
- weight this affects the frequency that the item will appear, for random item generation purposes. Both level and weight are only important on levels that randomly generate items using Level.flood_items().
- flags defines what item flags you give your item. It is recommended that you figure out your item's type before adding flags, as some are only important for certain types.
- set defines what item set the item is in. For instance, the gothic armor/boots and phaseshift armor/boots are part of item sets. Item sets themselves are separate objects that must be defined before any item objects that use them.
- firstmsg is what will appear in the message area the very first time any item from this object prototype is picked up.
- color_id sets the id of the item for color-binding purposes. DoomRL levers, for instance, are all set to the same color_id, so that they cannot be disguished by a clever player manipulating color.lua carefully.
- res_[damage_type] sets the resistance for a particular damage type onto the item. This is only important for weapons, armor, and boots, as the resistance can only help if the item is equipped.
- type is, quite possibly, the most important key for an item: it sets the item's type, which then determines what additional keys are required/allowed, what engine hooks it can use, and what properties it has. Each type will be explained over the course of the tutorial.
- ascii is the character that is used for the item on the map. Although this isn't explicitly a part of the base prototype, it exists across all item types, the only difference being its default character (which will be included with each type in this tutorial).
Item types are always written as ITEMTYPE_[type], where [type] is the actual type of the item. A shorthand o this form (e.g., "_RANGED") will be used throughout the tutorial.
Generally speaking, there are two practical groups by which item types can be categorized: inventory items and map items. Inventory items are anything that can be picked up and carried with you, while map items cannot. This categorization is used mostly for the convenience of this tutorial, as it is easier to find the item type you are looking for based on whether or not the item goes into your inventory.
Map Items
All items, both map and inventory, come with two properties and two engine hooks:
- Prototype Keys:
- itype is the item type in property form. As _NONE, an item cannot be picked up or used.
- proto is the prototype of the instantiated item.
- Engine hooks:
- OnCreate() triggers whenever the item is created. Its primary function is to add properties to an item they may otherwise change from instance to instance.
- OnEnter() triggers whenever a being enters the same tile as the item. Although technically it can be used for any item, it is most known for its coupling with the teleporter item type.
Map types have fewer properties and hooks associated with them, but they are by no means difficult to work with.
Deadweight (_NONE)
Deadweight items are, quite literally, placeholders. They serve little purpose other than display or preventing another item from being dropped onto its tile. It is technically possible to change a deadweight item that exists on the map into a different type, at which point other properties could be applied to it and would act similarly to another item type: however, with the engine hooks supplied in those other item types, there is almost never a need to do this.
The following is an example of a deadweight item:
Items{ name = "moss" sprite = 0, color = GREEN ascii = "~" level = 1, weight = 0, -- flags = {IF_NODESTROY, IF_NUKERESIST}, type = ITEMTYPE_NONE, function OnEnter(_,being) being.scount = being.scount - 100 end, }
Since we don't want this as a part of the items randomly generated, the weight is set to zero. The two flags, IF_NODESTROY (prevents item destruction by splash-damage explosions) and IF_NUKERESIST (prevents item destruction by nuclear explosion) are about the most use you could get out of a deadweight item, essentially allowing the it to stay put regardless of what the player may throw at it. (In the case of moss, however, these flags are unwanted, so it is commented out.) Finally, we use an OnEnter() hook so that anything that enters a tile with moss effectively takes a bit longer to move in it. The property "scount" is measured such that 1 turn = 100 scounts, so this would add an additional 1 turn to any move into moss.
Teleporter (_TELE)
Teleporter items are almost identical to deadweight items, except that they must define an OnEnter() hook, since it is so pivotal in their function. Teleporters use '*' as their default ASCII character.
In the case of DoomRL's base game, teleporter items are given an extra property using the OnCreate() hook that defines a particular coordinate on the map. This coordinate is then used during OnEnter() to teleport a being that enters the teleporter. Since we want the location to be different for every teleporter, this is why we don't have a "location" prototype key and, instead, have to alter its properties. A basic example is given below:
Items{ name = "teleporter", id = "port", sprite = 0, color = LIGHTBLUE, level = 1, weight = 0, type = ITEMTYPE_TELE, function OnCreate(self) local location = coord.random(area.get(area.FULL)) self:add_property("exit_pt",location) end, function OnEnter(_,being) being.displace(exit_pt) end }
- First, we use OnCreate() to determine a random location. This is done by using coord.random(), which returns a random coordinate between the two given coordinates. area.get() returns the upper-left and bottom-right coordinates of a given area: by selecting area.FULL (which is a pre-defined area that covers the entire map), we return the boundary coordinates of the map, which are then called into coord.random() to give us a random coordinate anywhere on the map.
- After we grab this location, we use thing:add_property() which takes in the key of the property to be added and the value it should be given. For our cases, we make a key called "exit_pt" (exit point) and set its value to the random coordinate we found.
- Finally, we use the function thing.displace() to move the being, and place it within OnEnter() so that it will occur whenever the being enters the same tile as the teleporter item.
This example doesn't handle problems such as locations that exist in a cell that blocks movement, however, so it should be improved upon if you want a more useful random teleporter.
Naturally, teleporter items don't need to be used as teleporters only: they are only named as such because of their unique use in the main game of DoomRL.
Powerup (_POWER)
Powerup items (or powerups) use a required OnPickup() hook in order to produce the result as seen in-game. They are meant to be consumed on use, and OnPickup() automatically takes care of this consideration. Powerups use '^' as their default ASCII character.
Here is a quick example of a powerup that makes use of the envirosuit pack's effect:
Items{ name = "envirosuit" sprite = 0, color = LIGHTGRAY, level = 9, weight = 200, type = ITEMTYPE_POWER, function OnPickup() ui.msg("You feel protected!") player:set_affect(STATUSGREEN,100) end, }
This is about as basic as you can get, using an almost-minimal number of prototype keys. the OnPickup() function displays a message identical to the envirosuit pack's, and then the player is given the enviro status with player:set_affect(status,duration). Note that duration is based on how many actions the player takes, not how many scounts/turns/seconds (that is, affects work on those on OnAction() hook).
Lever (_LEVER)
Levers are map items that come with the OnUse() and OnUseCheck() hooks, which serve as their primary function. In the main game, they are randomly strewn throughout the game in lever rooms, and there are a few specially-crafted ones in special levels. Levers use '&' as their default ASCII character.
Levers come with the following additional prototype keys:
Items{ .... type = ITEMTYPE_LEVER, --required type for a lever good = "neutral", --default is "" desc = "thermostat", --default is "" soundID = "lever", --default is "lever" fullchance = 10, --default is 0 warning = "This place looks fully air-conditioned." --default is "" }
- good is what what the lever shows in parentheses for a player with BF_LEVERSENSE1 (equivalent to having one rank in Intuition). By convention, this is "beneficial", "neutral", or "dangerous".
- desc is like good, but only displays for a player with BF_LEVERSENSE2 (equivalent to having two tanks in Intuition).
- soundID is the sound binding for the lever, which plays the sound automatically during OnUse().
- fullchance is the chance that, when randomly generated, the lever's effect will trigger across the entire map. This is mostly useless to modders, as the generation that creates levers does not allow for custom levers to be added to it.
- warning is the game message that appears at the start of a level with this lever if fullchance is true.
Some levers, by convention, are also given an extra property that references a particular area of the map, so that their effect can know where to get the job done. For levers such as those that remove walls, add a fluid, or hurt monsters, this is the property they use in order for their effect to work properly, and it is these levers that make use of the fullchance and warning fields. The generator itself has an algorithm to find rooms and return its area, but we can also define much simpler areas, as shown below:
Items{ name = "lever", id = "lever_gift_drop", color_id = "lever", level = 13, weight = 50, type = ITEMTYPE_LEVER, good = "neutral", desc = "drops random items", soundID = "lever", function OnCreate(self) local location = area.around(self:get_position(),1) self:add_property(drop_pt,location) self:add_property(times_used,"0") self:add_property(total_use,math.random(3)) end, function OnUseCheck(self) if self.times_used == self.total_use then ui.msg("Nothing happens.") return(false) end self.times_used = self.times_used + 1 return(true) end, function OnUse() ui.msg("An item materializes!") item = table.random_pick{"lmed","epack","pammo","pshell","procket","pcell"} Level.area_drop(self.drop_pt,item) end, }
Levers aren't complicated but they tend to have a number of steps, so we'll go through each piece separately:
- OnCreate() adds three properties to the lever: the target area (drop_pt), the number of times it can be used (total_use), and the number of times it has already been used (times_used). The target area is based on its surroundings using area.around(), which takes in a starting-point coordinate (found by thing:get_position()) and a number that corresponds to how many tiles around the coordinate you want to include. Our location is a 3x3 area centered about the lever, which we add to drop_pt.
- OnUseCheck() is how we determine if the lever can be used again, by comparing times_used to total_use. The hook itself requires a boolean return value: if true, then OnUse() can be called, if false, OnUse() is ignored when you try it use it. times_used starts at 0, but each successful OnUseCheck() increments it by one. Once it is equal to total_use, OnUseCheck() will return a false value, resulting in the lever doing nothing.
- OnUse() performs our lever's action. First we display the message that the lever has, indeed, done something. Then we randomly choose from a few item identifiers (specifically we choose from a large med-pack, envirosuit pack, and all four of the ammo packs). Finally, the chosen item is dropped into the area we designated through drop_pt.
The result of this lever is to drop an random (and pretty good) item each time it is used, and can be pulled anywhere from one to three times. (I can't say I wouldn't be happy to see such a lever in the real game!)
Inventory Items
Inventory items, in addition to the keys and hooks mentioned with map items, has the desc prototype key, which is a required string that describes the item in the player's inventory.
The more significant items are inventory items, most importantly equipment. There is, however, a lot to keep track of when creating these items.
Consumable (_PACK)
Consumable items are added into the inventory and directly used from it some number of times. In the base game, there are either items that are used once (at which point they are consumed), or can be used any number of times so long as the conditions are correct. Consumables use '+' as the default ASCII character.
Consumables must include a 'desc' prototype field that describes the item in the inventory, as well as an OnUse() hook by which the item can be used. However, they can also use the OnUseCheck(), OnPickup(), OnFirstPickup(), and OnPickupCheck() hooks if need be. The following example includes most of these hooks:
Items{ name = "holy cross", id = "hcross", level = 22, weight = 0, color = WHITE, type = ITEMTYPE_PACK, function OnPickupCheck() wpn_check = player.eq[weapon] == "spear" armr_check = player.eq[torso] == "aarmor" if not (wpn_check and armr_check) then ui.msg("You must prove yourself worthy of using this!") return(false) else return(true) end end, function OnFirstPickup() ui.msg("You hear angels singing!" player.hp = player.hpmax * 2 player.tired = false end, function OnUseCheck() if player.tired == false ui.msg("You are too tired to use this.") return(false) else return(true) end end, function OnUse() ui.msg("You are flooded with holy energy!" player.hp = player.hpmax * 2 player:set_affect(STATUSINVERT,20) player.tired = true return(false) end, }
- The first hook, OnPickupCheck(), checks to see whether or not the item can picked up at all. The conditions use for this are that the player is holding the Longinus Spear (spear) in their weapon slot and the Angelic Armor (aarmor) in their torso slot. Quite a hefty requirement! If these conditions are not met, the game gives you a vague message regarding your failure.
- The second hook, OnFirstPickup(), activates only the very first time you pick up the item. In fact, this hook is only called the first time you pick up any item with this prototype (this is why chainsaw gives you a berserk effect in the Chained Court but not again if you happen to be lucky enough to find a second one). On this first pickup, we get another message, a supercharge-like health effect, and the player's tactics are reset.
- The third hook, OnUseCheck(), determines whether or not the item can be used from the inventory by checking to see if the player's tactics are on 'cautious' (this is the false value for tired). If so, the player will use the item: otherwise, it will let the player know why.
- The final hook, OnUse(), activates the item's effect: another supercharge in health, and invincibility for 20 actions. Upon doing so, the player's tactics are set to 'tired' (this is the true value). Finally, as OnUse() takes a boolean result to see whether or not the consumable is actually consumed, we set it to false: this means that the item will stay in the player's inventory and can be used whenever OnUseCheck() will return a true value. (Basically, it's like the Arena Master's Staff but with a more useful ability.)
Such items in the base game have been coined "Relics", and there's a reason they are so rare. (Though none are quite THIS good.)
Ammunition (_AMMO) and Ammo Pack (_AMMOPACK)
Ammunition is an inventory item that automatically adds itself to weapons as necessary. (See Ranged Weapon for details regarding how this is accomplished.) They are a fairly simple item that uses a few extra prototype keys in order to keep track of the numbers regarding its capacity. Ammunition uses '|' as the default ASCII character.
Ammo packs are similar to ammunition, except that their function is different and twofold: it can be allocated to the prepared slot and used to reload ammo from there, and they can be unloaded in order to gain ammunition items. In the game, they are very similar, but for the purpose of modding, they can be quite different depending on your needs. An ammo pack uses '!' as this default ASCII character.
Ammunition and ammo packs carry the following additional prototype keys:
Items{ .... type = ITEMTYPE_AMMO, --use this or ITEMTYPE_AMMOPACK ammo = 50, --required field ammomax = 100, --required field ammoID = "shell", --required field (ammo packs only) }
- ammo is how much ammo the ammo or ammo pack holds when it drops randomly. In the base game, this is adjusted by the game's difficulty.
- ammomax is how much ammo can fit into a single ammo or ammo pack item. For ammo packs, this should be equal to ammo as they cannot be added to.
- ammoID is the kind of ammo that the ammo pack uses, for reloading and unloading purposes. Only include this key with ammo packs.
There isn't a lot you can do with ammunition other than create the necessary rounds for any custom weapons. You will be able to copy and paste a basic ammo prototype, for the most part:
Items{ name = "some kinda ammo", id = "anammo", sprite = 0, level = 1, weight = 1000, desc = "This is just ammo. Hurry up and reload your weapon!" type = ITEMTYPE_AMMO, ammo = 20, ammomax = 100, }
Ammo packs, on the other hand, can be customized a little bit if you want to be creative. Since they can be equipped, you can technically make use of the resistance prototype keys to grant extra resistance for equipping an ammo pack.
Armor (_ARMOR) and Boots (_BOOTS)
Melee Weapon (_MELEE)
Ranged Weapon (_RANGED) and Natural Ranged Weapon (_NRANGED)
_ARMOR uses '[' _BOOTS uses ';' _AMMO uses '|' _AMMOPACK uses '!' _RANGED uses '}' _NRANGED uses '?' _MELEE uses '\'