Creating Custom Items in ZDoom

Using DECORATE to Create Custom Items

ZDooM (and its derivatives GZDooM & SkullTag) allows the creation of items that look and behave completely differently from those in "vanilla" DooM. These new items, known as "actors", need to be defined; this is accomplished via a text-based "lump" (name for any entry in a DooM wad) known as DECORATE. The purpose of this article is not to provide an introduction to defining custom items; rather, it highlights some lessons that I recently learned while creating my own custom items. For information on creating DECORATE definitions, refer to these sources:

DECORATE-related Tutorials

New User's Guide to Editing with DECORATE

Creating Non-interactive Decorations

DooM World Forum Discussion on DECORATE

A. Power-Ups, Inventories, and Hubs

It turns out that many power-ups can't be carried from one map to another in a hub. For example if you want the player to pick up a radiation suit in one map and carry it for use in another map in the hub, the "standard" DooM radiation suit will not work. The item is activated as soon as you pick it up; but more importantly, as soon as you teleport into a new map you lose the power-up, even if there's plenty of time left on the item. In my opinion the biggest oversight in ZDooM's definitions in this regard, is the lack of portability of a computer map. You can pick up a computer map on one map in a hub and find that you've "lost" it when you travel to another map. [To be fair, however, when you return to the map in which you picked up the unit it is usable again.] So, in this section I will outline the steps to make a radiation suit that can be carried to, and used in, any map within a hub.

First, here's an example of a radiation suit that can be picked up in a map and put into an inventory for use in that or any other map in a hub.

////////////////////////////////////////
// Custom Radiation Suit //
////////////////////////////////////////
ACTOR SlimeSuit : PowerupGiver 20006
{
  Game Doom
  SpawnID 206
  Height 46
  +COUNTITEM
  +INVENTORY.HUBPOWER
  +INVENTORY.ALWAYSPICKUP
  +INVENTORY.INVBAR
  Inventory.Icon SUITA0
  Inventory.MaxAmount 1
  Inventory.PickupMessage "$GOTSUIT" // "Radiation Shielding Suit"
  Powerup.Type "IronFeet"
  States
  {
  Spawn:
    SUIT A -1 Bright
    Stop
  }
}

So let's examine the definition above, which is virtually the same as the one for DooM's radiation suit:

    1. You have to give the actor a unique name, especially one that has not been given to one of the DooM actors. In this case, SlimeSuit is a name that has not been taken; it also has the benefit of describing the actor you are creating.
    2. DooM's RadSuit inherits its properties from a PowerupGiver, which is used to give one of the existing powerups to the player using or picking up this item. It is only used as a base class for many predefined inventory items or as a base to define new powerup pickups. This is what you're doing here for SlimeSuit - letting the game know that your actor will be inheriting (in this case) an existing powerup that is already defined.
    3. If you want to be able to spawn the SlimeSuit in-game (e.g., via a script), you'll need to give it a unique SpawnID, in this case = 206.
    4. Now here's the key to making your actor usable in other maps in a hub - you'll add the flag +INVENTORY.HUBPOWER.
    5. You probably want to be able to show the player that the SlimeSuit is in her/his inventory. You'll use the flag +INVENTORY.INVBAR.
    6. Define the maximum number of SlimeSuits that the player can carry at any given time (in this case = 1) using Inventory.MaxAmount.
    7. Now here's the key step - you need to define what the PowerupGiver actually does. In this case, because you want the SlimeSuit to behave exactly like the RadSuit, you'll simply use the flag = Powerup.Type "IronFeet". IronFeet is an internal class that already contains all the instructions necessary for defining your SlimeSuit, so all you need to do is call it up in your definition.

B. Creating A Custom Berserk

