[CODE] Build-Style Toggleable Powerups

Post your example zscripts/ACS scripts/etc here.
Forum rules
The Projects forums are only for projects. If you are asking questions about a project, either find that project's thread, or start a thread in the General section instead.

Got a cool project idea but nothing else? Put it in the project ideas thread instead!

Projects for any Doom-based engine (especially 3DGE) are perfectly acceptable here too.

Please read the full rules for more details.
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

[CODE] Build-Style Toggleable Powerups

Post by Mikk- »

I found the need for a not-so-hacky implementation of Build Style toggleable powerups, so I wrote a clean, flexible, ZScript based toggleable powerup system. I felt it would be a nifty resource for those that may want to create their own powerups that work this way.

To create your own actor inherit from the ToggleableInventory class, set up your properties (type, depletion period & regeneration period, icons etc.) and voila! You can also inherit from the ToggleableInventory class with DECORATE, should you need to!

Code: Select all

class ToggleableInventory : customInventory
    {
    // whether the item is toggled "on" or "off"
    bool active;
    
    // the internal ticker (as opposed to performing expensive modulo operations).
    int ticker;
    
    // the type of powerup to toggle.
    property Type: Type; class<powerup> type;    

    // the time in tics (or negative values for seconds) it takes to deplete 1 unit
    property RegenPeriod: RegenTics; int RegenTics;         
    
    // The sounds to play when [De]Activating the powerup...
    property ActivateSound: ActivateSound; sound ActivateSound;
    property DeactivateSound: DeactivateSound; sound DeactivateSound;
    // ...Or to play when fully depleted. 
    property DepletedSound: DepletedSound; sound DepletedSound;
    
    // the time in tics (or negative values for seconds) it takes to regenerate 1 unit, 0 implies no regeneration.
    property DepletePeriod: ActiveTics; int ActiveTics;    
    
    // define the custom flags.
    private int ti_Flags;
    
    flagdef CheckFloorRegen:     ti_Flags,     0;    // only regenerate the powerup if the player is on the ground.
    flagdef ActiveOnly:         ti_Flags,    1;    // powerup can only be activated, not deactivated.
    
    default
        {
        // the default depletion period is 1 second.
        ToggleableInventory.DepletePeriod -1;        
        // the default regeneration period is 3 seconds.
        ToggleableInventory.RegenPeriod -3;      
        
        // the sounds to play when certain things happen
        ToggleableInventory.ActivateSound     "";
        ToggleableInventory.DeactivateSound "";
        ToggleableInventory.DepletedSound     "";
        
        inventory.Amount 100;                            
        inventory.MaxAmount 100;            
        inventory.interhubamount 100;
        +INVENTORY.INVBAR;
        // clear this flag on inheriting classes should you wish for the item to be
        // destroyed when depleted.
        +INVENTORY.KEEPDEPLETED;
        }
    
    override void AttachToOwner(actor other)
        {
        super.AttachToOwner(other);
        
        // if the period is negative, make positive & multiply by ticrate (35 in most cases).
        if(ActiveTics < 0)                                
            ActiveTics = (-ActiveTics) * TICRATE;
        
        if(RegenTics < 0)
            RegenTics = (-RegenTics) * TICRATE;
        }
        
    
    override void DoEffect()
        {
        if(active && amount < 1)                                    
            {
            // if the amount is 0, then remove the powerup.
            owner.TakeInventory(type, 1);    
            
            // turn the active state to false.      
            active = false;        
            
            // play the "Fully Depleted" sound.
            owner.A_StartSound(DepletedSound, CHAN_BODY);
            
            // if the item is not to be kept when reaching 0 units, remove it.   
            if(!bKEEPDEPLETED)                            
                DepleteOrDestroy();
                    return;
            }
            
        // if for some reason the powerup is no longer in the player's inventory
        // while active, deactivate the ToggleableInventory class.        
        if(active && !owner.CountInv(type))
            active = false;
        
        // deplete/regenerate the amount if ticker is equal to the DepletionPeriod/RegenPeriod.
        if(active && ++ticker == activeTics)    
                {
                ticker = 0;
                amount = max(amount - 1, 0);
                    return;
                }
            
        if(!active && amount < maxAmount && RegenTics && ++ticker == RegenTics)
            {
            ticker = 0;
            
            // If the +ToggleAbleInventory.CheckFloorRegen flag is enabled, don't perform regen.
            // This is to prevent regeneration while falling (good for jetpacks and stuff)
            if(bCheckFloorRegen && !owner.player.OnGround)
                return;
                
            amount = min(amount + 1, maxAmount);
                return;
            }
        }
        
    // this function simply substiutes GiveInventory to modify the given powerup's duration.    
    void GiveToggleablePowerup()                        
        {
        owner.GiveInventory(type, 1);        
        
        // find the powerup in the owner's inventory
        let i = PowerUp(owner.FindInventory(type));        
        
        // set the duration to around 2 years of real time, (might as well be infinite)
        // note: it is set to '0x7FFFFFFD' instead of '0x7FFFFFFF' to work with PowerTimeFreezer properly.
        if(i)
            i.EffectTics = 0x7FFFFFFD;                    
        }                                                
    
    // these virtual functions can be overridden in inheriting classes
    // to add things such as particle effects, sounds and other stuff
    // when the powerup is activated or deactivated.
    // don't forget to call the super. otherwise it will not play any sounds.
    virtual void ActivatePowerup()
        {
        owner.A_StartSound(ActivateSound, CHAN_BODY);
        }
        
    virtual void DeActivatePowerup()
        {
        owner.A_StartSound(DeactivateSound, CHAN_BODY);
        }
    
     states
        {
        Use:
            TNT1 A 0
                {
                // if there are no units left to consume 
                // don't activate the powerup.
                if(invoker.amount < 1)            
                    return;
                
                if(invoker.active)                        
                    {
                    //if the ACTIVEONLY flag is set, then skip deactivating the powerup.
                    if(invoker.bActiveOnly)
                        return;
                        
                    // remove the powerup if active... 
                    // these blocks are where you can also play sounds, visual flashes etc.
                    invoker.owner.TakeInventory(invoker.type, 1);
                    
                    // Perform audio & visual stuff in this function
                    // (can be overridden)
                    invoker.DeactivatePowerup();
                    }
                else    
                    {
                    // Give the powerup as defined in the function.
                    
                    invoker.GiveToggleablePowerup();    
                    // Perform audio & visual stuff in this function, too.
                    invoker.ActivatePowerup();
                    }
                
                // set the ticker to 0 when toggled.
                invoker.ticker = 0;
                // toggle the state of the powerup.
                invoker.active = !invoker.active;        
                }
            fail;                                        
        } 
    }
