• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Proper Bone Decay Suspension System

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
Introduction

Proper Bone Decay Suspension (PBDS) allows users to properly suspend/pause the bone decay of units, and also resume it at will. Additionally, the script itself contains every piece of information required to understand why it's necessary, how it works, what it contains, how to implement, and use it. However, I'll also add any relevant information in a less verbose manner here as well.

How to Implement
  1. If one downloads the test map, open its trigger editor, and copy the folder named "PBDS" into their map.
    • Additionally, the script can be copy and pasted without touching the test map.
  2. Set the two following variables:
    • FLESH_DECAY should be what your map's Decay Time (sec) - Flesh gameplay constant is.
    • BONE_DECAY should be what your map's Decay Time (sec) - Bonesgameplay constant is.
      • By default these constants are 2.00 and 88.00 respectively. They can be accessed under Advanced → Gameplay Constants...
  3. You're done.
    1. Furthermore, in the Lua version, these variables are called Setup.FleshDecay and Setup.BoneDecay respectively.
    2. Also, if one wishes to copy just the Lua script, PBDSInit() has to be called manually on map initialization. Copying the folder from the test map automates this process.
How to Use

Call the following function on units currently in the bone decay phase to suspend/resume it:
vJASS:
function PBDSUnitSuspendDecay takes unit u, boolean suspend returns nothing
Lua:
function PBDSUnitSuspendDecay(u, suspend)

Suspension example:
vJASS:
call PBDSUnitSuspendDecay(myUnit, true)
Lua:
PBDSUnitSuspendDecay(myUnit, true)
With the second passed argument being true, the bone decay of myUnit is suspended.

Resume example:
vJASS:
call PBDSUnitSuspendDecay(myUnit, false)
Lua:
PBDSUnitSuspendDecay(myUnit, false)
With the second passed argument being false, the bone decay of myUnit is resumed.

Supported Patch

In the library itself I recommend using at least the version I saved it on, but 1.31 should just about be fine. Anything before that would most likely not work, as I'm using new natives. I didn't test the system on older patches, so my personal recommendation stays 1.32.10.

Library (JASS)

Script (Lua)

Changelog


As mentioned before, copying this into one's map is enough without touching the test map.
Note: The script here has FLESH_DECAY and BONE_DECAY set to their default values, while the test map has a custom BONE_DECAY value. Keep this in mind.
vJASS:
library ProperBoneDecaySuspension initializer PBDSInit /* v1.2.1
********************************************************************************
*                                                                              *
*    This system allows users to properly suspend the bone decay of units,     *
*    and resume it at will. More specific info can be found further down.      *
*                                                                              *
*    Author:            J2Krauser                                              *
*                                                                              *
*    v1.0.0:            06.09.2020                                             *
*    v1.1.0:            08.11.2020                                             *
*    v1.2.0:            04.12.2020                                             *
*    v1.2.1:            29.04.2021                                             *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] REQUIREMENTS                                                           *
*                                                                              *
*        1. As I scripted (and saved) it using the latest version of           *
*        Warcraft, and editor at the time (1.32.10 - 1.32k), I recommend       *
*        using the system on at least these. However, it should work on        *
*        older patches just as well as long as one doesn't go too far back,    *
*        therefore this is more of an advice than a real requirement.          *
*        2. Nothing. No unit indexer dependency, or anything of the sort.      *
*        I found this important to point out.                                  *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] HOW TO IMPLEMENT                                                       *
*                                                                              *
*        1. Copy the folder called "PBDS".                                     *
*        2. Paste it into your map.                                            *
*        3. Read the HOW TO USE section.                                       *
*        4. Read the VARIABLES section.                                        *
*        5. Read the FUNCTIONS section.                                        *
*        6. Read the NOTES section.                                            *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] HOW TO USE                                                             *
*                                                                              *
*        After having copied the system into your map, perform the quick       *
*        setup below. Only two things are necessary if you have modified       *
*        the gameplays constants in your map for flesh and/or bone decay:      *
*        1. Set FLESH_DECAY to "Decay Time (sec) - Flesh" in your map.         *
*        2. Set BONE_DECAY to "Decay Time (sec) - Bones" in your map.          *
*        3. Set BLIZZARD_STYLE_CARGO to "false" if you want fully accurate     *
*        decay timers for units dropped from cargo holders.                    *
*                                                                              *
*        Additionally, you could fiddle with PERIOD, but I recommend not       *
*        to touch it. At this point everything should just work, the system    *
*        should just do its thing.                                             *
*                                                                              *
*        There is only one public function you have to use called              *
*        PBDSUnitSuspendDecay. It's fully explained further down, but the      *
*        gist of it is:                                                        *
*        If you want to suspend a unit's bone decay:                           *
*            call PBDSUnitSuspendDecay(unit, true)                             *
*        If you want to resume it:                                             *
*            call PBDSUnitSuspendDecay(unit, false)                            *
*                                                                              *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] SETUP                                                                  *
*                                                                              */
    globals
        private constant real PERIOD                  = 0.5

        private constant real FLESH_DECAY             = 2.00
        private constant real BONE_DECAY              = 88.00

        private constant boolean BLIZZARD_STYLE_CARGO = true
    endglobals