Most DooM items are automatically activated as soon as you pick them up. This, of course, defeats the purpose of having an inventory in which you can store items for later use. In addition, some items change the player's state upon being picked up; the Berserk is an example of such an item. When you pick it up your health automatically maxes out at 100 HP, your weapon is lowered and your fist is readied, and your punches are given greater strength. You don't want these state changes to occur, but you want to be able to call them up when you're ready to use this item. So here's an example:

 
///////////////////////////////
// Custom Berserk //
///////////////////////////////
ACTOR Pugilist : CustomInventory 20011
{
  Game Doom
  SpawnID 211
  +COUNTITEM
  +INVENTORY.HUBPOWER
  +INVENTORY.ALWAYSPICKUP
  +INVENTORY.INVBAR
  Inventory.Icon PSTRA0
  Inventory.MaxAmount 1
  Inventory.PickupMessage "$GOTBERSERK" // "Berserk!"
  Inventory.PickupSound "misc/p_pkup"
  States
  {
  Spawn:
    PSTR A -1
    Stop
  Use:
    TNT1 A 0 A_GiveInventory("PowerStrength")
    TNT1 A 0 HealThing(100, 0)
    TNT1 A 0 A_SelectWeapon("Fist")
    Stop
  }
}
    1. Before you examine the definition above, please open up the definition for the Berserk.
    2. You'll notice that the Berserk has a "Pickup" state. This provides a definition of what changes to make to the player's state upon the item being picked up. Of course, you want the item to be save to your inventory, not used up when it is picked up. Therefore, you need to delete the "Pickup" state and replace it with a "Use" state.
    3. However, as the state changes you wish to give the player when the item is used are the same as those when the Berserk is picked up, all you need to do is rename "Pickup" to "Use".
    4. All other changes to make the item storable in an inventory are described above for the SlimeSuit.

C. Creating A Custom SoulSphere

 Now let's look at a slightly more complicated example of an item that gives the player a powerup but does not have a "Use" state at all. The SoulSphere gives the player 100 HP but allows the maximum health to exceed 100 HP to a maximum of 200 HP. You want to be able to retain this property, but make it usable at will. So here's an example:

////////////////////////////////////
// Custom SoulSphere //
////////////////////////////////////
ACTOR BlueSphereHealth : Health
{

  Inventory.Amount 100

  Inventory.MaxAmount 200
  +INVENTORY.ALWAYSPICKUP
}

ACTOR BlueSphere : CustomInventory 20014
{
  Game Doom
  SpawnID 214
  +COUNTITEM
  +INVENTORY.HUBPOWER
  +INVENTORY.INVBAR
  +INVENTORY.ALWAYSPICKUP
  +INVENTORY.FANCYPICKUPSOUND
  inventory.maxamount 1
  inventory.icon SOULA0
  Inventory.PickupMessage "$GOTSUPER" // "Supercharge!"
  Inventory.PickupSound "misc/p_pkup"States
  {
  Spawn:
    SOUL ABCDCB 6 Bright
    LoopUse: TNT1 A 0 A_GiveInventory("BlueSphereHealth")
    Stop
  }
}
    1. Before you examine the definition above, please open up the definition for the SoulSphere.
    2. You'll notice that the SoulSphere only has a "Spawn" state and has no "Pickup" or "Use" states. This is because the SoulSphere is defined to inherit from the Class: Health, which is automatically activated upon being picked up. [Note that health items are always effective when picked up; they cannot be placed in the inventory.] Given that the health class does not lend itself to usage in an inventory, and the fact that the SoulSphere has no "Use" state, you have no choice but to create a CustomInventory item.
    3. CustomInventory items are special items that allow some very primitive scripted functionality using its states. In other words, if you want to include a "Pickup", "Use", or "Drop" state in the definition of your item, you must define it as a CustomInventory.
    4. Also, because there are no pre-defined CustomInventory items that behave like a SoulSphere, you need to create a completely new actor. In this case, I have named it BlueSphereHealth. This actor can inherit from Class: Health, as this is the behavior you desire for it.
    5. Your in-game actor will inherit all the health attributes from the BlueSphereHealth actor (via the "Use" state), but you'll need to define it as a CustomInventory. [Defining it as Class: Health will completely negate any inheritance from the other actor, as your "Use" states will become redundant. You'll note that I've taken the Inventory.Amount and Inventory.MaxAmount flags from the SoulSphere definition, and put them into the definition of my BlueSphereHealth.
    6. Your "Use" state must include a flag that instructs the game to use the pre-defined actor "BlueSphereHealth".
    7. All other changes to make the item storable in an inventory are described above for the SlimeSuit.
