Name | Type | is_array | initial_value |
peasant | unit | No |
--[[****************************************************************************
* *
* 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 = 10.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
do
local TestCommands = {}
TestCommands.TrgSpawn = CreateTrigger()
TestCommands.TrgSuspend = CreateTrigger()
TestCommands.TrgResume = CreateTrigger()
TestCommands.TrgSpeasant = CreateTrigger()
TestCommands.TrgRpeasant = CreateTrigger()
TestCommands.DeadUnitFilter = nil
local function TrgSpawnActions()
local r = Rect(-685.0, 461.0, 260.0, -317.0)
local u = CreateUnitAtLoc(Player(0), FourCC("hfoo"), Location(GetRandomReal(GetRectMinX(r), GetRectMaxX(r)), GetRandomReal(GetRectMinY(r), GetRectMaxY(r))), 270.00)
KillUnit(u)
end
local function DeadUnitFilterFunction()
if (GetUnitState(GetFilterUnit(), ConvertUnitState(0)) < 0.406) then
return true
end
return false
end
local function TrgSuspendActions()
local ug = CreateGroup()
local entireMap = GetWorldBounds()
GroupEnumUnitsInRect(ug, entireMap, TestCommands.DeadUnitFilter)
local firstOfGroup = FirstOfGroup(ug)
while (firstOfGroup ~= nil) do
PBDSUnitSuspendDecay(firstOfGroup, true)
GroupRemoveUnit(ug, firstOfGroup)
firstOfGroup = FirstOfGroup(ug)
end
DestroyGroup(ug)
ug = nil
DisplayTextToPlayer(GetLocalPlayer(), 0.00, 0.00, "Suspended")
end
local function TrgResumeActions()
local ug = CreateGroup()
local entireMap = GetWorldBounds()
GroupEnumUnitsInRect(ug, entireMap, TestCommands.DeadUnitFilter)
local firstOfGroup = FirstOfGroup(ug)
while (firstOfGroup ~= nil) do
PBDSUnitSuspendDecay(firstOfGroup, false)
GroupRemoveUnit(ug, firstOfGroup)
firstOfGroup = FirstOfGroup(ug)
end
DestroyGroup(ug)
ug = nil
DisplayTextToPlayer(GetLocalPlayer(), 0.00, 0.00, "Resumed")
end
local function TrgSpeasantActions()
PBDSUnitSuspendDecay(udg_peasant, true)
DisplayTextToPlayer(GetLocalPlayer(), 0.00, 0.00, "Peasant Decay Suspended")
end
local function TrgRpeasantActions()
PBDSUnitSuspendDecay(udg_peasant, false)
DisplayTextToPlayer(GetLocalPlayer(), 0.00, 0.00, "Peasant Decay Resumed")
end
function TestCommandsInit()
TestCommands.DeadUnitFilter = Filter(DeadUnitFilterFunction)
TriggerRegisterPlayerEvent(TestCommands.TrgSpawn, Player(0), ConvertPlayerEvent(17))
TriggerAddAction(TestCommands.TrgSpawn, TrgSpawnActions)
TriggerRegisterPlayerChatEvent(TestCommands.TrgSuspend, Player(0), ".suspend", true)
TriggerAddAction(TestCommands.TrgSuspend, TrgSuspendActions)
TriggerRegisterPlayerChatEvent(TestCommands.TrgResume, Player(0), ".resume", true)
TriggerAddAction(TestCommands.TrgResume, TrgResumeActions)
TriggerRegisterPlayerChatEvent(TestCommands.TrgSpeasant, Player(0), ".speasant", true)
TriggerAddAction(TestCommands.TrgSpeasant, TrgSpeasantActions)
TriggerRegisterPlayerChatEvent(TestCommands.TrgRpeasant, Player(0), ".rpeasant", true)
TriggerAddAction(TestCommands.TrgRpeasant, TrgRpeasantActions)
end
end