/*******************************************************************************
*                                                                              *
*    [] VARIABLES                                                              *
*                                                                              *
*        REAL                                                                  *
*        1. PERIOD: How often the the periodic trigger runs which does the     *
*        heavy lifting of this system. Can be changed if someone really        *
*        wants to, but I recommend it stays at its default 0.5 set by me.      *
*                                                                              *
*        REAL                                                                  *
*        2. FLESH_DECAY: In the editor: Advanced -> Gameplay Constants...      *
*        Here, find "Decay Time (sec) - Flesh", and this variable should       *
*        hold the same value one finds there.                                  *
*                                                                              *
*        REAL                                                                  *
*        3. BONE_DECAY: In the editor: Advanced -> Gameplay Constants...       *
*        Here, find "Decay Time (sec) - Bones", and this variable should       *
*        hold the same value one finds there.                                  *
*                                                                              *
*        BOOLEAN                                                               *
*        4. BLIZZARD_STYLE_CARGO: Corpses picked up by a cargo holder, then    *
*        put back down restart their decay timer if this is set to "true".     *
*        This is the default WC3 behaviour.                                    *
*                                                                              *
*                                   <<[**]>>                                   *
*        Starting here, the rest are set up by my script, and do not have      *
*        to be touched manually.                                               *
*                                   <<[**]>>                                   *
*                                                                              *
*                                                                              *
*        HASHTABLE                                                             *
*        5. pbdsDecayingUnitsHash: The main data structure the system uses     *
*        to function.                                                          *
*        It's structured in the following way:                                 *
*            [KEY]: UNIT HANDLE                     INTEGER                    *
*            [0]:   BONE DECAY                      REAL                       *
*            [1]:   SUSPENDED                       BOOLEAN                    *
*            [2]:   IS CARGO                        BOOLEAN                    *
*                                                                              *
*        In other words, the key is a unit.                                    *
*        0 holds its current bone decay timer, counting down. Basically the    *
*        amount of seconds left till it's removed from the game, and           *
*        disappears. 1 is a boolean that's true if the bone decay is           *
*        suspended at the moment, which means the bone decay timer at 0 is     *
*        not progressing; it's false if the unit is decaying, and will         *
*        eventually get removed. 2 signals if the unit is being held as        *
*        cargo.                                                                *
*                                                                              *
*        UNIT GROUP                                                            *
*        6. pbdsDecayingUnitsUG: Contains every unit that's in the             *
*        hashtable. The bottom line is, these are units which are in the       *
*        process of bone decay. They are eligible to be suspended.             *
*                                                                              *
*        TRIGGER                                                               *
*        7. trgUnitDeath: The trigger that fires for every unit death. If      *
*        the unit is decayable, it'll be eligible for bone decay               *
*        suspension, therefore it gets stored in pbdsDecayingUnitsHash and     *
*        pbdsDecayingUnitsUG.                                                  *
*                                                                              *
*        TRIGGER                                                               *
*        8. trgPeriodic: Periodically executed as long as the unit group is    *
*        not empty. Counts down the bone decay timers, removes decayed         *
*        units.                                                                *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] FUNCTIONS                                                              *
*                                                                              *
*        function PBDSUnitSuspendDecay                                         *
*        takes unit u, boolean suspend                                         *
*        returns nothing                                                       *
*            u:       The unit to suspend or resume bone decay for.            *
*            suspend: If true, suspends the decay. If false, resumes it.       *
*            HOW IT WORKS:                                                     *
*            1. Check if the unit is in pbdsDecayingUnitsUG. If not, do        *
*            nothing.                                                          *
*            2. If "suspend" is true:                                          *
*                1. If the boolean at index 1 for this unit in                 *
*                pbdsDecayingUnitsHash is false, set it to true, then call     *
*                native UnitSuspendDecay.                                      *
*                2. Call native SetUnitTimeScale. This native is used to       *
*                to set the unit's animation speed to 0% (0.00) which          *
*                prevents its corpse from disappearing over time.              *
*            3. If "suspend" is false:                                         *
*                1. If the boolean at index 1 for this unit in                 *
*                pbdsDecayingUnitsHash is true, set it to false.               *
*                2. native UnitSuspendDecay isn't necessary to be called       *
*                again here with false to resume the decay, as the system      *
*                will just remove the unit anyway once it decayed.             *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] NOTES                                                                  *
*                                                                              *
*        1. I tried using SetUnitTimeScale to speed up the bone decay          *
*        animation instead of manually removing the corpse once the timer      *
*        is up. This doesn't work. I don't know the exact functionality of     *
*        the game in the background, but it seems to me as if it's fully       *
*        hardcoded once the decay is resumed how long it will take for the     *
*        corpse to disappear, and changing animation speed has no effect on    *
*        it whatsoever. Why is this important to mention? Because corpses      *
*        will immediately disappear instead of quickly fading away. This is    *
*        normal with this system.                                              *
*                                                                              *
*        2. Why is the system necessary? Wouldn't it work to just call         *
*        the natives UnitSuspendDecay, and SetUnitTimeScale appropriately,     *
*        then call it a day? No, it would not work. Let me explain how         *
*        UnitSuspendDecay works to show why this system is needed.             *
*        Basically, once the decay is suspended, the corpse will still         *
*        disappear over time. This is why SetUnitTimeScale to 0.00 is used     *
*        so that the animation is halted. Bone decay animations also have      *
*        a fixed length much like every other, therefore just suspending       *
*        the decay would keep the unit around (I know this is a bit hard       *
*        to grasp for people who aren't that into these things.), but its      *
*        corpse would be gone. You could still raise the unit, or resurrect    *
*        it, the corpse would just not be there. Halting the animation         *
*        using SetUnitTimeScale is what keeps the corpse around.               *
*                                                                              *
*        So? What's the issue with just calling UnitSuspendDecay again with    *
*        false, then SetUnitTimeScale with 1.00? Well, this is where the       *
*        way it works comes into the picture. You see, if you do this, it      *
*        does NOT resume anything. You might think that would be the           *
*        expected behaviour, but it actually starts the unit's bone decay      *
*        all over again. You notice this once you have a few units which       *
*        didn't all die exactly at the same moment, then you suspend their     *
*        bone decay just to resume it again. Their corpses will all fade       *
*        away at the exact same moment, exactly after the amount of seconds    *
*        you have "Decay Time (sec) - Bones" set to in gameplay constants.     *
*                                                                              *
*        That's why this system is needed if you want to actually resume       *
*        bone decay instead of restarting it at its full duration.             *
*                                                                              *
********************************************************************************/

    globals
        private hashtable pbdsDecayingUnitsHash = InitHashtable()
        private group pbdsDecayingUnitsUG       = CreateGroup()
        private trigger trgUnitDeath            = CreateTrigger()
        private trigger trgPeriodic             = CreateTrigger()
    endglobals

    private function TrgUnitDeathActions takes nothing returns nothing  // On any unit's death.
        local unit thisUnit = GetTriggerUnit()
        local integer unitHandleId

        if (BlzGetUnitBooleanField(thisUnit, UNIT_BF_DECAYABLE)) then                                        // If the unit's death type has "Does decay" in there,
            set unitHandleId = GetHandleId(thisUnit)
            call TriggerSleepAction(BlzGetUnitRealField(thisUnit, UNIT_RF_DEATH_TIME) + FLESH_DECAY + 0.03)  // wait out its "Art - Death Time (seconds)" + FLESH_DECAY + a tiny amount to make sure it's in bone decay,
            call SaveReal(pbdsDecayingUnitsHash, unitHandleId, 0, BONE_DECAY)                                // save it in the hashtable
            call SaveBoolean(pbdsDecayingUnitsHash, unitHandleId, 1, false)                                  // with the boolean at index 1 set to false by default as its decay is not suspended automatically,
            call GroupAddUnit(pbdsDecayingUnitsUG, thisUnit)                                                 // then add it to pbdsDecayingUnitsUG.
            call EnableTrigger(trgPeriodic)                                                                  // Finally, start the periodic trigger in case the unit group was empty beforehand.
        endif                                                                                                // Note: Death Time is the amount of seconds from the unit's death till it starts decaying.
                                                                                                             // Note: Decaying starts with flesh decay, after which bone decay sets in.
        set thisUnit = null
    endfunction

    private function TrgPeriodicActions takes nothing returns nothing  // Runs every PERIOD seconds if pbdsDecayingUnitsUG is not empty.
        local integer i = 0
        local group tempUG = CreateGroup()  // A temporary group used to get rid of phantom units in pbdsDecayingUnitsUG.
        local unit enumUnit
        local integer enumUnitHandleId
        local real remaining = 0.00

        call BlzGroupAddGroupFast(pbdsDecayingUnitsUG, tempUG)  // Copy the contents of the original group into the temporary one,
        call GroupClear(pbdsDecayingUnitsUG)                    // then clear out the original. The temporary will be worked with.
        set enumUnit = BlzGroupUnitAt(tempUG, i)                // enumUnit is the variable used to cycle through the group's units.

        loop
            exitwhen i >= BlzGroupGetSize(tempUG)

            set enumUnitHandleId = GetHandleId(enumUnit)
            if ((not IsUnitType(enumUnit, UNIT_TYPE_DEAD)) and (GetUnitTypeId(enumUnit) != 0)) then  // If the unit is alive, for example because it got revived in some way,
                call SetUnitTimeScale(enumUnit, 1.00)                                                // simply remove it from the hashtable, and proceed to the next iteration of the loop.
                call FlushChildHashtable(pbdsDecayingUnitsHash, enumUnitHandleId)
            else
                if (IsUnitLoaded(enumUnit)) then                                            // If the corpse is currently held as cargo,
                    if (not LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2)) then   // but during the previous iteration, it wasn't,
                        call SaveBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2, true)  // mark it as cargo in the hashtable.
                    endif
                else                                                                               // If it's not held as cargo,
                    if (LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2)) then              // but during the previous iteration, it was (which means it was just dropped out of the cargo holder),
                        call SaveBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2, false)        // unmark it as cargo in the hashtable.
                        if (BLIZZARD_STYLE_CARGO) then                                             // If we're utilizing default WC3 cargo behaviour,
                            call SaveReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0, BONE_DECAY)  // reset its decay timer.
                        endif
                        if (LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 1)) then          // If its decay is currently suspended,
                            call UnitSuspendDecay(enumUnit, true)                                  // suspend it, and stop its animation again, since WC3 automatically restarts it when this occurs.
                            call SetUnitTimeScale(enumUnit, 0.00)
                        endif
                    endif

                    if (not LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 1)) then              // If the boolean at index 1 for this unit is false, its decay is not suspended,
                        set remaining = LoadReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0) - PERIOD  // therefore keep the countdown going.
                        if (remaining > 0.00) then                                                     // If the the timer hasn't expired yet,
                            call SaveReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0, remaining)       // keep going.
                        else
                            call FlushChildHashtable(pbdsDecayingUnitsHash, enumUnitHandleId)          // Otherwise, remove the unit from the hashtable, then remove the unit itself.
                            call RemoveUnit(enumUnit)                                                  // This is where the corpse disappears.
                        endif
                    endif
                endif

                if (GetUnitTypeId(enumUnit) != 0) then                // If the unit is not removed yet, its unit-type is not zero. If it's removed, it's a phantom unit.
                    call GroupAddUnit(pbdsDecayingUnitsUG, enumUnit)  // Only existing units (technically, corpses) are copied back into the original group.
                endif
            endif

            set i = i + 1
            set enumUnit = BlzGroupUnitAt(tempUG, i)
        endloop
        call DestroyGroup(tempUG)
        set tempUG = null

        if (BlzGroupGetSize(pbdsDecayingUnitsUG) < 1) then  // Disable this trigger if there are no decaying corpses currently.
            call DisableTrigger(GetTriggeringTrigger())
            return
        endif
    endfunction

    private function PBDSInit takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ(trgUnitDeath, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddAction(trgUnitDeath, function TrgUnitDeathActions)

        call DisableTrigger(trgPeriodic)
        call TriggerRegisterTimerEvent(trgPeriodic, PERIOD, true)
        call TriggerAddAction(trgPeriodic, function TrgPeriodicActions)
    endfunction

/*******************************************************************************
*                                                                              *
*    [] PUBLIC                                                                 *
*                                                                              *
********************************************************************************/

    function PBDSUnitSuspendDecay takes unit u, boolean suspend returns nothing  // Suspends bone decay of "u" if "suspend" is true, resumes it if false.
        local integer unitHandleId

        if (IsUnitInGroup(u, pbdsDecayingUnitsUG)) then
            set unitHandleId = GetHandleId(u)
            if (suspend) then
                if (not LoadBoolean(pbdsDecayingUnitsHash, unitHandleId, 1)) then
                    call SaveBoolean(pbdsDecayingUnitsHash, unitHandleId, 1, true)
                    call UnitSuspendDecay(u, true)
                    call SetUnitTimeScale(u, 0.00)
                endif
            else
                if (LoadBoolean(pbdsDecayingUnitsHash, unitHandleId, 1)) then
                    call SaveBoolean(pbdsDecayingUnitsHash, unitHandleId, 1, false)
                endif
            endif
        endif
    endfunction
endlibrary

Note: I recommend taking the folder from the test map instead here since the initializer has to be called manually, and people might forget about that. Bone decay time in the test map is set to 10 seconds instead of the default 88.
Lua:
--[[****************************************************************************
*                                                                              *
*    Proper Bone Decay Suspension System                                       *
*                                                                              *
*    This system allows users to properly suspend the bone decay of units,     *
*    and resume it at will. More specific info can be found further down.      *
*                                                                              *
*    Author:            J2Krauser                                              *
*                                                                              *
*    v1.0.0L:           08.09.2020                                             *
*    v1.1.0L:           08.11.2020                                             *
*    v1.1.1L:           26.11.2020                                             *
*    v1.2.0L:           04.12.2020                                             *
*    v1.2.1L:           29.04.2021                                             *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] REQUIREMENTS                                                           *
*                                                                              *
*        1. As I scripted (and saved) it using the latest version of           *
*        Warcraft, and editor at the time (1.32.10 - 1.32k), I recommend       *
*        using the system on at least these. However, it should work on        *
*        older patches just as well as long as one doesn't go too far back,    *
*        therefore this is more of an advice than a real requirement.          *
*        2. Nothing. No unit indexer dependency, or anything of the sort.      *
*        I found this important to point out.                                  *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] HOW TO IMPLEMENT                                                       *
*                                                                              *
*        1. Copy the folder called "PBDS".                                     *
*        2. Paste it into your map.                                            *
*        3. Read the HOW TO USE section.                                       *
*        4. Read the VARIABLES section.                                        *
*        5. Read the FUNCTIONS section.                                        *
*        6. Read the NOTES section.                                            *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] HOW TO USE                                                             *
*                                                                              *
*        After having copied the system into your map, perform the quick       *
*        setup below. Only two things are necessary if you have modified       *
*        the gameplays constants in your map for flesh and/or bone decay:      *
*        1. Set Setup.FleshDecay to "Decay Time (sec) - Flesh" in your map.    *
*        2. Set Setup.BoneDecay to "Decay Time (sec) - Bones" in your map.     *
*        3. Set Setup.BlizzardStyleCargo to "false" if you want fully          *
*        accurate decay timers for units dropped from cargo holders.           *
*                                                                              *
*        Additionally, you could fiddle with Setup.Period, but I recommend     *
*        not to touch it. At this point everything should just work, the       *
*        system should just do its thing.                                      *
*                                                                              *
*        There is only one public function you have to use called              *
*        PBDSUnitSuspendDecay. It's fully explained further down, but the      *
*        gist of it is:                                                        *
*        If you want to suspend a unit's bone decay:                           *
*            PBDSUnitSuspendDecay(unit, true)                                  *
*        If you want to resume it:                                             *
*            PBDSUnitSuspendDecay(unit, false)                                 *
*                                                                              *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] SETUP                                                                  *
*                                                                              *--]]

