🔵 << Previous: Player, PlayerInfo and PlayerPawn🔵 >> Next: Weapons, PSprite and overlays
- Overview
The word "Inventory" within GZDoom can refer to two different concepts:
-
A list of items held by an actor. Any actor can contain items, this isn't limited just to players . Monsters and even items themselves can hold other items inside them. The inventory is a special data type—a linked list. You can learn more about them in this section of the Arrays and linked lists chapter.
-
Inventory
, which is a base GZDoom class that all items are based on. Items are actors that can be picked up and are normally placed in the toucher's inventory.
This naming convention is, obviously, confusing, so in this chapter and the rest of the guide I'll use the terminology as follows:
-
inventory will refer to inventory as a list of items held by an actor;
-
Inventory
will refer to the baseInventory
class; -
item will refer to a specific item based on the
Inventory
class (or one ofInventory
's subclasses); -
pickup will refer to an item that specifically exists in the world and can be picked up by walking over it (not all items are like that).
Also, I'm going to use the term toucher to refer to the actor (usually a player pawn) that is receiving an item, because this is the name used in some of Inventory
functions (such as the Touch()
virtual function used by items to handle the process of picking up). If the actor already has the item in question, they're referred to as "owner", since the item will track them via their owner
field.
PIckups can be spawned in the map (provided they have a Spawn
sequence) and picked up by actors that have a PICKUP
flag set on them (which by default only includes actors based on PlayerPawn
). When that happens, in most cases the player pawn will get that item into their inventory. For this to happen the item must have a SPECIAL
flag set on it (which is set by default), and the item shouldn't have any flags that completely disable collision, such as NOCLIP
or NOINTERACTION
.
Note that items are interactive but they're not solid, so they're not supposed to have the SOLID
flag—if they did, the player wouldn't be able to intersect the item's hitbox and pick it up. It's also not a good idea to try and add collision to items, such as adding SHOOTABLE
or VULNERABLE
to them.
Items can also be given to actors directly by calling GiveInventory()
or A_GiveInventory
on them and specifying the item's class name and amount to give. Any actor can receive item this way, they don't need the PICKUP
flag.
This process of obtaining an item involves a whole lot of steps. Very roughly, without touching all the multiple nuances, the process looks as follows:
-
First, if the item is received in the world, when the player touches it, the item runs its
Touch()
function. This function is actually defined in Actor, but the Inventory class overrides it to run a series of checks. To handle the actual giving of the item,Touch()
runs theCallTryPickup()
function, which is a non-virtual function that performs a variety of checks. IfCallTryPickup()
is successful (the item is successfully picked up),Touch()
also plays the item's pickupsound and prints its pickupmessage for the player who picked it up. -
CallTryPickup()
performs a series of checks, such as making sure this actor can pick this item up (for example, processing its Inventory.ForbiddenTo and Inventory.RestrictedTo properties). -
First, the item will let other items already present in the toucher's inventory process itself with their
HandlePickup()
calls (more on that later).HandlePickup()
allows other items in the toucher's inventory intercept new pickups before they're picked up. As a result of that processing, the item may either be be allowed to move on to the next step, or gets destroyed, or it may be set up to be respawned later (for example, if it has thebALWAYSRESPAWN
flag, or thesv_itemrespawn
console command is enabled). This is mainly done to handle duplicate pickups, and some subclasses of Inventory also add special processing here: for example, the Weapon class uses itsHandlePickup
function so that when you pick up another weapon that you already have, you receive ammo for it instead. -
Then the item will check if the toucher already has a maximum amount of items of this class in their inventory. If they do, nothing happens. If the item was a world pickup, it stays in the world. If it was given to the actor directly (e.g. via
A_GiveInventory
), it's not given and is simply destroyed. -
If the toucher has never received this item before in the current play session:
The pickup gains the
bNOSECTOR
flag, so that it's no longer rendered or attached to any sectors. It still exists but is functionally detached from the world. After that one of the two things happens:-
In singleplayer: the item calls
AttachToOwner(toucher)
, becoming a part of its inventory list. -
In multiplayer: if the item was picked up from the world, it'll create a copy of itself, and that copy will set the toucher as
owner
, becoming a part of its inventory. After that the original pickup sets itself up to be respawned (upong respawning thebNOSECTOR
flag will be removed from it.)If the toucher has received this item before, the item doesn't place itself into their inventory; instead, it gets a pointer to an instance of itself that is already present in the toucher's inventory and increases itsamount
. After that, the origial item is either destroyed, or set (if it was a pickup and the gamemode/flags allow it), set up for respawn.
-
-
If the toucher has received this item before (i.e. they already have an instance of this item class in their inventory), then the item gets a pointer to an instance of itself in the toucher's inventory, increases its
amount
, and disappears (or sets itself up to be respawned).
The last step is especially important: when you already have an instance of an item in your inventory, picking up a new item simply increases the amount
; this is why when receiving multiple items of the same type, you don't get multiple item instances in your inventory; instead you get one instance of that item with a specific amount
value.
The complete process that takes place when an item actually involves a staggering number of checks. I've created a flow chart to represent it, however, I don't particularly recommend trying to get any practical use from it, it's purely for illustrative purposes. A more helpful flowchart is provided further in the chapter; this is for informative purposes only:
If the humongous structure above makes your head spin: do not fret. Like I said, this is for informative purposes only, and you don't have to actually refer to it while coding. You will find a more helpful instructional diagram further in the chapter.
Inventory
defines a bunch of item-specific properties and flags that have to be prefixed with Inventory.
in order to use them in the Default
block.
Here are the main Inventory
properties:
-
Inventory.amount
— the value by which the amount if the item is increased when you pick it up (1 by default). -
Inventory.maxamount
— the maximum amount; ifamount
of an item instance reaches this, you won't be able to pick up more. -
Inventory.pickupmessage
— a string printed on the screen when you pick up the item. It's not printed if the item is given directly. -
Inventory.pickupsound
— the sound played when the item is picked up. As with all sounds, this takes an SNDINFO definition, not a file name. It's not played if the item is given directly. -
Inventory.icon
— the graphic displayed in the HUD, provided the item also has theINVENTORY.INVBAR
flag set. This can either be a sprite or a graphic.
The other properties can be found on ZDoom wiki.
Here are the main Inventory
flags:
-
INVENTORY.INVBAR
— the item is displayed in the HUD while it's in inventory. -
INVENTORY.UNDROPPABLE
— the item can't be removed from actor's inventory. ImpliesUNCLEARABLE
. -
INVENTORY.UNCLEARABLE
— the item can't be cleared from the inventory by ACS scripts but can be dropped (for example, by using thedrop
console command). -
INVENRORY.UNTOSSABLE
— the item can be removed from the inventory with function calls but can't be dropped by using thedrop
console command. -
INVENTORY.IGNORESKILL
— used specifically byAmmo
(which is a subclass ofInventory
): the amount of ammo will not be doubled regardless of skill (by default the lowest and highest difficulty levels in Doom will double the amount of ammo received from pickups). -
INVENTORY.PERSISTENTPOWER
— the item is kept when travelling between levels (normally items are stripped, unless the levels belong to the same hub. -
INVENTORY.AUTOACTIVATE
— the item will call itsUse()
function when picked up. This is mostly used byPowerupGiver
.
The other flags can be found on ZDoom wiki.
Inventory
has a very large number of virtual functions. I'll cover the most important ones.
virtual void DoEffect()
This function is called every tic while the item has a valid owner
. It's similar to Tick()
, except Tick()
is run all the time, whether the item has an owner
or not. This function can be used to add behavior to the owner without having to replace their actor class.
Inventory
's own DoEffect()
is empty, but it's recommended to call super.DoEffect()
when overriding it, for consistency.
Normally, DoEffect()
won't run if the owner
is null, but under very special circumstances it can happen, so, when overriding, it's important to still null-check the owner
pointer. One notable example is when the owner has a Death
state sequence with a total duration of 0: if that happens, when the owner gets killed, the item's DoEffect()
will still run during that game's tic before the item is destroyed.
Note: 0-tic
Death
sequences are a bad practice and shouldn't be used; at least one empty state (such asTNT1 A 1
) should always be present. Still, since edge cases like this are possible, it's important to null-check theowner
.
virtual void AttachToOwner(Actor other)
This function is run when the item has been received for the first time (the toucher's inventory doesn't have instances of the item in it), so it has to be placed in the actor's inventory. Within this function, other
is a pointer to the actor who is receiving this item.
Overriding it is useful if you want to affect the toucher once, when the item is picked up. Note, the super.
call should always be present, otherwise the item won't be attached properly:
override void AttachToOwner(Actor other)
{
super.AttachToOwner(other);
// This block is only run if the item got
// successfully placed in the other's
// inventory:
if (owner)
{
owner.speed *= 0.5;
}
}
This code will reduce the owner's speed
by 50% when the item is picked up. Note, you will need to have means to restore their speed later—for example, with DetachFromOwner()
.
Note, this function isn't run for items that don't actually get placed in the inventory — notable examples are health and armor pickups (more on that later).
Note, however, that this virtual is only run when this item is received for the first time. If the toucher already has an item of this type in ther inventory, this virtual will not be called. If you want to apply some kind of an effect whenever an item is received, even if you receive more of the same item, give the item +INVENTORY.AUTOACTIVATE
and utilize its Use()
virtual instead. And if your item is based on the Powerup class, it's easier to utilize InitEffect()
instead (it's run when the Powerup becomes active).
virtual void DetachFromOwner()
This function is called when the item is destroyed and should be removed from the owner's inventory. In contrast to AttachFromOwner()
, this function doesn't have arguments because it always interacts with owner
.
Note, this function is triggered specifically when the item is removed from the inventory completely and causing it to be destroyed. Just using up all instances of the item, i.e. reducing its amount to 0, does not destroy the item: the owner still keeps an instance of the item in their inventory, but that instance's amount
is set to 0, making it unusable.
This function can be useful when you need to revert special behavior. Note, you have to call super.
in order for the actual removal to happen.
Example:
class SlowDown : Inventory
{
double prevSpeed; //this will store orginal owner's speed
int timer;
override void AttachToOwner(Actor other)
{
super.AttachToOwner(other);
// Check if the owner was established properly:
if (owner)
{
// Save owner's current speed, then reduce
// their speed by 50%:
prevSpeed = owner.speed;
owner.speed *= 0.5;
// Set up a timer:
timer = 175;
}
}
override void DoEffect()
{
super.DoEffect();
// Do nothing if the owner is frozen
// (by a 'freeze' cheat or a freeze powerup):
if (owner.isFrozen())
{
return;
}
// Increment the timer:
timer--;
// If the owner is null for some reason, or the
// timer has run out, destroy the item:
if (!owner || timer <= 0)
{
Destroy();
}
}
override void DetachFromOwner()
{
// If the owner still exists at this point,
// restore their speed:
if (owner)
{
owner.speed = prevSpeed;
}
// Proceed with item removal:
super.DetachFromOwner();
}
}
Note: This specific example is not really recommended for use, because for a timed effect like this it would make more sense to use a
Power
-based item and override itsInitEffect()
andEndEffect()
functions instead (more on those later).
virtual protected bool TryPickup (in out Actor toucher)
TryPickup()
is called by the item whenever an actor is trying to obtain that item, either by walking over it or by being given it directly (via scripts or cheats). Toucher
is a pointer to the actor receiving the item. Note, this function is not equivalent to AttachToOwner()
: AttachToOwner()
is called when the item has already been received and needs to be attached to the owner, whereas TryPickup()
is called before that, and it actually determines whether the item will be received or not. Also, as noted, AttachToOwner()
isn't run for items that don't actually get placed into inventory (such as health pickups that simply modify player's health value instead, then disappear), whereas TryPickup()
is always run, since it's the function that handles what happens when an item is received.
TryPickup()
normally returns true
when an item has been successfully picked up. For a simple example of how its return value works, let's say we're talking about a regular item that has amount
and maxamount
:
-
When you touch the item (or the item is given to you via other means),
TryPickup()
will check theamount
if this item type you already have in your inventory. -
If you haven't picked this item up before, the item will be moved into your inventory, its
amount
will be set to the same value as theInventory.amount
property on this item class (1 by default), andTryPickup()
will return true. -
If you have this item, but the amount you have is lower than
Inventory.maxamount
of the item,TryPickup()
will get a pointer to this item in your inventory, increase itsamount
, then destroy the pickup and returntrue
. -
If you already have the maximum possible amout,
TryPickup()
will returnfalse
and nothing will happen.
Normally, if you want something to happen as soon as the item is picked up, you'd only do it if TryPickup()
returns true, which would look as follows:
override bool TryPickup(in out Actor toucher)
{
// first call super. and save the result:
bool ret = super.TryPickup(toucher);
// If the result is true (the item was picked
// up successfuly), do your special behavior:
if (ret)
{
// special behavior
}
// Return the result, whatever it was:
return ret;
}
The TryPickup()
function is somewhat long (see on GZDoom Github), and each sublcass of Inventory
(such as Ammo
, Health
, etc.) adds their own behavior.
Note, overriding TryPickup()
is often difficult and is very rarely necessary. Most of the time it's easier to utilize AttachToOwner()
, InitEffect()
(for Powerups) or Use()
(combined with the Inventory.AUTOACTIVATE
flag).
Note that if you want to spawn an item first (for example, to modify its properties somehow), and then give it to the player, you should not attempt to call TryPickup()
directly. Instead there's a separate function intended to do exactly that: CallTryPickup()
(which is not virtual and isn't intended to be modified). This would look like this:
let ms = Inventory(Spawn("Soulsphere"));
if (ms)
{
ms.amount = 150;
// This assumes that the calling actor's 'target'
// pointer is not null and is probably a player:
ms.CallTryPickup(target);
}
This piece of code would spawn a Soulsphere, modify its amount
to be 150 instead of 100, and then give it to target
(which is assumed to be a player pawn). Note, this specific example wouldn't be useful in practice, it's just here to illustrate the concept.
For a more practical example, here's a special item that, by using a BlockThingsIterator would make the owner
(presumably, a player pawn) collect all health items within a 256x256 area around them:
class HealthCollector : Inventory
{
Default
{
+INVENTORY.UNDROPPABLE
+INVENTORY.UNTOSSABLE
}
override void DoEffect()
{
super.DoEffect();
// first, null-check that owner exists and is a player:
if (!owner || !owner.player)
{
// If not, destroy this item and do nothing else:
Destroy();
return;
}
// Establish a distance first:
double dist = 256;
// Create a BlockThingsIterator around the owner
// with the specified distance:
BlockThingsIterator itr = BlockThingsIterator.Create(owner, dist);
// Iterate through things found by the iterator:
while (itr.Next())
{
// Try casting the found object as Health:
let hp = Health(itr.thing);
// Check the cast was successful (otherwise
// this object isn't Health) and doube-check
// the distance from owner to it:
if (hp && owner.Distance3D(hp) <= dist)
{
// If the check was successful, attempt
// to have the item be consumed by the
// owner:
hp.CallTryPickup(owner);
}
}
}
}
This control item (which you will have to remember to give to the player, e.g. using an event handler) will force the owner (the player pawn) to automatically pick up all items that inherit from Health
(the base class for all health pickups, also a subclass of Inventory
) by calling CallTryPickup()
on them. Note, as a result of this, these items will call their own TryPickup()
functions and do what they need to do, such as checking amount/maxamount, etc.
Important note: CallTryPickup()
/TryPickup()
do not play the item's pickupsound or print its pickupmessage. Those are handled by separate virtual functions: PlayPickupSound()
and PrintPickupMessage()
respectively. By default, they're only called from the Touch()
virtual. If you want to simulate the item being received fully, i.e. with a sound and a message, just call Touch()
instead of CallTryPickup()
.
virtual void Touch (Actor toucher)
Touch()
is not an Inventory
-specific function, it's defined in Actor
, but Inventory
overrides it to handle the process of picking up an item pickup. Touch()
prints the item's pickupmessage (via PrintPickupMessage()
), plays its pickupsound (via PlayPickupSound()
), and also initiates TryPickup()
(via CallTryPickup()
).
Touch()
is essentially a "collision" function for items (and it's called after CanCollideWith()
, the main Actor collision function). It's only called for actors that have a +SPECIAL flag (which all items have by default), and interacts with actors that have a +PICKUP flag (which player pawns have by default, which lets them pick up items). Touch()
also calls PrintPickupMessage()
and PlayPickupSound()
to print the pickupmessage and play the pickupsound for the item.
Touch()
is only called for picking up items in the world, not for receiving items directly via A_GiveInventory
, GiveInventory
, console give
commands, ACS functions, etc. For this reason, if an item is given directly, it only calls CallTryPickup()
, and you don't hear a sound, get a flash or see a pickupmessage, since all of those are handled by Touch()
.
Touch()
can be called on items directly in order to give them to another actor, even if they're not colliding. Here's a variation of the example I used earlier with CallTryPickup()
, except using Touch()
:
class HealthCollector : Inventory
{
Default
{
+INVENTORY.UNDROPPABLE
+INVENTORY.UNTOSSABLE
}
override void DoEffect()
{
super.DoEffect();
if (!owner || !owner.player)
{
Destroy();
return;
}
double dist = 256;
BlockThingsIterator itr = BlockThingsIterator.Create(owner, dist);
while (itr.Next())
{
let hp = Health(itr.thing);
if (hp && owner.Distance3D(hp) <= dist)
{
hp.Touch(owner); //here's where we're using Touch()
}
}
}
}
virtual bool Use (bool pickup)
Use()
is called when an item is used. When it returns true
, the item is consumed.
"Using an item" means any of the following:
-
Selecting the item in the inventory bar and pressing the "use item" keybind (the item will need
+INVENTORY.INVBAR
set in order to show up in the HUD at all) -
Using the
use <itemname>
command either from a console or via a dedicated keybind -
If the item has
+INVENTORY.AUTOACTIVATE
set, it gets used automatically when received
Note that for the item to be usable, its Use()
function has to do something and then return true
. The base Inventory
class has an empty Use()
call that simply returns false
, so by default items are not usable.
There are also some subclasses of Inventory
that are defined to be useable: for example, HealthPickup
(a base for useable healing items, such as Hexen's Quartz Flask) or PowerupGiver
(a pickup for powerups that gives a related PowerUp
item upon use). Note that all Doom's PowerupGivers have the AUTOACTIVATE
flag set, so they're used automatically on pickup, but for example in Hexen and Heretic PowerupGivers are usually useable manually.
In DECORATE the only way to make a custom useable item was to inherit from CustomInventory
or classes such as HealthPickup
, PowerupGiver
, etc., since DECORATE doesn't have access to virtual functions. In ZScript, however, you simply need to inherit from Inventory
and then override Use()
.
virtual void ModifyDamage(int damage, Name damageType, out int newdamage, bool passive, Actor inflictor = null, Actor source = null, int flags = 0)
This function is primarily used by powerups such as PowerProtection and PowerDamage to modify the damage that the owner deals or receives. However, it's defined in the base Inventory
class, so you can set up any item in your inventory (a weapon, for example) to modify the incoming/outgoing damage.
Source
is the actor responsible for the damage—such as a player or a monster that used the attack. Inflictor
is a projectile or a puff used by said attack (and in many cases it can be null, e.g. in case of melee or hitscan attacks wh
While it has no return value, newdamage
is the outgoing value that will determine the final damage amount.
By checking for passive
you determine whether you want to modify outgoing damage (passive
is false) or incoming damage (passive
is true).
This function is documented and has examples on ZDoom Wiki.
This function's passive part is not called if the damage inflictor is set up to ignore protection, such as using a DamageMobj()
call with DMG_NO_PROTECT
flag. Its active part is, similarly, not called if the damage inflictor used by the player utilizes that flag.
virtual void AbsorbDamage (int damage, Name damageType, out int newdamage, Actor inflictor = null, Actor source = null, int flags = 0))
AbsorbDamage()
is a function very similar to ModifyDamage()
, except it's run after it, and is normally used for armor rather than PowerProtection/PowerDamage. In contrast to ModifyDamage()
, AbsorbDamage()
only works in passive mode.
Overriding this function can be used similarly to ModifyDamage()
to alter how much damage the owner receives. Note that it can also be bypassed, but the conditions are different from those of bypassing ModifyDamage()
: AbsorbDamage()
isn't called if the inflictor is set to bypass armor specfiically, e.g. if it's a projectile with +PIERCEARMOR
flag, or a DamageMobh()
call with the DMG_NO_ARMOR
flag.
void DepleteOrDestroy()
This is a non-virtual function that by default is called when an item's amount in your inventory reaches 0. It determines whether the item should remain in your inventory (with its amount set to 0, so it's still unusable) or be removed completely.
If you want to make sure a specific item (like a control item that exists only to add some special effects/behaviors to the owner) is removed when a certain condition is met, use Destroy()
instead.
If the item is destroyed, it calls DetachFromOwner()
first, so you can still override that to add some "when this item is removed" behaviors.
virtual bool HandlePickup (Inventory item)
HandlePickup()
is an interesting virtual function: it's called on all items in your inventory when you receive any new item. With this, items in your inventory can process other items you're receiving and do something with that data. This can even be used to override what items you can or cannot receive. The structure of this function, with examples, is documented on ZDoom Wiki, so I won't go into details here.
This function is actually very useful in gameplay mods where you replace the original Doom items with your modded versions. Usually, mods have this problem where the player can receive both the vanilla items and the modded items. This can happen if the player uses a give all
cheat, or they play through a map that has ACS scripts that, for example, remove all your weapons and then give you back a standard Fist or Pistol. This is inconvenient, because those vanilla weapons, despite not being bound to slots, may show up in your HUD (depending on how it's set up), and the player may even be occasionally auto-switched to those weapons when they run out of ammo for a mod's weapon.
You can circumvent this by having one special control item in the player's inventory, and using that control item's HandlePickup()
override to intercept the other items the player attempts to receive (by any means) and, for example, give other items instead. However, please note that this is not a surefire way to catch other items:
-
If multiple items call
HandlePickup()
to process the same item, then multipleHandlePickups()
call run in stack succession, i.e. beginning with the item you received last and towards the item you received first. As a result,HandlePickup()
will not be able to process items that have their ownHandlePickup()
override, if those items were received later. -
In addition, if an item runs
HandlePickup()
first and returnstrue
, it blocks otherHandlePickup()
calls that come later in the stack from processing the item. -
Finally, if an item overrides
TryPickup()
and does not callCallHandlePickup()
in it,HandlePickup
simply will not be called for this item at all, and it won't be processed by it.
virtual string PickupMessage()
This function returns the string that functions as the item's pickupmessage
(gets printed on the screen when the item is picked up). Note, when you set the value of Inventory.pickupmessage
in the item's Default block, this value isn't printed directly; instead it's saved in an Inventory
-specific variable pickupMsg
, which is then processed by the PickupMessage()
function. By default it does this:
virtual String PickupMessage ()
{
return PickupMsg;
}
(See GZDoom Github)
You can override this function to add more complex pickup messages. For example, let's say you want your ammo pickups to print the amount you receive when picking up, not just a standard line like "Picked up a clip". This can be done:
class Clip_Test : Clip
{
override string PickupMessage()
{
// First retrieve and localize the default
// pickupmessage for this item:
string pkm = StringTable.Localize(pickupMsg);
// Now use String.Format() to combine the
// default message with a number and pass
// the item's amount to this number:
return String.Format("%s (+%d)", pkm, amount);
}
}
When this Clip is picked up, you'll see "Picked up a Clip (+10)" as the pickupmessage.
Note that all vanilla items use LANGUAGE lump references, rather than literal strings in their code (e.g. for Clip it's "$GOTCLIP"
), and it's always a good idea to keep your text strings in that lump rather than write them out literally in ZScript.
LANGUAGE strings need to be localized with the StringTable.Localize()
function to be printed, otherwise the identifier will be printed literally, such as "$GOTCLIP". By default PickupMessage()
doesn't do it, because when it simply returns pickupMsg
, the engine will localize that string automatically. However, when you're returning a more complex value—such as our formatted string there—and only a part of it needs localization, you have to do it manually in ZScript, hence the StringTable.Localize()
call.
virtual void PlayPickupSound (Actor toucher)
PlayPickupSound()
is the function that plays the item's pickupsound
as defined in its properties. It handles various flags and attenutation for the sound.
One potentially useful feature that may require overriding is function is if you want pickupsounds from different pickups to not cut each other off (which they will do by default). Unfortunately, you'll have to copy-paste the whole contents of the original override to inject your own behavior, since it has no return value and doesn't have a flags argument. To do this you will have to copy the entire function, but replace the last line as follows:
flags |= CHANF_OVERLAP;
toucher.A_StartSound(PickupSound, chan, flags, 1, atten);
Addign CHANF_OVERLAP
to the flags
value (which is defined at the start of the override) will let the various pickupsounds play over each other on the same channel.
The full example of how to do this is provided on ZDoom Wiki.
With all of this information, you may be easily confused about which virtual function you need to override to add custom behavior. Worry not, for I have prepared a flowchart guide that should answer this question (most of the time, anyway), and it's a lot more useful than the previous one (but still quite big):
There are multiple special subclasses of inventory that define their own special behavior. All of these subclasses support Inventory
's virtual functions, properties and flags, being its descendants, but their behavior can be rather different from standard items. Here I'll provide a brief overview of how some of the most common variations are different.
There is a number of different healing items used in various games supported by GZDoom. Their code can be found on GZDoom Github.
Health
is the base class used by simple "pick up to heal" items, such as Doom's Stimpack, Medikit or Soulsphere. These items are never placed in the toucher's inventory. When picked up, they check the toucher's health
value against their own maxamount
value, and if the former is smaller than the latter, the item will add its amount
to the toucher's health
and destroy itself. Otherwise the pickup will do nothing and stay in the world (unless it has the INVENTORY.ALWAYSPICKUP
flag enabled, in which case the pickup will simply be destroyed).
HealthPickup
is a different healing item class, unrelated to Health
. It gets put in the player's inventory like a traditional item. In contrast to Inventory
, however, its Use()
function isn't empty: instead, when an item of this type is used, it'll attemp to heal the owner and, if successful, Use()
will return true
, reducing the amount
of this item in your inventory. Hexen's Quartz Flask (internally ArtiHealth
) is an example of this.
HealthPickup
has another nifty property: HealthPickup.Autouse
. When the owner's health reaches the value specified in this property, this item will be used automatically, if present. (In case of the aforementioned Quartz Flask, the value is 1.)
Note, not all health pickups are based on these two. For example, Doom's Megasphere, which givs both armor and health, is actually a CustomInventory
that gives two separate items based on Health
and Armor
(as seen here).
Armor
is a variation of Inventory
that defines virtually no special behavior and serves as a base for other armor classes. It should never be given directly. New armors you create also shouldn't be based on Armor; you want BasicArmorPickup or BasicArmorBonus most of the time.
The main armor class is BasicArmor
: the player always has an instance of BasicArmor
in their inventory, this is the class that actually handles damage reduction (via an AbsorbDamage()
override). BasicArmor
defines a number of custom properties, such as savepercent
(by how much the damage is reduced) and actualSaveAmount
(the amount of armor, calculated from saveAmount
of armor pickups).
Armor pickups are never placed in your inventory; instead they get access to BasicArmor
in the toucher's inventory and modify its properties, then get removed (or set up for respawning). This follows the same logic as health pickups that modify the toucher's health and are then removed.
BasicArmor
never functions as a pickup and is already present in the player's inventory on map start. Whenever you need to get information about the player's current armor, such as its amount, damage reduction or even icon, you need to do it by getting a pointer to BasicArmor
in their inventory and reading information off it, such as let arm = BasicArmor(ppawn.FindInventory("BasicArmor"));
(where ppawn
is a pointer to the PlayerPawn you need).
Another variation of basic armor exists known as HexenArmor
. It's an analog of BasicArmor
made specifically for Hexen, which consits of 4 separate armor pieces held in an array, with a special formula for armor reduction based on which pieces you have. In principle it functions the same way as BasicArmor
, and, just like BasicArmor
, it's always present in the player's inventory, even if you're not playing Hexen. It's fairly rarely used in custom projects due to added complexity.
You can learn more about HexenArmor on ZDoom Wiki.
Classes:BasicArmorPickup - ZDoom Wiki
BasicArmorPickup
is, as its name implies, the base class for all armor pickups—the armor items that you can actually spawn in the world and pick up. As mentioned above, the armor pickups are never placed in your inventory, instead they get a pointer to BasicArmor
in your inventory and modify its values. This is the class that the various in-game armors are based on.
BasicArmorPickup
overrides the Use()
function to set the player's armor values; as such, it has +INVENTORY.AUTOACTIVATE
set by default so that the Use()
function is actually called.
Note that armor amount can be modified based on the game's difficulty level, if a custom difficulty level is set up to include that modifier. Armor pickups check G_SkillPropertyFloat(SKILLP_ArmorFactor))
in order to get that modifier.
Classes:BasicArmorBonus - ZDoom Wiki
BasicArmorBonus
is somewhat similar to BasicArmorPickup
with 2 main differences:
-
It's set up to be always picked up by default (due to
+INVENTORY.ALWAYSPICKUP
) -
If you already have armor, it never modifies its
savepercent
, only its amount.
This means that armor bonuses are set up to add to your current armor; they only give you armor if you had no armor before.
This is how Dooms' ArmorBonus
functions: it will always add to your armor, so if you had Blue Armor before (50% damage reduction), it won't be reverted to another armor type, and so on.
Weapon
is a subclass of StateProvider
, which is a subclass of Inventory
. Weapons are also items, but they define a whole lot of custom behavior, such as:
-
The ability to draw sprite animations on the screen, which is defined in
StateProvider
and inherited byWeapon
. Note that the sprite animation itself is handled by another class,PSprite
. (This is covered in detail in the Weapons, Overlays and PSprite chapter. -
Weapons consume
Ammo
when fired. They also giveAmmo
, if you're picking a weapon that you already have. -
Weapons also define a slew of custom functions for attacking, PSprite manipulation and other things.
Aside from that, weapons also function as items: they have access to all Inventory
functions and they even utilize their amount
, so it's possible, for example, to check the amount of your current weapon and display different animations (such as displaying a dual-wield animation when the amount is more than 1).
Ammo
is a base class for ammo items, which are normally required to use weapons. Like most items, ammo items get placed into the toucher's inventory when they're picked up for the first time, and after that the instance of that ammo is never removed, even if its amount reaches 0. Picking up new ammo pickups of the same time increases the amount of the ammo item in your inventory.
Ammo utilizes HandlePickup()
overrides to handle the process of picking up ammo. It also checks if the current difficulty level has a factor affecting how much ammo you're supposed to get (in Doom you get double ammo on "I'm Too Young To Die" and "Nightmare!", for example).
What's also special about Ammo
is that it's designed in a way that only inheriting from Ammo
directly creates a new ammo type, whereas inheriting again from a custom ammo type simply creates a new pickup of the same type.
This behavior can be changed (for example, if you want to create a custom version of the base Ammo
class) by overriding Ammo
's GetParentAmmo
function. For example:
class MyBaseAmmo : Ammo
{
override Class<Ammo> GetParentAmmo ()
{
class<Object> type = GetClass();
// This part adds MyBaseAmmo class to exceptions,
// so that it's not considered a new ammo type:
while (type.GetParentClass() != "Ammo" && type.GetParentClass() != "MyBaseAmmo" && type.GetParentClass() != NULL)
{
type = type.GetParentClass();
}
return (class<Ammo>)(type);
}
Classes:PowerupGiver - ZDoom Wiki Classes:Powerup - ZDoom Wiki
PowerupGiver
is a class for items that give the owner a related Powerup
when used. PowerupGivers themselves don't have any effects, they're just items, but they're the ones that function as pickups. The actual powerup effect, however, is always defined in a Powerup
-based class, which is attached to its respective PowerupGiver via its Powerup.Type
property.
PowerupGivers are also placed into player's inventory by default, and they give the owner
their related powerup in their Use()
function. Most powerup givers in Hexen or Heretic, for example, are meant to be used from the inventory bar, whereas in Doom all powerup givers have +INVENTORY.AUTOEFFECT
set, which makes them activate immediately upon pickup.
If a powerup is never meant to be spawned in a map and function as a pickup, it doesn't need a PowerupGiver at all.
Powerup
is a class based on Inventory
that is designed to provide some special effects for its owner
while it's in their inventory. Powerups utilize various functions to handle those effects (such as using ModifyDamage()
to modify the incoming/outgoing damage, for example). The key features of Powerup
compared to Inventory
are:
a
-
Effect duration tracked via an integer variable
effectTics
. This value is reduced in the powerup'sDoEffect()
override, and once it reaches 0, the powerup destroys itself. -
InitEffect()
andEndEffect()
virtual functions that determine what happens when the powerup activates and deactivates (including when its duration runs out).
Powerup
doesn't define any special behavior by itself. The actual powerups are classes such as PowerDamage
, PowerProtection
, PowerHighJump
, PowerInvulnerability
and others. You can also easily create your own powerups by inheriting from Powerup
and overriding its InitEffect()
, EndEffect()
and, if necessary, DoEffect()
, to determine its effects. For example:
class InvulnerabilityAndReflect : Powerup
{
uint prevRenderstyle;
Default
{
// Negative duration sets the duration
// in seconds rather than tics:
Powerup.Duration -30;
}
override void InitEffect()
{
super.InitEffect();
if (owner)
{
owner.bINVULNERABLE = true;
owner.bREFLECTIVE = true;
// Save the owner's current renderstyle:
prevRenderstyle = owner.GetRenderstyle();
owner.A_SetRenderstyle(owner.alpha, STYLE_Stencil);
owner.SetShade("FF0000");
}
}
override void EndEffect()
{
super.EndEffect();
if (owner)
{
owner.bINVULNERABLE = false;
owner.bREFLECTIVE = false;
// Restore owner's renderstyle to what it was:
owner.A_SetRenderstyle(owner.alpha, prevRenderstyle);
}
}
}
This custom powerup makes the onwer invulnerable, reflects projectiles that hit them, and also changes their renderstyle so that they appear in a flat red color. It'll work both on monsters and players (note, for players it'll also color their weapon sprite).
Powerup
and PowerupGiver
also has one more interesting interaction: there's a number of properties that can be defined either in PowerupGiver or Powerup, and they will have the same effect. For example, Powerup.Color
, Powerup.Duration
, as well as certain powerup-specific properties such as Powerup.Strength
, can be defined in either class. If they're defined in the PowerupGiver, the respective Powerup will automatically gain those features as soon as the powerup giver gives the powerup to the player.
Note that if you want to create a powerup that is never meant to be spawned in the map or picked up, and instead should only be given directly, creating a PowerupGiver for it is not necessary. You can simply define a Powerup and its properties as you wish and give it to the player directly.
Powerup
is also a good base not only for powerups, but for any sort of timed effect in general, as long as that effect is meant to be attached to an actor. For example, if you want to attach a damage-over-time effect to a monster when they're hit with your fire projectile, and also spawn some fire/smoke particles around them, you can do this by creating a custom powerup with a specific duration and spawns those effects in DoEffect()
on its owner
.
Classes:CustomInventory - ZDoom Wiki
CustomInventory
is a class that used to be very heavily utilized in DECORATE because it allows for some basic scripting. In ZScript its use is much more limited but not non-existent.
CustomInventory
is designed to support special state sequences: it enters its Pickup
sequence when it's picked up, and its Use
sequence when it's used (which is either when the player selects it and uses it, or automatically if it has +INVENTORY.AUTOACTIVATE
set. Using those state sequences, you can add special behavior to it by directly putting it into those states. Note that these states don't support duration or animation, they can only execute functions. An example of CustomInventory
used in the original code is Doom's Megasphere, which looks as follows:
class Megasphere : CustomInventory
{
Default
{
+COUNTITEM
+INVENTORY.ALWAYSPICKUP
+INVENTORY.ISHEALTH
+INVENTORY.ISARMOR
Inventory.PickupMessage "$GOTMSPHERE";
Inventory.PickupSound "misc/p_pkup";
}
States
{
Spawn:
MEGA ABCD 6 BRIGHT;
Loop;
Pickup:
TNT1 A 0 A_GiveInventory("BlueArmorForMegasphere", 1);
TNT1 A 0 A_GiveInventory("MegasphereHealth", 1);
Stop;
}
}
Note:
BlueArmorForMegasphere
is essentially a clone of BlueArmor, andMegasphereHealth
is a simple item that provides 200 health up to the maximum of 200. They're defined right next to the Megasphere in the code, as seen here.
In ZScript this specific feature isn't really necessary, since you can use virtual overrides (Use()
, AttachToOwner()
, TryPickup()
) to achieve the same results.
However, CustomInventory
has one more feature that still makes it relevant: it's not based directly on Inventory
, but rather on StateProvider
. This means that it's the only class aside from Weapon
that is allowed to utilize PSprite
and draw sprite animations on the screen. This can be useful in specific instances, for example if you want to create a quick secondary weapon (such as Duke3D-styles quick kick) that can be activated at a button press and is completely independent from the actual weapon you currently have selected. Doing this, however, requires several specific steps, which will be described in Weapons, Overlays and PSprite.
Other notable subclasses of Inventory are described on ZDoom Wiki:
🔵 << Previous: Player, PlayerInfo and PlayerPawn🔵 >> Next: Weapons, overlays and PSprite