D. Creating An Inventory Item That Is Usable Only After One or More Conditions Are Met

In an adventure scenario, and particularly within a hub, it may be necessary to prevent a player from using an inventory item prematurely. For example, let's say the player needs a radiation suit in Map05 of a hub in order to make progress through a nukage area, but the suit is available only in Map02. If the player picks the suit up in Map02 and accidentally uses it before getting to Map05, s/he is essentialy stuck in the game, as the nukage area cannot be traversed without the suit. So here's an example of how to set up the actor in such a manner that the suit (called a "special" SlimeSuit in this tutorial) cannot be used before reaching the nukage area:

//////////////////////////////////////////////////////
// Custom Radiation Suit (Special) //
//////////////////////////////////////////////////////
ACTOR SlimeSuitProtection : RadSuit
{
  +INVENTORY.ALWAYSPICKUP
}

ACTOR SlimeSuit : CustomInventory 20006
{
  Game Doom
  SpawnID 206
  Height 46
  +COUNTITEM
  +INVENTORY.HUBPOWER
  +INVENTORY.INVBAR
  inventory.maxamount 1
  inventory.icon SUITX0
  Inventory.PickupMessage "$GOTSUIT" // "Radiation Shielding Suit"
  Inventory.PickupSound "misc/p_pkup"States
  {
  Spawn:
    SUIT Z -1 Bright
    Stop
  Use:
    TNT1 A 0 A_JumpIf((ACS_ExecuteWithResult(666,0,0,0)) < 1, "FailState")
    TNT1 A 0 A_GiveInventory("SlimeSuitProtection", 1)
    Stop
  FailState:
    TNT1 A 0 A_Print("SlimeSuit Use Disabled in this Sector.")
    Fail
  }
}
    1. You'll notice that, at its core, the definition is very similar to that of the Custom Radiation Suit at the top of this tutorial.
    2. The key difference between the "Special" SlimeSuit and the "Regular" SlimeSuit in this tutorial is that the former is defined as a CustomInventory item, while the latter inherits from DooM's Radiation Suit via the PowerupGiver class, which is used to give one of the existing powerups to the player. It is necessary to define the special slimesuit as a CustomInventory item because a "regular" inventory item does not support the type of feature that is needed to control the item's use (see 5, below).
    3. As with the custom SoulSphere above, because there are no pre-defined CustomInventory items that behave like a RadSuit, you need to create a completely new actor. In this case, I have named it SlimeSuitProtection. This actor directly inherits from Class: RadSuit, as this is the behavior you desire for it.
    4. You'll notice that the sprite graphic used for this special SlimeSuit is not the same as the regular SlimeSuit (which in turn was using the sprite graphic of DooM, which is SUITA0). The special suit uses a sprite graphic named SUITZ0, which happens to be different in appearance from SUITA0 to mark it as a special item. This is done so that the regular radiation suit (or regular SlimeSuit) can be used in the same game without causing confusion about the inventory item's capabilities. If you have no need to distinguish between the special SlimeSuit or regular SlimeSuit/RadiationSuit you can simply use the DooM's sprite graphic and name.
    5. Your in-game actor will inherit all the radiation protection attributes from the SlimeSuitProtection actor (via the "Use" state). However, note that there's a "condition" placed in the "Use" state, which only allows the inventory item to be used if a specific condition has been met. This is implemented via a JumpIf expression. Let's see how this expression and state work:
      a. The first part of the expression, A_JumpIf, jumps the specified amount of states (not frames) forward if the expression evaluates to true.
      b. The second part of the expression, (ACS_ExecuteWithResult(666,0,0,0)) < 1, sets up the condition that JumpIf is testing. In this case, it is testing script number 666 to see if the "return value" is less than 1. (More on "return value" later in this tutorial).
      c. The final part of this expression, "FailState", specifies the name of the state to which to jump if the test proves to be true (i.e., return value is less than 1). In this case, the "Use" state is considered to have failed, and the item is "disabled" for use by the player.
      d. If the JumpIf determines that the result of the test is untrue, the definition jumps to the next frame, namely: TNT1 A 0 A_GiveInventory("SlimeSuitProtection", 1), which then activates the SlimeSuit.
    In summary, if the script passes a value less than 1, the "Use" state is deemed to have failed, and the item cannot be used. If the script passes a value of 1 or more, the item is activated.
    6. The FailState provides a message that provides a clue to the player; it is entirely optional. The key aspect of this state is the "Fail" statement, which prevents the item from being used.
    7. All other changes to make the item storable in an inventory are described above for the SlimeSuit.