do
   local Setup = {}

   Setup.Period = 0.5

   Setup.FleshDecay = 2.0
   Setup.BoneDecay = 88.0

   Setup.BlizzardStyleCargo = true

--[[****************************************************************************
*                                                                              *
*    [] VARIABLES                                                              *
*                                                                              *
*        REAL                                                                  *
*        1. Setup.Period: How often the the periodic trigger runs which        *
*        does the heavy lifting of this system. Can be changed if someone      *
*        really wants to, but I recommend it stays at its default 0.5 set      *
*        by me.                                                                *
*                                                                              *
*        REAL                                                                  *
*        2. Setup.FleshDecay: In the editor: Advanced -> Gameplay              *
*        Constants...                                                          *
*        Here, find "Decay Time (sec) - Flesh", and this variable should       *
*        hold the same value one finds there.                                  *
*                                                                              *
*        REAL                                                                  *
*        3. Setup.BoneDecay: In the editor: Advanced -> Gameplay               *
*        Constants...                                                          *
*        Here, find "Decay Time (sec) - Bones", and this variable should       *
*        hold the same value one finds there.                                  *
*                                                                              *
*        BOOLEAN                                                               *
*        4. Setup.BlizzardStyleCargo: If "true", units dropped by cargo        *
*        holders will reset their decay timer. This is the default WC3         *
*        behaviour.                                                            *
*                                                                              *
*                                   <<[**]>>                                   *
*        Starting here, the rest are set up by my script, and do not have      *
*        to be touched manually.                                               *
*                                   <<[**]>>                                   *
*                                                                              *
*                                                                              *
*        TABLE                                                                 *
*        5. PBDS.DecayingUnitsTable: The main data structure the system        *
*        uses to function.                                                     *
*        It's structured in the following way:                                 *
*            [PARENT KEY]:   UNIT HANDLE             INTEGER                   *
*            ["bone decay"]: BONE DECAY SECONDS LEFT REAL                      *
*            ["suspended"]:  DECAY IS SUSPENDED      BOOLEAN                   *
*            ["cargo"]:      UNIT IS CARGO           BOOLEAN                   *
*                                                                              *
*        In other words, the key is a unit.                                    *
*        0 holds its current bone decay timer, counting down. Basically the    *
*        amount of seconds left till it's removed from the game, and           *
*        disappears. 1 is a boolean that's true if the bone decay is           *
*        suspended at the moment, which means the bone decay timer at 0 is     *
*        not progressing; it's false if the unit is decaying, and will         *
*        eventually get removed. 2 signals if the unit is being held as        *
*        cargo.                                                                *
*                                                                              *
*        UNIT GROUP                                                            *
*        6. PBDS.DecayingUnitsUG: Contains every unit that's in the table.     *
*        The bottom line is, these are units which are in the process of       *
*        bone decay. They are eligible to be suspended.                        *
*                                                                              *
*        TRIGGER                                                               *
*        7. PBDS.TrgUnitDeath: The trigger that fires for every unit death.    *
*        If the unit is decayable, it'll be eligible for bone decay            *
*        suspension, therefore it gets stored in PBDS.DecayingUnitsTable       *
*        and PBDS.DecayingUnitsUG.                                             *
*                                                                              *
*        TRIGGER                                                               *
*        8. PBDS.TrgPeriodic: Periodically executed as long as the unit        *
*        group is not empty. Counts down the bone decay timers, removes        *
*        decayed units.                                                        *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] FUNCTIONS                                                              *
*                                                                              *
*        function PBDSUnitSuspendDecay(u, suspend)                             *
*            UNIT                                                              *
*            u:       The unit to suspend or resume bone decay for.            *
*            BOOLEAN                                                           *
*            suspend: If true, suspends the decay. If false, resumes it.       *
*            HOW IT WORKS:                                                     *
*            1. Check if the unit is in PBDS.DecayingUnitsUG. If not, do       *
*            nothing.                                                          *
*            2. If "suspend" is true:                                          *
*                1. If "bone decay" for this unit in                           *
*                PBDS.DecayingUnitsTable is false, set it to true, then        *
*                call native UnitSuspendDecay.                                 *
*                2. Call native SetUnitTimeScale. This native is used to       *
*                to set the unit's animation speed to 0% (0.00) which          *
*                prevents its corpse from disappearing over time.              *
*            3. If "suspend" is false:                                         *
*                1. If "bone decay" for this unit in                           *
*                PBDS.DecayingUnitsTable is true, set it to false.             *
*                2. native UnitSuspendDecay isn't necessary to be called       *
*                again here with false to resume the decay, as the system      *
*                will just remove the unit anyway once it decayed.             *
*                                                                              *
********************************************************************************
*                                                                              *
*    [] NOTES                                                                  *
*                                                                              *
*        1. I tried using SetUnitTimeScale to speed up the bone decay          *
*        animation instead of manually removing the corpse once the timer      *
*        is up. This doesn't work. I don't know the exact functionality of     *
*        the game in the background, but it seems to me as if it's fully       *
*        hardcoded once the decay is resumed how long it will take for the     *
*        corpse to disappear, and changing animation speed has no effect on    *
*        it whatsoever. Why is this important to mention? Because corpses      *
*        will immediately disappear instead of quickly fading away. This is    *
*        normal with this system.                                              *
*                                                                              *
*        2. Why is the system necessary? Wouldn't it work to just call         *
*        the natives UnitSuspendDecay, and SetUnitTimeScale appropriately,     *
*        then call it a day? No, it would not work. Let me explain how         *
*        UnitSuspendDecay works to show why this system is needed.             *
*        Basically, once the decay is suspended, the corpse will still         *
*        disappear over time. This is why SetUnitTimeScale to 0.00 is used     *
*        so that the animation is halted. Bone decay animations also have      *
*        a fixed length much like every other, therefore just suspending       *
*        the decay would keep the unit around (I know this is a bit hard       *
*        to grasp for people who aren't that into these things.), but its      *
*        corpse would be gone. You could still raise the unit, or resurrect    *
*        it, the corpse would just not be there. Halting the animation         *
*        using SetUnitTimeScale is what keeps the corpse around.               *
*                                                                              *
*        So? What's the issue with just calling UnitSuspendDecay again with    *
*        false, then SetUnitTimeScale with 1.00? Well, this is where the       *
*        way it works comes into the picture. You see, if you do this, it      *
*        does NOT resume anything. You might think that would be the           *
*        expected behaviour, but it actually starts the unit's bone decay      *
*        all over again. You notice this once you have a few units which       *
*        didn't all die exactly at the same moment, then you suspend their     *
*        bone decay just to resume it again. Their corpses will all fade       *
*        away at the exact same moment, exactly after the amount of seconds    *
*        you have "Decay Time (sec) - Bones" set to in gameplay constants.     *
*                                                                              *
*        That's why this system is needed if you want to actually resume       *
*        bone decay instead of restarting it at its full duration.             *
*                                                                              *
********************************************************************************--]]

   local PBDS = {}

   PBDS.DecayingUnitsTable = {}
   PBDS.DecayingUnitsUG = CreateGroup()
   PBDS.TrgUnitDeath = CreateTrigger()
   PBDS.TrgPeriodic = CreateTrigger()

   local function TrgUnitDeathActions()  -- On any unit's death.
      local thisUnit = GetTriggerUnit()
      local unitHandleId = GetHandleId(thisUnit)

      if (BlzGetUnitBooleanField(thisUnit, ConvertUnitBooleanField(FourCC("udec")))) then                                   -- If the unit's death type has "Does decay" in there,
         TriggerSleepAction(BlzGetUnitRealField(thisUnit, ConvertUnitRealField(FourCC("udtm"))) + Setup.FleshDecay + 0.03)  -- wait out its "Art - Death Time (seconds)" + Setup.FleshDecay + a tiny amount to make sure it's in bone decay,
         if (not PBDS.DecayingUnitsTable[unitHandleId]) then
            PBDS.DecayingUnitsTable[unitHandleId] = {}                                                                      -- save it in the table
         end
         if (not PBDS.DecayingUnitsTable[unitHandleId]["bone decay"]) then
            PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = {}
         end
         PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = Setup.BoneDecay
         if (not PBDS.DecayingUnitsTable[unitHandleId]["suspended"]) then
            PBDS.DecayingUnitsTable[unitHandleId]["suspended"] = {}
         end
         PBDS.DecayingUnitsTable[unitHandleId]["suspended"] = false                                                         -- with "suspended" set to false by default as its decay is not suspended automatically,
         if (not PBDS.DecayingUnitsTable[unitHandleId]["cargo"]) then
            PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = {}
         end
         PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = false                                                             -- and "cargo" set to false because a dying unit cannot be held as cargo,
         GroupAddUnit(PBDS.DecayingUnitsUG, thisUnit)                                                                       -- then add it to PBDS.DecayingUnitsUG.
         EnableTrigger(PBDS.TrgPeriodic)                                                                                    -- Finally, start the periodic trigger in case the unit group was empty beforehand.
      end                                                                                                                   -- Note: Death Time is the amount of seconds from the unit's death till it starts decaying.
   end                                                                                                                      -- Note: Decaying starts with flesh decay, after which bone decay sets in.

   local function TrgPeriodicActions()  -- Runs every Setup.Period seconds if PBDS.DecayingUnitsUG is not empty.
      local tempUG = CreateGroup()  -- A temporary group used to get rid of phantom units in PBDS.DecayingUnitsUG.

      BlzGroupAddGroupFast(PBDS.DecayingUnitsUG, tempUG)  -- Copy the contents of the original group into the temporary one,
      GroupClear(PBDS.DecayingUnitsUG)                    -- then clear out the original. The temporary will be worked with.

      for i = 0, BlzGroupGetSize(tempUG) - 1, 1 do
         local enumUnit = BlzGroupUnitAt(tempUG, i)
         local unitHandleId = GetHandleId(enumUnit)

         if ((not IsUnitType(enumUnit, UNIT_TYPE_DEAD)) and (GetUnitTypeId(enumUnit) ~= 0)) then  -- If the unit is alive, for example because it got revived in some way,
            SetUnitTimeScale(enumUnit, 1.00)                                                      -- simply remove it from the table, and proceed to the next iteration of the loop.
            PBDS.DecayingUnitsTable[unitHandleId] = nil
         else
            if (IsUnitLoaded(enumUnit)) then                                 -- If the corpse is currently held as cargo,
               if (not PBDS.DecayingUnitsTable[unitHandleId]["cargo"]) then  -- but during the previous iteration, it wasn't,
                  PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = true      -- mark it as cargo in the table.
               end
            else                                                                            -- If it's not held as cargo,
               if (PBDS.DecayingUnitsTable[unitHandleId]["cargo"]) then                     -- but during the previous iteration, it was (which means the cargo carrier just dropped it),
                  PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = false                    -- unmark it as cargo in the table.
                  if (Setup.BlizzardStyleCargo) then                                        -- If we're utilizing default WC3 cargo behaviour,
                     PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = Setup.BoneDecay  -- reset its decay timer.
                  end
                  if (PBDS.DecayingUnitsTable[unitHandleId]["suspended"]) then  -- If its decay is currently suspended,
                     UnitSuspendDecay(enumUnit, true)                           -- suspend it, and stop its animation again, since WC3 automatically restarts it when this situation occurs.
                     SetUnitTimeScale(enumUnit, 0.00)
                  end
               end

               if (not PBDS.DecayingUnitsTable[unitHandleId]["suspended"]) then                         -- If "suspended" for this unit is false, its decay is not suspended,
                  local remaining = PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] - Setup.Period  -- therefore keep the countdown going.
                  if (remaining > 0.00) then                                                            -- If the the timer hasn't expired yet,
                     PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = remaining                    -- keep going.
                  else
                     PBDS.DecayingUnitsTable[unitHandleId] = nil                                        -- Otherwise, remove the unit from the table, then remove the unit itself.
                     RemoveUnit(enumUnit)                                                               -- This is where the corpse disappears.
                  end
               end
            end

            if (GetUnitTypeId(enumUnit) ~= 0) then           -- If the unit is not removed yet, its unit-type is not zero. If it's removed, it's a phantom unit.
               GroupAddUnit(PBDS.DecayingUnitsUG, enumUnit)  -- Only existing units (technically, corpses) are copied back into the original group.
            end
         end
      end
      DestroyGroup(tempUG)
      tempUG = nil

      if (BlzGroupGetSize(PBDS.DecayingUnitsUG) < 1) then  -- Disable this trigger if there are no decaying corpses currently.
         DisableTrigger(GetTriggeringTrigger())
         return
      end
   end

