header general

Actors (DECORATE)

Alternate Fire/Reload/Zoom/...States on Weapons

Thanks to WildWeasel for teaching me a non-ACS way of doing this. I've been meaning to write this for ages, but never got around to it.

One of the more obscure tricks with weapons is the ability to add a new firing state (a state that can be activated aside from primary and alternate fire). This is usually used to have a key that specifically zooms or reloads. However, this is actually quite a simple trick.

For reference, here are three wiki pages:


I'll put it in basic terms, then explain each part further. Essentially, you have a dummy inventory item as a flag for the weapon to check for in the Ready state, and this item is given and taken away by a pair of CustomInventory items which are bound to a key.

I'll use a third Fire state as an example. To explain further, we start with our fake inventory item; an item that literally has
no use besides A_JumpIfInventory to check for:


Actor IsFiring : Inventory
{
  Inventory.Amount 1 //This doesn't technically need to be here but it's better to make sure
  Inventory.MaxAmount 1
  -INVBAR //Keeps it from displaying in the inventory bar, but you don't have to worry
  States
  {
  Spawn:
    TNT1 A 1
    Fail //Keeps it from really doing anything in the world
  }

All this will do is sit in your inventory. Now we have an alternate pistol checking for this:

Actor Pistol2 : DoomWeapon 5010 //Named differently so it won't conflict with Doom's
  {
  Game Doom
  Weapon.SelectionOrder 1900
  Weapon.AmmoUse 1
  Weapon.AmmoGive 20
  Weapon.AmmoType "Clip"
  AttackSound "weapons/pistol"
  Obituary "$OB_MPPISTOL"
  +WEAPON.WIMPY_WEAPON
  Inventory.Pickupmessage "$PICKUP_PISTOL_DROPPED"
  Decal BulletChip
  States
  {
  Ready:
    PISG A 0 A_JumpIfInventory("IsFiring", 1, "AltAltFire") //We're checking for the fake inventory item
    PISG A 1 A_WeaponReady
    Loop
  Deselect:
    PISG A 1 A_Lower
    Loop
  Select:
    PISG A 1 A_Raise
    Loop
  Fire:
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  AltFire: //Because why not, we're already using altfire!
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  AltAltFire:
    PISG A 4
    PISG BBB 0 A_FirePistol
    PISG B 6 A_FirePistol //Fires four times so we know it's working
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  Flash:
    PISF A 7 Bright A_Light1
    Goto LightDone
    PISF A 7 Bright A_Light0
    Goto LightDone
  Spawn:
    PIST A -1
    Stop
  }
}

There! Our pistol is checking for that item, and has a new firing state. Now we just need those two CustomInventory items:

Actor Action_Fire : CustomInventory
{
  Inventory.Amount 1
  Inventory.MaxAmount 1
  -INVBAR
  States
  {
  Use:
    TNT1 A 0 A_GiveInventory("IsFiring", 1)
    Fail // It's important that these items end in "Fail" instead of "Stop" or else they are removed from inventory as soon as they are used. Fail will keep them in your inventory.
  }
}

Actor Action_FireCancel : CustomInventory
{
  Inventory.Amount 1
  Inventory.MaxAmount 1
  -INVBAR
  States
  {
  Use:
    TNT1 A 0 A_TakeInventory("IsFiring", 1)
    Fail
  }
}


When Action_Fire is used, it will give the fake item, but when Action_FireCancel is used, it will take it away again. Thus, using one will make the pistol fire, and the other will make it stop. It is important that both of these are given to the player at the start with Player.StartItem, or this will not work.

Now to bind these two items to a key:

AddKeySection "Your Section Name" YourSectionsName
AddMenuKey "Alternate AltFire" +altaltfire
Alias +altaltfire "Use Action_Fire" // + events occur when the key is pressed.
Alias -altaltfire "Use Action_FireCancel" // - events occur when the key is released.
DefaultBind x +altaltfire // only the + event needs to be bound.


Check the wiki link above about adding keysections for explanations of what these do. Now, when the key is pressed it will give the item (activating AltAltFire), and when the key is released, it will take the item away (making it stop firing).

And it's that simple! If you want to test it out, here's a full Decorate and KeyConf:

Actor Pistol2 : DoomWeapon 5010 //Named differently so it won't conflict with Doom's
  {
  Game Doom
  Weapon.SelectionOrder 1900
  Weapon.AmmoUse 1
  Weapon.AmmoGive 20
  Weapon.AmmoType "Clip"
  AttackSound "weapons/pistol"
  Obituary "$OB_MPPISTOL"
  +WEAPON.WIMPY_WEAPON
  Inventory.Pickupmessage "$PICKUP_PISTOL_DROPPED"
  Decal BulletChip
  States
  {
  Ready:
    PISG A 0 A_JumpIfInventory("IsFiring", 1, "AltAltFire") //We're checking for the fake inventory item
    PISG A 1 A_WeaponReady
    Loop
  Deselect:
    PISG A 1 A_Lower
    Loop
  Select:
    PISG A 1 A_Raise
    Loop
  Fire:
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  AltFire: //Because why not, we're already using altfire!
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  AltAltFire:
    PISG A 4
    PISG BBB 0 A_FirePistol
    PISG B 6 A_FirePistol //Fires four times so we know it's working
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  Flash:
    PISF A 7 Bright A_Light1
    Goto LightDone
    PISF A 7 Bright A_Light0
    Goto LightDone
  Spawn:
    PIST A -1
    Stop
  }
}

Actor IsFiring : Inventory
{
  Inventory.Amount 1 //This doesn't technically need to be here but it's better to make sure
  Inventory.MaxAmount 1
  -INVBAR //Keeps it from displaying in the inventory bar, but you don't have to worry
  States
  {
  Spawn:
    TNT1 A 1
    Fail //Keeps it from really doing anything in the world
  }
}

Actor Action_Fire : CustomInventory
{
  Inventory.Amount 1
  Inventory.MaxAmount 1
  -INVBAR
  States
  {
  Use:
    TNT1 A 0 A_GiveInventory("IsFiring", 1)
    Fail // It's important that these items end in "Fail" instead of "Stop" or else they are removed from inventory as soon as they are used. Fail will keep them in your inventory.
  }
}

Actor Action_FireCancel : CustomInventory
{
  Inventory.Amount 1
  Inventory.MaxAmount 1
  -INVBAR
  States
  {
  Use:
    TNT1 A 0 A_TakeInventory("IsFiring", 1)
    Fail
  }
}

Actor DoomPlayer2 : DoomPlayer
{
  Player.StartItem "Pistol2", 1
  Player.StartItem "Clip", 50
  Player.StartItem "Fist"
  Player.StartItem "Action_Fire", 1
  Player.StartItem "Action_FireCancel", 1
}


KeyConf:

ClearPlayerClasses
AddPlayerClass DoomPlayer2

AddKeySection "Your Section Name" YourSectionsName
AddMenuKey "Alternate AltFire" +altaltfire
Alias +altaltfire "Use Action_Fire" // + events occur when the key is pressed.
Alias -altaltfire "Use Action_FireCancel" // - events occur when the key is released.
DefaultBind x +altaltfire // only the + event needs to be bound.


Keep in mind, by the way, Fire and AltFire have some special behavior that alerts monsters, and A_Refire only works for Fire/Hold and AltFire/AltHold. Here's another AltAltFire that emulates that behavior:

AltAltFire:
    PISG A 4 A_AlertMonsters //Because it alerts monsters as soon as the state is entered.
    PISG BBB 0 A_FirePistol
    PISG B 6 A_FirePistol //Fires four times so we know it's working
    PISG C 4
    PISG B 5 A_JumpIfInventory("IsFiring", 1, "AltAltFire") //Fire again if the button is still pressed, like A_Refire.
    Goto Ready

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.

CustomInventory Items as New Functions

Before we start, this is just a really tiny tutorial for a little trick you can take advantage of. Only really two links you need to know about, and they're things you want to know anyway:
Sometimes, you want monsters, players, or other actors to do a lot of things at once, while it's doing other things. For example, if you wanted to make a standard imp do something interesting in its See state if it's fully submerged underwater, you might do something like this:

  See:
    TROO A 0 A_JumpIf(waterlevel == 3, 2)
    TROO A 0 A_ChangeFlag("NoPain", 0)
    Goto See+3
    TROO A 0 A_ChangeFlag("NoPain", 1)
    TROO A 3 A_Chase
    TROO A 0 A_JumpIf(waterlevel == 3, 2)
    TROO A 0 A_ChangeFlag("NoPain", 0)
    Goto See+7
    TROO A 0 A_ChangeFlag("NoPain", 1)
    TROO A 3 A_Chase
    TROO B 0 A_JumpIf(waterlevel == 3, 2)
    TROO B 0 A_ChangeFlag("NoPain", 0)
    Goto See+11
    TROO B 0 A_ChangeFlag("NoPain", 1)
    TROO B 3 A_Chase
    TROO B 0 A_JumpIf(waterlevel == 3, 2)
    TROO B 0 A_ChangeFlag("NoPain", 0)
    Goto See+15
    TROO B 0 A_ChangeFlag("NoPain", 1)
    TROO B 3 A_Chase
    TROO C 0 A_JumpIf(waterlevel == 3, 2)
    TROO C 0 A_ChangeFlag("NoPain", 0)
    Goto See+19
    TROO C 0 A_ChangeFlag("NoPain", 1)
    TROO C 3 A_Chase
    TROO C 0 A_JumpIf(waterlevel == 3, 2)
    TROO C 0 A_ChangeFlag("NoPain", 0)
    Goto See+23
    TROO C 0 A_ChangeFlag("NoPain", 1)
    TROO C 3 A_Chase
    TROO D 0 A_JumpIf(waterlevel == 3, 2)
    TROO D 0 A_ChangeFlag("NoPain", 0)
    Goto See+27
    TROO D 0 A_ChangeFlag("NoPain", 1)
    TROO D 3 A_Chase
    TROO D 0 A_JumpIf(waterlevel == 3, 2)
    TROO D 0 A_ChangeFlag("NoPain", 0)
    Goto See+31
    TROO D 0 A_ChangeFlag("NoPain", 1)
    TROO D 3 A_Chase
    Loop
//Don't quote me on this, I haven't tested it. Just an example of what it might look like

Pretty obviously, this is hilariously ugly, can bug out easily if not done right, and you have to keep close track of your jumps, gotos and etc. THERE IS A BETTER WAY TO DO THIS!

In "real" programming, if you have one function that does the same thing repeatedly in a sequence with other stuff, we outsource that repeated thing to another function and call that, so it looks cleaner and is easier to troubleshoot. This is why, for example, A_MissileAttack has A_FaceTarget built in, instead of calling it with a 0-delay just beforehand. Unfortunately in Decorate, we can't define new functions (the A_ things)... But we have CustomInventory!

CustomInventory, by definition, is an item that allows you to define a Pickup state (or Use, but we want Pickup for this) with existing functions to make a powerup or other item that does anything you want it to do, with a few limitations. These functions in the Pickup and Use states are executed BY THE ACTOR WHO PICKED UP THE ITEM, not the item itself. Thus, we can slap some of those functions we have in a CustomInventory item, and A_GiveInventory it so we can execute those checks and flag changes to one call!

Yes! As the wiki page says, monsters can have items! They have no code for picking up items (for good or ill), so they either have to give it to themselves or it has to be done in ACS.

  See:
    TROO AABBCCDD 3 A_GiveInventory("ImpWaterCheck", 1) //Execute our new function!
    Loop

Actor ImpWaterCheck : CustomInventory
{
  Inventory.PickupMessage ""
  Inventory.PickupSound ""
  -CountItem
  states
  {
  Spawn:
    TNT1 A 1
    Fail //We don't want this ever spawning.
  Pickup:
    TNT1 A 0 A_Chase
    TNT1 A 0 A_JumpIf(waterlevel == 3, "Underwater")
    TNT1 A 0 A_ChangeFlag("NoPain", 0)
    Stop
  Underwater: //Because new states are just awesome
    TNT1 A 0 A_ChangeFlag("NoPain", 1)
    Stop
  }
}

It requires a new CustomInventory item, but we've just made our See state much easier to follow. Now, keep in mind, CustomInventory items CANNOT have delays! Nor would we want them to in this case because it we want all of this to happen immediately when it's given. Still, CustomInventory items can't have delays, and no matter what number you put in, it'll still execute all of it with 0 delays. Now, I really don't recommend this for just collapsing a few 0-delay things together, but it's ideal for when you have checks, jumps and things like that in a looping state, because that gets difficult to manage how the state flows.

Another thing I like to use this for is when I have a monster that is heavily built in to some ACS scripts. If you want to use an ACS script change a flag on a monster, for example, but don't want to use SetActorState (and risk interrupting an animation), you can give a CustomInventory item that does it.

Making a powered/unpowered weapon

Every once in a while, I see a post asking how to make weapons affected by a Tome of Power, or a weapon that doesn't implement it 100% correctly, so here's a tutorial to explain it.

First off, there are three wiki pages to look at:

  • PowerupGiver with the Powerup.Type WeaponLevel2: This is the powerup that activates it. For creating a new weapon, it's not necessary to deal with, but it helps to explain how the system works. For testing, you can use the Powerup console cheat to activate.
  • Weapon.SisterWeapon: This goes on both the unpowered and powered weapons, it's what tells the WeaponLevel2 powerup what weapons go together.
  • +Weapon.Powered_Up: This tells it what weapon to use when under the effects of a WeaponLevel2 powerup. Without this or Weapon.SisterWeapon, it can't work.


I'll use Doom's Pistol to illustrate. This is it before we add support for WeaponLevel2.


Actor Pistol2 : DoomWeapon 5010 //This is named differently so it won't conflict with Doom's
{
  Game Doom
  Weapon.SelectionOrder 1900
  Weapon.AmmoUse 1
  Weapon.AmmoGive 20
  Weapon.AmmoType "Clip"
  AttackSound "weapons/pistol"
  Obituary "$OB_MPPISTOL"
  +WEAPON.WIMPY_WEAPON
  Inventory.Pickupmessage "$PICKUP_PISTOL_DROPPED"
  States
  {
  Ready:
    PISG A 1 A_WeaponReady
    Loop
  Deselect:
    PISG A 1 A_Lower
    Loop
  Select:
    PISG A 1 A_Raise
    Loop
  Fire:
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  Flash:
    PISF A 7 Bright A_Light1
    Goto LightDone
    PISF A 7 Bright A_Light0
    Goto LightDone
  Spawn:
    PIST A -1
    Stop
  }
}

To get started, we create a second weapon inheriting from the pistol:


Actor PoweredPistol : Pistol
{
  States
  {
  Fire:
    PISG A 4
    PISG BB 0 A_FirePistol //Added this just to make it different
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  }
}

You'll notice there is a LOT less code here, in the powered version. That's because with inheritence, it's getting information like states and properties from the original. If you want to change anything, replace it in the new version, like with this Fire state.
Something to remember, if the powered version has its own Ready state, it will play the Lower and Raise animations when the WeaponLevel2 powerup starts and ends. You can see this with the Staff in Heretic.
Next, we add Weapon.SisterWeapon and the +Weapon.Powered_Up flag.


Actor Pistol2 : DoomWeapon 5010
{
  Game Doom
  Weapon.SelectionOrder 1900
  Weapon.AmmoUse 1
  Weapon.AmmoGive 20
  Weapon.AmmoType "Clip"
  AttackSound "weapons/pistol"
  Weapon.SisterWeapon "PoweredPistol2"
  Obituary "$OB_MPPISTOL"
  +WEAPON.WIMPY_WEAPON
  Inventory.Pickupmessage "$PICKUP_PISTOL_DROPPED"
  States
  {
  Ready:
    PISG A 1 A_WeaponReady
    Loop
  Deselect:
    PISG A 1 A_Lower
    Loop
  Select:
    PISG A 1 A_Raise
    Loop
  Fire:
    PISG A 4
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  Flash:
    PISF A 7 Bright A_Light1
    Goto LightDone
    PISF A 7 Bright A_Light0
    Goto LightDone
  Spawn:
    PIST A -1
    Stop
  }
}

Actor PoweredPistol2 : Pistol
{
  Weapon.SisterWeapon "Pistol2"
  +Weapon.Powered_Up
  States
  {
  Fire:
    PISG A 4
    PISG BB 0 A_FirePistol //Added this just to make it different
    PISG B 6 A_FirePistol
    PISG C 4
    PISG B 5 A_ReFire
    Goto Ready
  }
}

That links them together. It should be just that easy; when a WeaponLevel2 powerup is activated the Pistol2 weapon will automatically become the PoweredPistol2 weapon. You now have a version of Doom's pistol that can be affected by a Tome of Power.

Using arguments in Decorate

argument1When I say "argument," I don't mean conflict or debate, I mean a value that is passed to a function, so the actor in question can be customized by a mapper, without editing the actual code. These values can be set in a map editor, to be used by the Decorate.

To start out, here are two helpful links:

  • A_JumpIf -- A function that can jump an amount of frames based on arguments set in the editor.
  • Decorate expressions -- A list of helpful things that can be used in the same way.

To use arguments in Decorate, you use the Args[] keyword. Args[] tells the decorate that you mean arguments. There are five values; 0 through 4, that correspond to arguments 1 through 5 in the editor. KEEP IN MIND, when we use Args[2], for example, ZDoom uses argument 3, as set in the editor. As my computer science professor said: engineers have four fingers, because they start counting at 0.

There are two ways arguments can be incorporated into Decorate:

A_JumpIf is a very flexible jump function, capable of many things (A_JumpIf( ACS_ExecuteWithResult(###) == 1 ), "SomeState"), for example, is very powerful, and is one of the best ways of combining ACS and Decorate), and even makes some of the other Jump functions obsolete. It can also be used with the Args[] keyword to reference arguments. If I want to make a jump to the pain state when the fifth argument is 0, for example, I do this:

A_JumpIf(Args[4] == 0, "Pain")

Keep in mind, A_JumpIf is like an If statement, in ACS: You have to tell it what it's comparing to. If you use A_JumpIf(Args[4], "Pain"), it won't work, because it doesn't know what it's doing with Args[4].

The second way is using Args[] directly in a parameter. This is used a lot in several effects in the SFX Shoppe. If I did this:

A_CustomMissile("Projectile", Args[1], 0, Random(-Args[2], Args[2])

It would use whatever is set in the second argument for the height of the projectile, with a random spread depending on the third argument (plugging 5 in, in the editor, would make the spread -5 to 5). Args[], to ZDoom, is just a number, and can be plugged in wherever ZDoom is looking for a number.

Also, I haven't run into this, nor do I expect anybody to, but you can use both together to avoid, for example, dividing by 0, like so:

A_JumpIf(Args[0] <1, "Fail")
A_CustomMissile("Projectile", 10/Args[0])

That way, if the first argument is 0 (which can happen easily, since arguments are 0 by default), it won't even try.

I hope this helps! A lot of things, especially SFX, are a lot more useful when a mapper can customize its behavior. If a mapper doesn't have to even open up the wad to customize how an actor will act, they can set up more interesting situations or more beautiful environments for the player, for example.

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.