Difference between revisions of "Modding:Tutorial/Custom Items"
From DoomRL Wiki
Game Hunter (Talk | contribs) (started a bit) |
Game Hunter (Talk | contribs) m (→Weapon (_MELEE, _RANGED _NRANGED): syntax fix) |
||
(13 intermediate revisions by one user not shown) | |||
Line 1: | Line 1: | ||
In the following tutorial you will learn the basics of [[Modding:Item|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. | In the following tutorial you will learn the basics of [[Modding:Item|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. | ||
+ | |||
+ | 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. | ||
==Base Prototype== | ==Base Prototype== | ||
Line 29: | Line 31: | ||
*''name'' is what the item appears to be in-game (e.g. using the 'look' command). | *''name'' is what the item appears to be in-game (e.g. using the 'look' command). | ||
− | *''id'' is | + | *''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. | *''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. | *''overlay'', like ''sprite'', is based on there being graphical tiles in DoomRL. You can ignore this key entirely for the time being. | ||
Line 39: | Line 41: | ||
*''firstmsg'' is what will appear in the message area the very first time any item from this object prototype is picked up. | *''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. | *''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. | + | *''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. (Note that DAMAGE_IGNOREARMOR has no resistance.) |
− | *''type'' is, quite possibly, the most important key for an item | + | *''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). | *''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 | + | 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. |
− | == | + | ==Base Properties== |
− | + | Items only have two properties that coincide with all item types: | |
− | *''itype'' is the item type in property form. | + | *''itype'' is the item type in property form, and specifies what can and can't be done with the item. For instance, with type _NONE, an item cannot be picked up or used. |
*''proto'' is the prototype of the instantiated item. | *''proto'' is the prototype of the instantiated item. | ||
− | + | However, items also have all of the properties that [[Modding:Thing|thing objects]] do, since the item object is a subclass of things. This means that they come with a bunch of other properties you can use. (Some of the thing properties are readonly, meaning they can't be changed once the instantiated object is initialized. These will be bolded for clarification.) | |
− | + | *The following properties are the same as the prototype keys: | |
+ | **''color'' | ||
+ | **'''''id''''' | ||
+ | **''name'' | ||
+ | **''sprite'' | ||
+ | **''res_bullet'' | ||
+ | **''res_shrapnel'' | ||
+ | **''res_melee'' | ||
+ | **''res_acid'' | ||
+ | **''res_fire'' | ||
+ | **''res_plasma'' | ||
+ | *The following properties have a different name than a prototype key, but function exactly the same as the key: | ||
+ | **''picture'' is the property of ''ascii'' | ||
+ | *The following properties are automatically given by default: | ||
+ | **''nameplural'' is the name of items when a plural amount is described. In the case of items, the ''name'' is always appended with an 's' for ''nameplural''. | ||
+ | **'''''sid''''' is a string identifier, similar to '''''id'''''. This is what is used dominantly by modders, and is set based on the item's ''id'' key. | ||
+ | **'''''uid''''' is a unique identifier across all instantiated objects. This is often used to save the state of a game. | ||
+ | **'''''x''''' is the x-coordinate of the item on the map. | ||
+ | **'''''y''''' is the y-coordinate of the item on the map. '''''x''''' and '''''y''''' make up an item's [[Modding:Coord|coordinate]] indirectly. | ||
+ | **'''''__ptr''''' is a pointer to the object in the engine: you'll never have to worry about this property. | ||
− | == | + | ==Engine Hooks== |
− | + | All items can use two engine hooks: | |
− | + | *'''OnCreate'''(''item'') triggers whenever ''item'' is allocated to memory. Its primary function is to add properties to an item they may otherwise change from instance to instance. This will also trigger if ''item'' is created but not yet placed on the map. | |
+ | *'''OnEnter'''(''item'',''being'') triggers whenever ''being'' enters the same tile as ''item''. Although technically it can be used with any item, it is most known for its use with the teleporter item type. | ||
+ | |||
+ | All other hooks (and there are many) are dependent on the item's type. Whenever a new hook is introduced, it will be explained thoroughly: when another item type also uses that hook, the name of the hook will be given but not explained. | ||
+ | |||
+ | ==Map Items== | ||
+ | Map items have fewer properties and hooks associated with them, but they are by no means any less difficult to work with. However, these items tend to be used as a part of the map, rather than a part of the player's resources, hence their name. Often they can be specifically designed for a particular map, accompanied by script from the level's own hooks that trigger when the item itself triggers. | ||
+ | |||
+ | ===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: | ||
<source lang="lua"> | <source lang="lua"> | ||
Items{ | Items{ | ||
− | name = " | + | name = "moss" |
− | + | ||
sprite = 0, | sprite = 0, | ||
− | color = | + | color = GREEN |
+ | ascii = "~" | ||
+ | level = 1, | ||
weight = 0, | weight = 0, | ||
− | type = | + | -- flags = {IF_NODESTROY, IF_NUKERESIST}, |
− | function OnCreate(self) | + | type = ITEMTYPE_NONE, |
+ | |||
+ | OnEnter = function(_,being) | ||
+ | being.scount = being.scount - 100 | ||
+ | end, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | name = "teleporter", | ||
+ | id = "port", | ||
+ | sprite = 0, | ||
+ | color = LIGHTBLUE, | ||
+ | level = 1, | ||
+ | weight = 0, | ||
+ | type = ITEMTYPE_TELE, | ||
+ | |||
+ | OnCreate = function(self) | ||
local location = coord.random(area.get(area.FULL)) | local location = coord.random(area.get(area.FULL)) | ||
self:add_property("exit_pt",location) | self:add_property("exit_pt",location) | ||
end, | end, | ||
− | + | OnEnter = function(_,being) | |
being.displace(exit_pt) | being.displace(exit_pt) | ||
end | end | ||
Line 79: | Line 137: | ||
</source> | </source> | ||
− | 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. | + | *First, we use '''OnCreate()''' to determine a random location. This is done by using the '''coord.random()''' method, 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 the '''thing:add_property()''' method, 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 method '''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) come with an extra engine hook: | ||
+ | *'''OnPickup'''(''item'',''player'') triggers whenever ''item'' is picked up using the "get" command by ''player''. Technically the second argument can work with any being, but players are the only being that actually pick up powerup items. | ||
+ | |||
+ | '''OnPickup()''' is a required hook for powerups, used 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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | name = "envirosuit" | ||
+ | sprite = 0, | ||
+ | color = LIGHTGRAY, | ||
+ | level = 9, | ||
+ | weight = 200, | ||
+ | type = ITEMTYPE_POWER, | ||
+ | |||
+ | OnPickup = function() | ||
+ | ui.msg("You feel protected!") | ||
+ | player:set_affect(STATUSGREEN,100) | ||
+ | end, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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 the '''player:set_affect'''(''status'',''duration'') method (and receives a message using the '''ui.msg()''' method). Note that ''duration'' is based on how many actions the player takes, not how many scounts/turns/seconds (that is, affects work using an '''OnAction()''' hook). | ||
+ | |||
+ | ===Lever (_LEVER)=== | ||
+ | Levers are map items that come with two more engine hooks: | ||
+ | *'''OnUse'''(''item'',''being'') is triggered whenever ''item'' is used by ''being''. It returns a boolean output that determines whether or not ''item'' is "consumed" (that is, no longer exists) after use. (defaults to false). | ||
+ | *'''OnUseCheck'''(''item'',''being'') is triggered whenever ''item'' is used by ''being'' but before '''OnUse()'''. Its boolean return output determines whether or not '''OnUse()''' itself triggers, which includes using ''item'' at all (and is required in the script). | ||
+ | |||
+ | These hooks serve as the lever's 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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | .... | ||
+ | type = ITEMTYPE_LEVER, --required type for a lever | ||
+ | good = "neutral", --defaults to "" | ||
+ | desc = "thermostat", --defaults to "" | ||
+ | soundID = "lever", --defaults to "lever" | ||
+ | fullchance = 10, --defaults to 0 | ||
+ | warning = "This place looks fully air-conditioned." --defaults to "" | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | *''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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | 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", | ||
+ | |||
+ | OnCreate = function(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, | ||
+ | |||
+ | OnUseCheck = function(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, | ||
+ | |||
+ | OnUse = function() | ||
+ | ui.msg("An item materializes!") | ||
+ | item = table.random_pick{"lmed","epack","pammo","pshell","procket","pcell"} | ||
+ | Level.area_drop(self.drop_pt,item) | ||
+ | end, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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 uses its output to communicate this: 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 on the sidebar, and the ''desc'' property, which is the name of the item in the inventory (e.g., modified chaingun (1d7x6) [40/40] (P1F1)). | ||
+ | |||
+ | 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. It also comes with the following engine hooks: | ||
+ | |||
+ | *'''OnUse'''(''item'',''being'') | ||
+ | *'''OnUseCheck'''(''item'',''being'') | ||
+ | *'''OnPickup'''(''item'',''player'') | ||
+ | *'''OnFirstPickup'''(''item'',''being'') triggers the first time any instance belonging to prototype ''item'' is picked up by ''being''. Any subsequent pickups by ''being'', whether ''item'' is the same instance or different, will not trigger this hook. | ||
+ | *'''OnPickupCheck'''(''item'',''being'') triggers whenever ''item'' is picked up by ''being'' but before '''OnPickup()''' and '''OnFirstPickup()'''. Its boolean return output determines whether or not the aforementioned hooks also trigger, which includes picking up ''item'' at all (and is required in the script). | ||
+ | |||
+ | The following example includes most of these hooks in action: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | name = "holy cross", | ||
+ | id = "hcross", | ||
+ | level = 22, | ||
+ | weight = 0, | ||
+ | color = WHITE, | ||
+ | type = ITEMTYPE_PACK, | ||
+ | |||
+ | OnPickupCheck = function() | ||
+ | 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, | ||
+ | |||
+ | OnFirstPickup = function() | ||
+ | ui.msg("You hear angels singing!" | ||
+ | player.hp = player.hpmax * 2 | ||
+ | player.tired = false | ||
+ | end, | ||
+ | |||
+ | OnUseCheck function() | ||
+ | if player.tired == false | ||
+ | ui.msg("You are too tired to use this.") | ||
+ | return(false) | ||
+ | else | ||
+ | return(true) | ||
+ | end | ||
+ | end, | ||
+ | |||
+ | OnUse function() | ||
+ | ui.msg("You are flooded with holy energy!" | ||
+ | player.hp = player.hpmax * 2 | ||
+ | player:set_affect(STATUSINVERT,20) | ||
+ | player.tired = true | ||
+ | return(false) | ||
+ | end, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | *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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | .... | ||
+ | type = ITEMTYPE_AMMOPACK, --use this or ITEMTYPE_AMMOPACK | ||
+ | ammo = 50, --required field | ||
+ | ammomax = 100, --required field | ||
+ | ammoID = "shell", --required field (_AMMOPACK only) | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | *''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. | ||
+ | |||
+ | These keys also correspond exactly to the ammo and ammo pack properties for a particular item instance: the only slight exception is that ammoID as a property is called "ammoid". | ||
+ | |||
+ | 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: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | name = "some kind of 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, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 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. In addition, the equipping hooks (see Armor and Boots) can be given to ammo packs, though this has not been used in practice. | ||
+ | |||
+ | ===Armor (_ARMOR) and Boots (_BOOTS)=== | ||
+ | Armor and boots items are purely meant to be equipped, and use the various properties that come with equipping protective gear. In particular, it can use any of the following hooks: | ||
+ | |||
+ | *'''OnPickup'''(''item'',''being'') | ||
+ | *'''OnFirstPickup'''(''item'',''being'') | ||
+ | *'''OnPickupCheck'''(''item'',''being'') | ||
+ | *'''OnEquip'''(''item'',''being'') triggers whenever ''being'' equips ''item''. This only occurs for equipping, not unequipping (see '''OnRemove()'''). | ||
+ | *'''OnEquipCheck'''(''item'',''being'') triggers whenever ''being'' equips ''item'' but before '''OnEquip()'''. Its boolean return output determines whether or not '''OnEquip()''' triggers, which includes equipping ''item'' at all (and is required in the script). | ||
+ | *'''OnEquipTick'''(''item'',''being'') triggers every time ''being'', with ''item'' equipped, takes an action: it occurs immediately after the action has been processed. | ||
+ | *'''OnRemove'''(''item'',''being'') triggers whenever ''being'' unequips ''item''. | ||
+ | *'''OnKill'''(''item'',''being_killer'',''being_killed'') triggers whenever ''being_killed'', wearing ''item'', dies due to a damage source caused by ''being_killer''. It can't be used to prevent death, so it won't stop the player from losing the game. | ||
+ | |||
+ | Armor uses '[' as its default ASCII character, and boots use ';' as its default ASCII character. | ||
+ | |||
+ | Armor and boots share the following additional prototype keys: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | .... | ||
+ | type = ITEMTYPE_ARMOR, --use this for ITEMTYPE_BOOTS | ||
+ | armor = 2, --required field | ||
+ | durability = 100, --defaults to 100 | ||
+ | knockmod = 25, --defaults to 0 | ||
+ | movemod = -10, --defaults to 0 | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | *''armor'' is the protection value of the item. This is a straight-up damage reduction, not to be confused with resistances (which work in terms of percentages). | ||
+ | *''durability'' is the "health" of the item: as damage sources hurt the being wearing the item, it loses durability. If the armor's current durability (the property) reaches zero, it is destroyed unless specifically flagged not to. | ||
+ | *''knockmod'' is the knockback modifier of the item. This can be a positive or negative value: positive values increase the modifier and negative values decrease the modifier. Increased knockback makes you fly farther due to damage. | ||
+ | *''movemod'' is the movespeed modifier of the item. Increased movespeed makes your movement faster. | ||
+ | |||
+ | *''armor'' is the protection value of the item instance, as determined by the ''armor'' prototype key. | ||
+ | *''durability'' is the current value of the item instance's durability. | ||
+ | *''maxdurability'' is the maximum value of the item instance's durability, as determined by the ''durability'' prototype key. | ||
+ | *''movemod'' is the movespeed modifier of the item instance, as determined by the ''movemod'' prototype key. | ||
+ | *''knockback'' is the knockback modifier of the item instance, as determined by the ''knockmod'' prototype key. | ||
+ | |||
+ | Armor and boots tend to be pretty simple, although using them in combination with item sets can lead to interesting possibilities. | ||
+ | |||
+ | <source lang="lua"> | ||
+ | ItemSets{ | ||
+ | .... | ||
+ | } | ||
+ | |||
+ | Items{ | ||
+ | name = "space boots", | ||
+ | id = "spacefoot", | ||
+ | set = "spaceset", | ||
+ | level = 12, | ||
+ | weight = 5, | ||
+ | desc = "They are quite magnetic and hold up against environemental damage. " .. | ||
+ | "Also, they're boots! From space!", | ||
+ | type = ITEMTYPE_BOOTS, | ||
+ | res_acid = 25, | ||
+ | res_fire = 25, | ||
+ | armor = 1, | ||
+ | durability = 300, | ||
+ | knockmod = -50, | ||
+ | movemod = -15, | ||
+ | } | ||
+ | |||
+ | Items{ | ||
+ | name = "space armor", | ||
+ | id = "spacebody", | ||
+ | set = "spaceset", | ||
+ | level = 12, | ||
+ | weight = 5, | ||
+ | desc = "This looks just like the clothing from one of those ancient " .. | ||
+ | "explorers from your space history books.", | ||
+ | type = ITEMTYPE_ARMOR, | ||
+ | res_acid = 25, | ||
+ | res_plasma = 25, | ||
+ | res_fire = 25, | ||
+ | armor = 2, | ||
+ | durability = 300, | ||
+ | knockmod = 0, | ||
+ | movemod = 0, | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | === Weapon (_MELEE, _RANGED _NRANGED)=== | ||
+ | Weapon items are what allow beings to attack with a greater ferocity than their ToDam and ToHit modifiers will allow. They replace the [[fists]] "weapon" when equipped in the weapon slots, and can also be equipped to the prepped slot. There are three item types that all count as weapons: | ||
+ | |||
+ | *Melee weapons (_MELEE) only allow the being to attack as they would with their fists (i.e., adjacent to their position). The alt fire can be a ranged attack of sorts (e.g., [[alternate fire#throw|throw]]) but cannot use ammo to do so. They are the most limited weapon type in terms of additional functionality. A melee weapon uses "\" as its default ASCII character. | ||
+ | *Ranged weapons (_RANGED) allow the being to attack anything within its vision and the weapon's own maximum range. This type of weapon requires ammo in order to fire. A ranged weapon uses "}" as its default ASCII character. | ||
+ | *Natural ranged weapons(_NRANGED) are similar to ranged weapons, except that a few prototype keys (such as ammoID) are avoided. The main purpose of natural ranged weapons is to give non-player beings a means to attack at range (i.e., an imp's fireball attack is a natural ranged weapon). A natural ranged weapon uses "?" as its default ASCII character. | ||
+ | |||
+ | Weapons can use nearly all of the hooks mentioned earlier, as well as a few others unique to items that can be used to attack: | ||
+ | |||
+ | *'''OnPickup'''(''item'',''being'') | ||
+ | *'''OnFirstPickup'''(''item'',''being'') | ||
+ | *'''OnPickupCheck'''(''item'',''being'') | ||
+ | *'''OnEquip'''(''item'',''being'') | ||
+ | *'''OnEquipCheck'''(''item'',''being'') | ||
+ | *'''OnEquipTick'''(''item'',''being'') | ||
+ | *'''OnRemove'''(''item'',''being'') | ||
+ | *'''OnKill'''(''item'',''being'') | ||
+ | *'''OnFire'''(''item'',''being'') triggers whenever ''being'' fires with ''item''. Its boolean return output determines whether or not ''item'' is fired. | ||
+ | *'''OnAltFire'''(''item'',''being'') triggers whenever ''being'' uses the alternate fire on ''item'' to attack. Its boolean return output determines whether or not ''item'' is fired. | ||
+ | *'''OnReload'''(''item'',''being'') triggers whenever ''being'' reloads ''item''. Its boolean return output determines whether or not ''item'' will be reloaded. | ||
+ | *'''OnAltReload'''(''item'',''being'') triggers whenever ''being'' uses the alternate reload command with ''item''. Its boolean return output determines whether or not the alternate reload script on ''item'' is run. | ||
+ | *'''OnFired'''(''item'',''being'') triggers immediately after ''item'' is fired by ''being''. It will not trigger if '''OnFire()''' or '''OnAltFire()''' did not trigger as well. (Note that this does not work for ITEMTYPE_MELEE.) | ||
+ | *'''OnHitBeing'''(''item'',''being_hitter'',''being_hit'') triggers whenever ''being_hitter'', firing ''item'', hits ''being_hit'' with a projectile from the item. (Note that this does not work for ITEMTYPE_MELEE.) | ||
+ | |||
+ | Weapons share the following additional prototype keys: | ||
+ | |||
+ | <source lang="lua"> | ||
+ | Items{ | ||
+ | .... | ||
+ | type = ITEMTYPE_RANGED, --use this, or ITEMTYPE_MELEE, or ITEMTYPE_NRANGED | ||
+ | damage = "4d3", --required field | ||
+ | damagetype = DAMAGE_BULLET, --required field | ||
+ | group = "weapon-pistol", --defaults to "weapon-other" | ||
+ | fire = 12, --defaults to 10 | ||
+ | acc = 6, --defaults to 0 | ||
+ | radius = 0, --defaults to 0 (_RANGED and _NRANGED only) | ||
+ | shots = 1, --defaults to 0 | ||
+ | ammoID = "ammo", --required field (_RANGED only) | ||
+ | ammomax = 10, --required field (_RANGED only) | ||
+ | reload = 12, --defaults to 10 (_RANGED only) | ||
+ | shotcost = 1, --defaults to 0 (_RANGED only) | ||
+ | altfire = ALT_AIMED, --defaults to ALT_NONE (_MELEE and _RANGED only) | ||
+ | altfirename = "aimed shot", --default depends on altfire (_MELEE and _RANGED only) | ||
+ | altreload = RELOAD_FULL, --defaults to RELOAD_NONE (_RANGED only) | ||
+ | altreloadname = "full", --default depends on altreload (_RANGED only) | ||
+ | soundID = "pistol", --defaults to "id" (_RANGED and _NRANGED only) | ||
+ | missile = "gun", --required field (optional for _MELEE) | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | *''damage'' is the weapon's damage, written in [[dice notation]]. | ||
+ | *''damagetype'' is the weapon's [[damage type]], one of seven possibilities. (See [[Modding:Constants#DamageType|DamageType]] for identifiers.) | ||
+ | *''group'' is the group that the weapon belongs to, for statistical purposes. Note that setting this has no effect on weapon-specific traits like [[Son of a Gun]] or [[Shottyman]]: these are determined by the weapon's flags. | ||
+ | *''fire'' is how long it takes to attack with the weapon, in game turns. | ||
+ | *''acc'' is the [[accuracy]] of the weapon. | ||
+ | *''radius'' is the [[explosions|explosion]] blast radius of the weapon's projectile. If radius is set to 0, no explosion occurs. | ||
+ | *''shots'' is the number of times the weapon attacks or fires projectiles. The default, 0, still causes one attack to occur. | ||
+ | *''ammoID'' is the identifer of the ammo that is to be used in this ranged weapon. This is what the weapon will look for in the player's inventory (or prepped slot in the case of ammo packs) when reloading. | ||
+ | *''ammomax'' is the maximum amount of ammo that the weapon can hold, also known as its capacity or clip size. | ||
+ | *''shotcost'' is the amount of ammo used to fire a single projectile of the weapon. The default, 0, still takes one ammo per projectile. | ||
+ | *''altfire'' is the identifer of an alternate fire mode for the weapon. This can either be one of several [[Modding:Constants#AltFire|pre-defined alternate fires]] or the name of a custom function. | ||
+ | *''altfirename'' is the name of the alternate fire mode, as it appears in the inventory screen. Most of the pre-defined alternate fires have such a name as its default, and in those cases this field can be ignored. | ||
+ | *''altreload'' is the identifer of an alternate reload mode for the weapon. This can either be one of several [[Modding:Constants#AltReload|pre-defined alternate reloads]] or the name of a custom function. | ||
+ | *''altreloadname'' is the name of the alternate fire mode, as it appears in the inventory screen. As with altfirename, this can likely be ignored if using a pre-defined alternate reload. | ||
+ | *''soundID'' is the sound of the projectile as the weapon fires. By default, it uses the same ID as the weapon's, so there is little reason to include your own. | ||
+ | *''missile'' is the identifier of the missile that this weapon fires. There are a few pre-defined missiles in the base game that can be used, or you can use your own. In addition, the missile's definition can be inlined into the weapon (see [[Modding:Tutorial/Game_Objects#Prototype|here]] for the basic structure). | ||
+ | |||
+ | Weapons also come with quite a few properties, although many can be directly related to a prototype key. The following are exactly related: | ||
+ | |||
+ | *''ammo'' | ||
+ | *''ammomax'' | ||
+ | *''acc'' | ||
+ | *''missile'' | ||
+ | *''shots'' | ||
+ | *''shotcost'' | ||
+ | *''damagetype'' | ||
+ | *''altfire'' | ||
+ | *''altreload'' | ||
+ | |||
+ | The following properties are different only by name: | ||
− | + | *''ammoid'' is the property of ''ammoID'' | |
+ | *''blastradius'' is the property of ''radius'' | ||
+ | *''reloadtime'' is the property of ''reload'' | ||
+ | *''usetime'' is the property of ''fire'' | ||
− | Finally, | + | Finally, the damage prototype key is broken into three separate properties: |
− | + | *''damage_sides'' is the number after the "d" | |
+ | *''damage_dice'' is the number before the "d" | ||
+ | *''damage_add'' is a constant added to all damage rolls (acts like the ToDam prototype field for beings) | ||
− | + | Weapons are a very important facet of the game, and so a separate tutorial will be prepared to handle examples and explanations for many different kinds of weapons. | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + |
Latest revision as of 15:54, 11 October 2011
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.
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.
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. (Note that DAMAGE_IGNOREARMOR has no resistance.)
- 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.
Base Properties
Items only have two properties that coincide with all item types:
- itype is the item type in property form, and specifies what can and can't be done with the item. For instance, with type _NONE, an item cannot be picked up or used.
- proto is the prototype of the instantiated item.
However, items also have all of the properties that thing objects do, since the item object is a subclass of things. This means that they come with a bunch of other properties you can use. (Some of the thing properties are readonly, meaning they can't be changed once the instantiated object is initialized. These will be bolded for clarification.)
- The following properties are the same as the prototype keys:
- color
- id
- name
- sprite
- res_bullet
- res_shrapnel
- res_melee
- res_acid
- res_fire
- res_plasma
- The following properties have a different name than a prototype key, but function exactly the same as the key:
- picture is the property of ascii
- The following properties are automatically given by default:
- nameplural is the name of items when a plural amount is described. In the case of items, the name is always appended with an 's' for nameplural.
- sid is a string identifier, similar to id. This is what is used dominantly by modders, and is set based on the item's id key.
- uid is a unique identifier across all instantiated objects. This is often used to save the state of a game.
- x is the x-coordinate of the item on the map.
- y is the y-coordinate of the item on the map. x and y make up an item's coordinate indirectly.
- __ptr is a pointer to the object in the engine: you'll never have to worry about this property.
Engine Hooks
All items can use two engine hooks:
- OnCreate(item) triggers whenever item is allocated to memory. Its primary function is to add properties to an item they may otherwise change from instance to instance. This will also trigger if item is created but not yet placed on the map.
- OnEnter(item,being) triggers whenever being enters the same tile as item. Although technically it can be used with any item, it is most known for its use with the teleporter item type.
All other hooks (and there are many) are dependent on the item's type. Whenever a new hook is introduced, it will be explained thoroughly: when another item type also uses that hook, the name of the hook will be given but not explained.
Map Items
Map items have fewer properties and hooks associated with them, but they are by no means any less difficult to work with. However, these items tend to be used as a part of the map, rather than a part of the player's resources, hence their name. Often they can be specifically designed for a particular map, accompanied by script from the level's own hooks that trigger when the item itself triggers.
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, OnEnter = function(_,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, OnCreate = function(self) local location = coord.random(area.get(area.FULL)) self:add_property("exit_pt",location) end, OnEnter = function(_,being) being.displace(exit_pt) end }
- First, we use OnCreate() to determine a random location. This is done by using the coord.random() method, 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 the thing:add_property() method, 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 method 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) come with an extra engine hook:
- OnPickup(item,player) triggers whenever item is picked up using the "get" command by player. Technically the second argument can work with any being, but players are the only being that actually pick up powerup items.
OnPickup() is a required hook for powerups, used 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, OnPickup = function() 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 the player:set_affect(status,duration) method (and receives a message using the ui.msg() method). Note that duration is based on how many actions the player takes, not how many scounts/turns/seconds (that is, affects work using an OnAction() hook).
Lever (_LEVER)
Levers are map items that come with two more engine hooks:
- OnUse(item,being) is triggered whenever item is used by being. It returns a boolean output that determines whether or not item is "consumed" (that is, no longer exists) after use. (defaults to false).
- OnUseCheck(item,being) is triggered whenever item is used by being but before OnUse(). Its boolean return output determines whether or not OnUse() itself triggers, which includes using item at all (and is required in the script).
These hooks serve as the lever's 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", --defaults to "" desc = "thermostat", --defaults to "" soundID = "lever", --defaults to "lever" fullchance = 10, --defaults to 0 warning = "This place looks fully air-conditioned." --defaults to "" }
- 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", OnCreate = function(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, OnUseCheck = function(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, OnUse = function() 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 uses its output to communicate this: 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 on the sidebar, and the desc property, which is the name of the item in the inventory (e.g., modified chaingun (1d7x6) [40/40] (P1F1)).
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. It also comes with the following engine hooks:
- OnUse(item,being)
- OnUseCheck(item,being)
- OnPickup(item,player)
- OnFirstPickup(item,being) triggers the first time any instance belonging to prototype item is picked up by being. Any subsequent pickups by being, whether item is the same instance or different, will not trigger this hook.
- OnPickupCheck(item,being) triggers whenever item is picked up by being but before OnPickup() and OnFirstPickup(). Its boolean return output determines whether or not the aforementioned hooks also trigger, which includes picking up item at all (and is required in the script).
The following example includes most of these hooks in action:
Items{ name = "holy cross", id = "hcross", level = 22, weight = 0, color = WHITE, type = ITEMTYPE_PACK, OnPickupCheck = function() 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, OnFirstPickup = function() ui.msg("You hear angels singing!" player.hp = player.hpmax * 2 player.tired = false end, OnUseCheck function() if player.tired == false ui.msg("You are too tired to use this.") return(false) else return(true) end end, OnUse function() 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_AMMOPACK, --use this or ITEMTYPE_AMMOPACK ammo = 50, --required field ammomax = 100, --required field ammoID = "shell", --required field (_AMMOPACK 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.
These keys also correspond exactly to the ammo and ammo pack properties for a particular item instance: the only slight exception is that ammoID as a property is called "ammoid".
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 kind of 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. In addition, the equipping hooks (see Armor and Boots) can be given to ammo packs, though this has not been used in practice.
Armor (_ARMOR) and Boots (_BOOTS)
Armor and boots items are purely meant to be equipped, and use the various properties that come with equipping protective gear. In particular, it can use any of the following hooks:
- OnPickup(item,being)
- OnFirstPickup(item,being)
- OnPickupCheck(item,being)
- OnEquip(item,being) triggers whenever being equips item. This only occurs for equipping, not unequipping (see OnRemove()).
- OnEquipCheck(item,being) triggers whenever being equips item but before OnEquip(). Its boolean return output determines whether or not OnEquip() triggers, which includes equipping item at all (and is required in the script).
- OnEquipTick(item,being) triggers every time being, with item equipped, takes an action: it occurs immediately after the action has been processed.
- OnRemove(item,being) triggers whenever being unequips item.
- OnKill(item,being_killer,being_killed) triggers whenever being_killed, wearing item, dies due to a damage source caused by being_killer. It can't be used to prevent death, so it won't stop the player from losing the game.
Armor uses '[' as its default ASCII character, and boots use ';' as its default ASCII character.
Armor and boots share the following additional prototype keys:
Items{ .... type = ITEMTYPE_ARMOR, --use this for ITEMTYPE_BOOTS armor = 2, --required field durability = 100, --defaults to 100 knockmod = 25, --defaults to 0 movemod = -10, --defaults to 0 }
- armor is the protection value of the item. This is a straight-up damage reduction, not to be confused with resistances (which work in terms of percentages).
- durability is the "health" of the item: as damage sources hurt the being wearing the item, it loses durability. If the armor's current durability (the property) reaches zero, it is destroyed unless specifically flagged not to.
- knockmod is the knockback modifier of the item. This can be a positive or negative value: positive values increase the modifier and negative values decrease the modifier. Increased knockback makes you fly farther due to damage.
- movemod is the movespeed modifier of the item. Increased movespeed makes your movement faster.
- armor is the protection value of the item instance, as determined by the armor prototype key.
- durability is the current value of the item instance's durability.
- maxdurability is the maximum value of the item instance's durability, as determined by the durability prototype key.
- movemod is the movespeed modifier of the item instance, as determined by the movemod prototype key.
- knockback is the knockback modifier of the item instance, as determined by the knockmod prototype key.
Armor and boots tend to be pretty simple, although using them in combination with item sets can lead to interesting possibilities.
ItemSets{ .... } Items{ name = "space boots", id = "spacefoot", set = "spaceset", level = 12, weight = 5, desc = "They are quite magnetic and hold up against environemental damage. " .. "Also, they're boots! From space!", type = ITEMTYPE_BOOTS, res_acid = 25, res_fire = 25, armor = 1, durability = 300, knockmod = -50, movemod = -15, } Items{ name = "space armor", id = "spacebody", set = "spaceset", level = 12, weight = 5, desc = "This looks just like the clothing from one of those ancient " .. "explorers from your space history books.", type = ITEMTYPE_ARMOR, res_acid = 25, res_plasma = 25, res_fire = 25, armor = 2, durability = 300, knockmod = 0, movemod = 0, }
Weapon (_MELEE, _RANGED _NRANGED)
Weapon items are what allow beings to attack with a greater ferocity than their ToDam and ToHit modifiers will allow. They replace the fists "weapon" when equipped in the weapon slots, and can also be equipped to the prepped slot. There are three item types that all count as weapons:
- Melee weapons (_MELEE) only allow the being to attack as they would with their fists (i.e., adjacent to their position). The alt fire can be a ranged attack of sorts (e.g., throw) but cannot use ammo to do so. They are the most limited weapon type in terms of additional functionality. A melee weapon uses "\" as its default ASCII character.
- Ranged weapons (_RANGED) allow the being to attack anything within its vision and the weapon's own maximum range. This type of weapon requires ammo in order to fire. A ranged weapon uses "}" as its default ASCII character.
- Natural ranged weapons(_NRANGED) are similar to ranged weapons, except that a few prototype keys (such as ammoID) are avoided. The main purpose of natural ranged weapons is to give non-player beings a means to attack at range (i.e., an imp's fireball attack is a natural ranged weapon). A natural ranged weapon uses "?" as its default ASCII character.
Weapons can use nearly all of the hooks mentioned earlier, as well as a few others unique to items that can be used to attack:
- OnPickup(item,being)
- OnFirstPickup(item,being)
- OnPickupCheck(item,being)
- OnEquip(item,being)
- OnEquipCheck(item,being)
- OnEquipTick(item,being)
- OnRemove(item,being)
- OnKill(item,being)
- OnFire(item,being) triggers whenever being fires with item. Its boolean return output determines whether or not item is fired.
- OnAltFire(item,being) triggers whenever being uses the alternate fire on item to attack. Its boolean return output determines whether or not item is fired.
- OnReload(item,being) triggers whenever being reloads item. Its boolean return output determines whether or not item will be reloaded.
- OnAltReload(item,being) triggers whenever being uses the alternate reload command with item. Its boolean return output determines whether or not the alternate reload script on item is run.
- OnFired(item,being) triggers immediately after item is fired by being. It will not trigger if OnFire() or OnAltFire() did not trigger as well. (Note that this does not work for ITEMTYPE_MELEE.)
- OnHitBeing(item,being_hitter,being_hit) triggers whenever being_hitter, firing item, hits being_hit with a projectile from the item. (Note that this does not work for ITEMTYPE_MELEE.)
Weapons share the following additional prototype keys:
Items{ .... type = ITEMTYPE_RANGED, --use this, or ITEMTYPE_MELEE, or ITEMTYPE_NRANGED damage = "4d3", --required field damagetype = DAMAGE_BULLET, --required field group = "weapon-pistol", --defaults to "weapon-other" fire = 12, --defaults to 10 acc = 6, --defaults to 0 radius = 0, --defaults to 0 (_RANGED and _NRANGED only) shots = 1, --defaults to 0 ammoID = "ammo", --required field (_RANGED only) ammomax = 10, --required field (_RANGED only) reload = 12, --defaults to 10 (_RANGED only) shotcost = 1, --defaults to 0 (_RANGED only) altfire = ALT_AIMED, --defaults to ALT_NONE (_MELEE and _RANGED only) altfirename = "aimed shot", --default depends on altfire (_MELEE and _RANGED only) altreload = RELOAD_FULL, --defaults to RELOAD_NONE (_RANGED only) altreloadname = "full", --default depends on altreload (_RANGED only) soundID = "pistol", --defaults to "id" (_RANGED and _NRANGED only) missile = "gun", --required field (optional for _MELEE) }
- damage is the weapon's damage, written in dice notation.
- damagetype is the weapon's damage type, one of seven possibilities. (See DamageType for identifiers.)
- group is the group that the weapon belongs to, for statistical purposes. Note that setting this has no effect on weapon-specific traits like Son of a Gun or Shottyman: these are determined by the weapon's flags.
- fire is how long it takes to attack with the weapon, in game turns.
- acc is the accuracy of the weapon.
- radius is the explosion blast radius of the weapon's projectile. If radius is set to 0, no explosion occurs.
- shots is the number of times the weapon attacks or fires projectiles. The default, 0, still causes one attack to occur.
- ammoID is the identifer of the ammo that is to be used in this ranged weapon. This is what the weapon will look for in the player's inventory (or prepped slot in the case of ammo packs) when reloading.
- ammomax is the maximum amount of ammo that the weapon can hold, also known as its capacity or clip size.
- shotcost is the amount of ammo used to fire a single projectile of the weapon. The default, 0, still takes one ammo per projectile.
- altfire is the identifer of an alternate fire mode for the weapon. This can either be one of several pre-defined alternate fires or the name of a custom function.
- altfirename is the name of the alternate fire mode, as it appears in the inventory screen. Most of the pre-defined alternate fires have such a name as its default, and in those cases this field can be ignored.
- altreload is the identifer of an alternate reload mode for the weapon. This can either be one of several pre-defined alternate reloads or the name of a custom function.
- altreloadname is the name of the alternate fire mode, as it appears in the inventory screen. As with altfirename, this can likely be ignored if using a pre-defined alternate reload.
- soundID is the sound of the projectile as the weapon fires. By default, it uses the same ID as the weapon's, so there is little reason to include your own.
- missile is the identifier of the missile that this weapon fires. There are a few pre-defined missiles in the base game that can be used, or you can use your own. In addition, the missile's definition can be inlined into the weapon (see here for the basic structure).
Weapons also come with quite a few properties, although many can be directly related to a prototype key. The following are exactly related:
- ammo
- ammomax
- acc
- missile
- shots
- shotcost
- damagetype
- altfire
- altreload
The following properties are different only by name:
- ammoid is the property of ammoID
- blastradius is the property of radius
- reloadtime is the property of reload
- usetime is the property of fire
Finally, the damage prototype key is broken into three separate properties:
- damage_sides is the number after the "d"
- damage_dice is the number before the "d"
- damage_add is a constant added to all damage rolls (acts like the ToDam prototype field for beings)
Weapons are a very important facet of the game, and so a separate tutorial will be prepared to handle examples and explanations for many different kinds of weapons.