--[[****************************************************************************
*                                                                              *
*    [] PUBLIC                                                                 *
*                                                                              *
********************************************************************************--]]

   function PBDSInit()
      for i = 0, bj_MAX_PLAYER_SLOTS - 1, 1 do
         TriggerRegisterPlayerUnitEvent(PBDS.TrgUnitDeath, Player(i), ConvertPlayerUnitEvent(20), nil)
      end
      TriggerAddAction(PBDS.TrgUnitDeath, TrgUnitDeathActions)

      DisableTrigger(PBDS.TrgPeriodic)
      TriggerRegisterTimerEvent(PBDS.TrgPeriodic, Setup.Period, true)
      TriggerAddAction(PBDS.TrgPeriodic, TrgPeriodicActions)
   end

   function PBDSUnitSuspendDecay(u, suspend)  -- Suspends bone decay of "u" if "suspend" is true, resumes it if false.
      if (IsUnitInGroup(u, PBDS.DecayingUnitsUG)) then
         local unitHandleId = GetHandleId(u)
         if (suspend) then
            if (not(PBDS.DecayingUnitsTable[unitHandleId]["suspended"])) then
               PBDS.DecayingUnitsTable[unitHandleId]["suspended"] = true
               UnitSuspendDecay(u, true)
               SetUnitTimeScale(u, 0.00)
            end
         else
            if (PBDS.DecayingUnitsTable[unitHandleId]["suspended"]) then
               PBDS.DecayingUnitsTable[unitHandleId]["suspended"] = false
            end
         end
      end
   end