and here are a couple of examples of togglable powerups:

Code: Select all

// in zscript...
class ToggleableWings : ToggleableInventory
    {
    default
        {
        ToggleableInventory.Type "PowerFlight";
        
        ToggleableInventory.DepletePeriod 10;    
        ToggleableInventory.RegenPeriod -2;        
        
        // cannot regenerate power when in the air.
        +ToggleableInventory.CHECKFLOORREGEN;
        }
    
    // causes an orange flash when activated.
    override void ActivatePowerup()
        {
        super.ActivatePowerup();
        
        owner.A_SetBlend("ff9900", 0.5, 25);
        }
    }
 
// in DECORATE   
actor ToggleableRadsuit : ToggleableInventory
    {
    ToggleableInventory.Type "PowerIronFeet"
    
    // the period here is 2 seconds.
    ToggleableInventory.DepletePeriod -1

    // 0 implies that there is no regeneration.        
    ToggleableInventory.RegenPeriod -1        

    ToggleableInventory.ActivateSound     "switches/normbutn"
    ToggleableInventory.DeactivateSound "switches/exitbutn"
    ToggleableInventory.DepletedSound     "brain/spit"

    // clear the INVENTORY.KEEPDEPLETED flag, should you want the powerup to be removed upon depletion.
    -INVENTORY.KEEPDEPLETED                        
    }                            
    } 