So now let's take a look at the "switch" that determines when the SlimeSuit can be activated - Script 666. Essentially, we're setting up the condition like a reverse switch, with the condition indicating that the player is notat the area where the SlimeSuit is required. So long as the condition is true the return value will be 0, and the expression will jump to the fail state. As soon as the player arrives at the appropriate area, the return value changes to 1, the JumpIf condition becomes false, and the definition jumps to the A_GiveInventory frame, which activates the SlimeSuit.

Because it serves as a switch, Script 666 will look different in maps where SlimeSuit use is "denied" and in maps where it is "allowed". [As an aside, the script number is arbitrary. You can pick any number, so long as it's not already in use in all of the maps. A suitably high number (e.g., 999) would work just as well, as it's unlikely that a map has that many scripts - assuming they are consecutive.] First, a look at a map where SlimeSuit use is denied:

//////////////////////////////////////////////////////////////////////////////////

// Script 666: Prevents radiation suit from being used //

//////////////////////////////////////////////////////////////////////////////////

  script 666 (void)

  {
    SetResultValue (0);
  }

An ACS_ExecuteWithResult script must always be accompanied by a script with SetResultValue. The Special SlimeSuit definition is running the ACS_ExecuteWithResult script, which in turn is looking for the result from a script that returns a value via a SetResultValue. This value can either be a numeric value or a True/False value. In our example it's a numeric value, namely 0. Remember, you want the A_JumpIf expression to failin this instance, which means the value needs to be less than 1. Any time you try to "use" the SlimeSuit in your inventory, ACS_ExecuteWithResult runs Scrpit 666, gets a returned value less than 1, jumps to the FailState state, and prevents the item from being used. Now lets look at the map where SlimeSuit use is allowed:

//////////////////////////////////////////////////////////////////////////////////

// Script 666: Prevents radiation suit from being used //

//////////////////////////////////////////////////////////////////////////////////

   script 666 (void)
  {
    SetResultValue (1);

  }

Pretty self-explanatory - by setting the return value to 1, you are now allowing the definition to jump to the instruction that activates the SlimeSuit. This will allow you to use the SlimeSuit as soon as you enter the map. But what if you didn't want the player to use the SlimeSuit right away upon entering the map? You'd have to create a switch within a switch that would remain turned off until the player reached the proper point in the map. You'd set this up in two steps, as follows:

/////////////////////////////////////////////////////////////////////////////////
// Script 9: Message about radiation suit on Map02 //
/////////////////////////////////////////////////////////////////////////////////
  int Map05;
  script 9 (void)
  {
    Map05 = 1;
    print(s:"Activate Radiation Suit from your inventory before entering nukage.");
  }

////////////////////////////////////////////////////////////////////////
// Script 666: Allows radiation suit to be used //
////////////////////////////////////////////////////////////////////////
  script 666 (void)
  {
    if(!Map05)
      {
      print(s:"You are not yet ready to use the SlimeSuit.");
      SetResultValue (0);
      }
    else
      SetResultValue (1);

  }


Script 9 is set up to throw the first switch, allowing Script 666 to throw the second switch. [Script 9 also has a message, but that's entirely optional.] Script 9 can either be activated by pressing an actual switch, or by crossing a linedef, or by entering a sector, or by any other means by which a script may be triggered. Until Script 9 is triggered, the variable 'Map05' will not be switched on, and Script 666 will return a value of 0. As soon as the player reaches the designated point on the map and triggers Script 9, Script 666 will return a value of 1. This will allow the DECORATE definition to properly complete the "Use" state and activate the SlimeSuit.
Log in to comment
Cookies make it easier for us to provide you with our services. With the usage of our services you permit us to use cookies.
More information Ok Decline

Donate and Help keeping the Realm667 alive!