end

JASS:
v1.2.1

  • Fixed a small bug with cargo units.
  • Added a new option to retain default WC3 cargo functionality. This is now the default behaviour.
v1.2.0
  • Fixed a bug where corpses kept as cargo kept decaying, and eventually disappearing inside the cargo holder.
  • Fixed a reference leak.
  • Slight performance improvements.
v1.1.0
  • Fixed a bug where revived units would get prematurely removed.
v1.0.0
  • Initial release.
Lua:
v1.2.1L

  • Fixed a small bug with cargo units.
  • Added a new option to retain default WC3 cargo functionality. This is now the default behaviour.
v1.2.0L
  • Fixed a bug where corpses kept as cargo kept decaying, and eventually disappearing inside the cargo holder.
v1.1.1L
  • Implementation moved from hashtables to Lua tables.
v1.1.0L
  • Fixed a bug where revived units would get prematurely removed.
v1.0.0L
  • Initial release.
Contents

Proper Bone Decay Suspension System (Map)

Proper Bone Decay Suspension System (Map)

Reviews
Wrda
VJASS version: 1)The globals should be start with uppercase, because they're not local. Personally I would also add underscore after the "trg" and "pbds" just to be more distinct. In other words, it would be Pbds_DecayingUnitsHash or...
Level 5
Joined
Jul 31, 2020
Messages
103
Hi.