Last edited by Mikk- on Fri Apr 23, 2021 5:35 am, edited 6 times in total.
User avatar
Ac!d
Posts: 345
Joined: Tue Apr 02, 2019 5:13 am
Location: France

Re: [CODE] Build-Style Toggleable Powerups

Post by Ac!d »

I don't know if we can call this an issue but, If I hit an exit map with a ToggleableInventory still activated, the ToggleableInventory will still deplete itself and the powerup won't be activated for the next level.

Edit : You must add the flags +INVENTORY.HUBPOWER and / or +INVENTORY.PERSISTENTPOWER to the powerups used for ToggleableInventory.Type
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

Re: [CODE] Build-Style Toggleable Powerups

Post by Mikk- »

Updated the code with a few changes. It addresses the issue that Ac!d raised, albeit in a different way. It checks whether the player has the powerup when moving between maps/hubs, if the powerup isn't found, the main item is deactivated.

You may still want to add the +INVENTORY.PERSISTENTPOWER / +INVENTORY.HUBPOWER flags, if that's the sort of behaviour you require.
User avatar
Xim
Posts: 2086
Joined: Fri Feb 20, 2009 2:46 pm
Location: somewhere with trees

Re: [CODE] Build-Style Toggleable Powerups

Post by Xim »

This is an excellent script. Although I had to come up with a little hack for the item to play a sound when used, because it doesn't seem to perform the use sound assigned (at least in decorate), so I added a "Use:" state like this:

Code: Select all

Use:
  TNT1 A 0 A_StartSound("misc/gasmask", CHAN_BODY)
  Goto Super::Use
Probably a better way to do it, I'm not much of a coder. But this seems to work for now.

PS: Might be cool to have a version that uses an ammo type, but that'd probably be a whole other project. :P
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

Re: [CODE] Build-Style Toggleable Powerups

Post by Mikk- »

Neat idea - I've tweaked the code a little in the main post. I've added three new properties, two virtual functions and a custom flag & modified the timers to work with an internal timer instead of performing a modulo operation on the item's age.
The new properties are all related to sound: (De)ActivateSound & DepletedSound, the (De)Activated sounds are obviously played when the item is activated or deactivated. The Depleted sound is played once the item's amount is reduced to 0.

The virtual functions ActivatePowerup & DeactivatePowerup have been implemented to help with incorporating custom special effects. With an override of these functions you can spawn actors or particles or call A_SetBlend. Really handy to use when you want to add a little more flair when activating or deactivating your powerups. Just make sure to call the super. of the function you're overriding otherwise it may not play the sounds!

Finally the custom flag is +ToggleableInventory.CHECKFLOORREGEN, this flag disables powerup regeneration when the player is not currently standing on the floor - This is useful in the case of a flight powerup, say for example your flight powerup expires mid air, this prevents cheesing of fall damage by letting it regenerate one point and being able to cancel your momentum.

On the idea of using ammo as a counter: it's an interesting idea, however it may be better as a separate item to prevent clutter with the main code. I'll take a look.
User avatar
Ac!d
Posts: 345
Joined: Tue Apr 02, 2019 5:13 am
Location: France

Re: [CODE] Build-Style Toggleable Powerups

Post by Ac!d »

I have a question : how can I reproduce a "Duke Nukem 3D Steroids" Toggleable Powerup without deactivation and make it working with a regeneration if wanted by the coder ?
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

Re: [CODE] Build-Style Toggleable Powerups

Post by Mikk- »