I added an additional Lua version of this system just now. However, I have to point out I quickly checked on Lua syntax yesterday night in about an hour, then converted the script into Lua, therefore I'm not exactly clear with proper practices. I tested it thoroughly, it seems to function properly, but if any Lua enthusiast can take a look at it, and tell me if there's something I shouldn't do the way I did, that would be helpful. Thank you.
 
Level 4
Joined
Apr 27, 2021
Messages
52
Interesting concept. I wanted something similar for my RPG map.
But I dont know how to use this bundle as its in lua and Jass
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,864
VJASS version: 1)The globals should be start with uppercase, because they're not local. Personally I would also add underscore after the "trg" and "pbds" just to be more distinct. In other words, it would be Pbds_DecayingUnitsHash or PBDS_DecayingUnitsHash, specially because it is the name of the system. Currently it causes conflict between global and local variables when reading.
2) It would really be better if you used timer instead of the wait in TrgUnitDeathActions, since it is more precise. Shouldn't be very difficult for the reason that you're already using a hashtable, so the association of timer with unit is trivial.

Lua version: 1) I believe the following variables should be treated like constants:
Lua:
   Setup.Period = 0.5

   Setup.FleshDecay = 2.0
   Setup.BoneDecay = 88.0

   Setup.BlizzardStyleCargo = true
->
Lua:
   Setup.PERIOD = 0.5

   Setup.FLESH_DECAY = 2.0
   Setup.BONE_DECAY = 88.0

   Setup.BLIZZARD_STYLE_CARGO = true
Lua submission rules didn't probably exist on the creation of this resource or weren't agreed upon, but some of the VJASS rules are being applied to Lua resources. here you can know more.
2) TrgUnitDeathActions can be reduced to:
Lua:
local function TrgUnitDeathActions()  -- On any unit's death.
    local thisUnit = GetTriggerUnit()
    local unitHandleId = GetHandleId(thisUnit)

    if (BlzGetUnitBooleanField(thisUnit, ConvertUnitBooleanField(FourCC("udec")))) then                                   -- If the unit's death type has "Does decay" in there,
       TriggerSleepAction(BlzGetUnitRealField(thisUnit, ConvertUnitRealField(FourCC("udtm"))) + Setup.FleshDecay + 0.03)  -- wait out its "Art - Death Time (seconds)" + Setup.FleshDecay + a tiny amount to make sure it's in bone decay,
       if (not PBDS.DecayingUnitsTable[unitHandleId]) then
          PBDS.DecayingUnitsTable[unitHandleId] = {}
          PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = Setup.BoneDecay
          PBDS.DecayingUnitsTable[unitHandleId]["suspended"] = false
          PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = false                                                          -- save it in the table
       end                                                                                                                -- with "suspended" set to false by default as its decay is not suspended automatically,
       PBDS.DecayingUnitsTable[unitHandleId]["cargo"] = false                                                             -- and "cargo" set to false because a dying unit cannot be held as cargo,
       GroupAddUnit(PBDS.DecayingUnitsUG, thisUnit)                                                                       -- then add it to PBDS.DecayingUnitsUG.
       EnableTrigger(PBDS.TrgPeriodic)                                                                                    -- Finally, start the periodic trigger in case the unit group was empty beforehand.
    end                                                                                                                   -- Note: Death Time is the amount of seconds from the unit's death till it starts decaying.