I've updated the code with a new flag - This can be set with with +ToggleableInventory.ACTIVEONLY.

This flag will make it so that when the powerup is activated - it will drain all of its power and +Inventory.KEEPDEPELTED flag is present, the item will begin to regenerate once it is fully drained.

Hope this helps!
User avatar
Ac!d
Posts: 345
Joined: Tue Apr 02, 2019 5:13 am
Location: France

Re: [CODE] Build-Style Toggleable Powerups

Post by Ac!d »

It doesn't work like i expected. When I press the 'use item' button if the toggleable powerup is activated, it will deactivate him.
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

Re: [CODE] Build-Style Toggleable Powerups

Post by Mikk- »

Did you copy the updated code from the OP? You will also have to add the new flag to your custom actor. For example

Code: Select all

class ToggleableWings : ToggleableInventory
    {
    default
        {
        ToggleableInventory.Type "PowerFlight";
        
        ToggleableInventory.DepletePeriod 10;    
        ToggleableInventory.RegenPeriod -2;        
        
        // cannot regenerate power when in the air.
        +ToggleableInventory.CHECKFLOORREGEN;
        +ToggleableInventory.ACTIVEONLY; // this line must be present.
        
        }
    
    // causes an orange flash when activated.
    override void ActivatePowerup()
        {
        super.ActivatePowerup();
        
        owner.A_SetBlend("ff9900", 0.5, 25);
        }
    } 
User avatar
Ac!d
Posts: 345
Joined: Tue Apr 02, 2019 5:13 am
Location: France

Re: [CODE] Build-Style Toggleable Powerups

Post by Ac!d »

I copy the updated code from the OP and added the new flag to my custom actor. But I can still deactivate my powerup if activated.

Code: Select all

Class ToggleableBerserk : ToggleableInventory Replaces Berserk // The effect of this item cannot be deactivated once activated. He will drain completely. (In theory...) 
{
    Default
    {
        ToggleableInventory.Type "PowerStrength";
        ToggleableInventory.DepletePeriod 5;
        ToggleableInventory.RegenPeriod 0;
        Inventory.InterHubAmount 100;
        -INVENTORY.KEEPDEPLETED
        +INVENTORY.UNDROPPABLE
        +INVENTORY.UNTOSSABLE

        Inventory.PickupMessage "$GOTBERSERK";
        Inventory.PickupSound "misc/p_pkup";
        Tag "$TAG_PSTR";

        +ToggleableInventory.ACTIVEONLY;
        +COUNTITEM
    }
    States
    {
    Spawn:
        PSTR A -1 Bright;
        Stop;
    }
}
User avatar
Mikk-
Posts: 2274
Joined: Tue Jun 30, 2009 1:31 pm
Location: Somewhere off Kanagawa

Re: [CODE] Build-Style Toggleable Powerups

Post by Mikk- »

Interesting, it appears that there's some weird stuff going on with my flags - if the +ToggleableInventory.CHECKFLOOREGEN flag is enabled, then the powerup will work as intended. Which is very odd behaviour. I'll look into it.

edit: It seems that I was using the flags incorrectly. To solve the issue I had to remove any comparisons to the ti_Flags int, e.g if(ti_Flags & bCheckFloorRegen) ---> if(bCheckFloorRegen). The updated code in the main post should solve all your issues!
User avatar
MObreck
Posts: 64
Joined: Fri Sep 04, 2020 8:08 pm

Re: [CODE] Build-Style Toggleable Powerups

Post by MObreck »

Thanks for this. I'm using it for a few of the inventory items in the TC I'm working on.

There is one exploit in your code: since the internal ticker is reset every time the powerup is toggled a player can cheat the depletion rate by rapidly toggling the powerup on/off so the ticker never has time to actually deplete the PowerUp's count. Not a viable exploit for something like flying/radsuit but definitely for something like light amp goggles. I disabled the ticker reset for my TC.

Return to “Script Library”