end
Or even in this way:
Lua:
PBDS.DecayingUnitsTable[unitHandleId] = {["bone decay"] = Setup.BoneDecay,
    ["suspended"] = false,
    ["cargo"] = false
}
All that PBDS.DecayingUnitsTable[unitHandleId]["bone decay"] = {} does and the rest results in a 3D array, which isn't needed, and you're not in fact using later.
3) Using timers in Lua are easier due to anonymous functions.
Note: Even though in this case it doesn't matter, you're not really required to use unit's handle id in Lua for arrays, you can use the unit itself.
There's not really a need to nil tempUG, Lua doesn't suffer from reference counter leaks.

Both versions: 1) TrgPeriodicActions can be improved by caching BlzGroupGetSize(tempUG) before using it in the loop.
2) "return" isn't doing anything in TrgPeriodicActions.
3) Currently doesn't work if the unit never got into the cargo. Requires an "else" part, shown here:
JASS:
private function TrgPeriodicActions takes nothing returns nothing  // Runs every PERIOD seconds if pbdsDecayingUnitsUG is not empty.
        local integer i = 0
        local group tempUG = CreateGroup()  // A temporary group used to get rid of phantom units in pbdsDecayingUnitsUG.
        local unit enumUnit
        local integer enumUnitHandleId
        local real remaining = 0.00

        call BlzGroupAddGroupFast(pbdsDecayingUnitsUG, tempUG)  // Copy the contents of the original group into the temporary one,
        call GroupClear(pbdsDecayingUnitsUG)                    // then clear out the original. The temporary will be worked with.
        set enumUnit = BlzGroupUnitAt(tempUG, i)                // enumUnit is the variable used to cycle through the group's units.

        loop
            exitwhen i >= BlzGroupGetSize(tempUG)

            set enumUnitHandleId = GetHandleId(enumUnit)
            if ((not IsUnitType(enumUnit, UNIT_TYPE_DEAD)) and (GetUnitTypeId(enumUnit) != 0)) then  // If the unit is alive, for example because it got revived in some way,
                call SetUnitTimeScale(enumUnit, 1.00)                                                // simply remove it from the hashtable, and proceed to the next iteration of the loop.
                call FlushChildHashtable(pbdsDecayingUnitsHash, enumUnitHandleId)
            else
                if (IsUnitLoaded(enumUnit)) then                                            // If the corpse is currently held as cargo,
                    if (not LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2)) then   // but during the previous iteration, it wasn't,
                        call SaveBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2, true)  // mark it as cargo in the hashtable.
                    endif
                else                                                                               // If it's not held as cargo,
                    if (LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2)) then              // but during the previous iteration, it was (which means it was just dropped out of the cargo holder),
                        call SaveBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 2, false)        // unmark it as cargo in the hashtable.
                        if (BLIZZARD_STYLE_CARGO) then                                             // If we're utilizing default WC3 cargo behaviour,
                            call SaveReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0, BONE_DECAY)  // reset its decay timer.
                        endif
                        if (LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 1)) then          // If its decay is currently suspended,
                            call UnitSuspendDecay(enumUnit, true)                                  // suspend it, and stop its animation again, since WC3 automatically restarts it when this occurs.
                            call SetUnitTimeScale(enumUnit, 0.00)
                        endif
                    endif

                    if (not LoadBoolean(pbdsDecayingUnitsHash, enumUnitHandleId, 1)) then              // If the boolean at index 1 for this unit is false, its decay is not suspended,
                        set remaining = LoadReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0) - PERIOD  // therefore keep the countdown going.
                        if (remaining > 0.00) then                                                     // If the the timer hasn't expired yet,
                            call SaveReal(pbdsDecayingUnitsHash, enumUnitHandleId, 0, remaining)       // keep going.
                        else
                            call FlushChildHashtable(pbdsDecayingUnitsHash, enumUnitHandleId)          // Otherwise, remove the unit from the hashtable, then remove the unit itself.
                            call RemoveUnit(enumUnit)                                                  // This is where the corpse disappears.
                        endif
                    else //added
                        call UnitSuspendDecay(enumUnit, true)  //added                                 // suspend it, and stop its animation again, since WC3 automatically restarts it when this occurs.
                        call SetUnitTimeScale(enumUnit, 0.00)  //added
                    endif
                endif

                if (GetUnitTypeId(enumUnit) != 0) then                // If the unit is not removed yet, its unit-type is not zero. If it's removed, it's a phantom unit.
                    call GroupAddUnit(pbdsDecayingUnitsUG, enumUnit)  // Only existing units (technically, corpses) are copied back into the original group.
                endif
            endif

            set i = i + 1
            set enumUnit = BlzGroupUnitAt(tempUG, i)
        endloop
        call DestroyGroup(tempUG)
        set tempUG = null

        if (BlzGroupGetSize(pbdsDecayingUnitsUG) < 1) then  // Disable this trigger if there are no decaying corpses currently.
            call DisableTrigger(GetTriggeringTrigger())
            return
        endif
    endfunction
4) In TrgUnitDeathActions, there must be a check after the wait (the unit could be removed, either manually or any other case like raise dead, or be alive again due to ressurection), specially in Lua with the check if unit ~= nil

All in all, definetely a useful system that correctly suspends and resumes corpses, however, it requires some fixes.
 
Top