From 337ab7f2c37a0eb086b7f76c25d2831801fef861 Mon Sep 17 00:00:00 2001
From: RikuTheKiller <88713943+RikuTheKiller@users.noreply.github.com>
Date: Fri, 7 Nov 2025 16:25:16 +0200
Subject: [PATCH] Refactors status effects to be based on subsystem ticks,
among a few other minor status effect fixes/refactors (#93694)
## About The Pull Request
Refactors status effects to track their durations and tick intervals
using counters.
In effect, [var/duration] now directly refers to how many deciseconds
are left on the status effect.
I've also moved the old [var/tick_interval] [world.time] implementation
to a tick-based [var/time_until_next_tick] counter.
There are a couple, less noteworthy changes in here as well. The main
one is that there was an unused bit of bloat code for setting tick
intervals based on a random lower and upper threshold, but that can be
done in tick() now so it's completely redundant, and I thus removed it
entirely. That makes parts of [proc/process] much easier to read.
I added/modified some unit tests (which I expect to fail) to verify that
[var/duration] and [var/tick_interval] are both multiples of the
subsystem wait assigned to the status effect. If the programmer wants a
duration of 2.5 seconds, they expect it to work that way, but it won't
because SSfastprocess only ticks once every 0.2 seconds, which 2.5 is
not a multiple of. This becomes way more apparent when a status effect
is set to use SSprocessing.
The final, perhaps most important unit test I've added, is one that
verifies that the overall tick count and overall accumulated
[seconds_between_ticks] are equal to "[var/duration] /
[var/tick_interval]" and "[var/duration]" respectively.
## Why It's Good For The Game
The main thing this PR fixes is timing inconsistencies. Before this PR,
durations and tick intervals were tracked using world.time, while the
[proc/tick] call timing was dependent on the wait time of the subsystem
the status effect was processing on. Thing is, SSfastprocess and
SSprocessing rarely run completely in one tick during real gameplay.
This led to a continuous desync where status effects were consistently
inconsistent in their overall tick count. This is a big problem as
[seconds_between_ticks] is constant and thus doesn't account for this
difference in tick count.
As an example, Changeling's Fleshmend has a duration of 10 seconds, a
tick interval of 1 second and a healing rate of 4 brute per tick.
Previously, if the server was lagging even slightly and it only ticked 8
times over the course of 10 seconds, you would heal 32 health rather
than the 40 that a full Fleshmend would give you. The total effect
potency of a status effect being reliant on server lag is incredibly
stupid, especially for status effects that have an associated cost.
(like the aforementioned Fleshmend)
As for the refactors, they make status effect code easier to read and
debug. Unit tests also make verifying things are working as intended
much easier.
## Changelog
:cl:
fix: Status effects now tick consistently, with Fleshmend and such
giving a consistent total healing amount. Report any oddities.
refactor: Status effect code is now easier to read and makes more sense.
Again, report any oddities, the changes are major.
/:cl:
---
code/datums/status_effects/_status_effect.dm | 58 +++++++------
code/datums/status_effects/buffs.dm | 4 +-
.../status_effects/buffs/stop_drop_roll.dm | 8 +-
code/datums/status_effects/debuffs/choke.dm | 2 +-
.../status_effects/debuffs/dizziness.dm | 2 +-
.../debuffs/dna_transformation.dm | 4 +-
.../status_effects/debuffs/hallucination.dm | 2 +-
.../status_effects/debuffs/jitteriness.dm | 4 +-
.../status_effects/debuffs/screen_blur.dm | 2 +-
.../status_effects/debuffs/staggered.dm | 6 +-
code/datums/status_effects/stacking_effect.dm | 2 +-
.../changeling/powers/darkness_adaptation.dm | 2 +-
.../antagonists/heretic/magic/fire_blast.dm | 2 +-
.../cain_and_abel/dagger_status_effects.dm | 4 +-
.../basic/farm_animals/goose/goose_vomit.dm | 4 +-
code/modules/mob/living/status_procs.dm | 56 ++++++-------
.../projectiles/ammunition/ballistic/rifle.dm | 2 +-
.../spells/spell_types/cone/cone_of_cold.dm | 2 +-
code/modules/unit_tests/burning.dm | 4 +-
code/modules/unit_tests/mob_damage.dm | 2 +-
.../unit_tests/status_effect_validity.dm | 84 +++++++++++++++++--
21 files changed, 161 insertions(+), 95 deletions(-)
diff --git a/code/datums/status_effects/_status_effect.dm b/code/datums/status_effects/_status_effect.dm
index 2b44e8bbaf0..4a569858655 100644
--- a/code/datums/status_effects/_status_effect.dm
+++ b/code/datums/status_effects/_status_effect.dm
@@ -3,19 +3,16 @@
/datum/status_effect
/// The ID of the effect. ID is used in adding and removing effects to check for duplicates, among other things.
var/id = "effect"
- /// When set initially / in on_creation, this is how long the status effect lasts in deciseconds.
- /// While processing, this becomes the world.time when the status effect will expire.
- /// -1 = infinite duration.
+ /// This is how long the status effect lasts in deciseconds.
+ /// You can put STATUS_EFFECT_PERMANENT (or INFINITY) here for infinite duration.
var/duration = STATUS_EFFECT_PERMANENT
- /// When set initially / in on_creation, this is how long between [proc/tick] calls in deciseconds.
- /// Note that this cannot be faster than the processing subsystem you choose to fire the effect on. (See: [var/processing_speed])
- /// While processing, this becomes the world.time when the next tick will occur.
- /// -1 = will prevent ticks, and if duration is also unlimited (-1), stop processing wholesale.
+ /// This is how long between [proc/tick] calls in deciseconds.
+ /// This has to be a multiple of the [var/wait] of the subsystem this status effect is running on, which is based on [var/processing_speed].
+ /// Putting STATUS_EFFECT_NO_TICK here will stop [proc/tick] calls, and if [var/duration] is STATUS_EFFECT_PERMANENT, it stops processing entirely.
+ /// Putting STATUS_EFFECT_AUTO_TICK here will make every subsystem tick call [proc/tick], making the tick interval depend entirely on [var/processing_speed]
var/tick_interval = 1 SECONDS
- ///If our tick intervals are set to be a dynamic value within a range, the lowerbound of said range
- var/tick_interval_lowerbound
- ///If our tick intervals are set to be a dynamic value within a range, the upperbound of said range
- var/tick_interval_upperbound
+ /// The time until the next [proc/tick] call, gets set to [var/tick_interval] after every [proc/tick] call and decrements on every [proc/process] call.
+ var/time_until_next_tick
/// The mob affected by the status effect.
VAR_FINAL/mob/living/owner
/// How many of the effect can be on one mob, and/or what happens when you try to add a duplicate.
@@ -58,10 +55,9 @@
// we will optionally allow INFINITY, because i imagine it'll be convenient in some places,
// but we'll still set it to -1 / STATUS_EFFECT_PERMANENT for proper unified handling
duration = STATUS_EFFECT_PERMANENT
- if(duration != STATUS_EFFECT_PERMANENT)
- duration = world.time + duration
+
if(tick_interval != STATUS_EFFECT_NO_TICK)
- tick_interval = world.time + tick_interval
+ time_until_next_tick = tick_interval
if(alert_type)
var/atom/movable/screen/alert/status_effect/new_alert = owner.throw_alert(id, alert_type)
@@ -69,7 +65,7 @@
linked_alert = new_alert //so we can reference the alert, if we need to
update_shown_duration()
- if(duration > world.time || tick_interval > world.time) //don't process if we don't care
+ if(duration != STATUS_EFFECT_PERMANENT || tick_interval != STATUS_EFFECT_NO_TICK) //don't process if we don't care
switch(processing_speed)
if(STATUS_EFFECT_FAST_PROCESS)
START_PROCESSING(SSfastprocess, src)
@@ -108,7 +104,7 @@
if(!linked_alert || !show_duration)
return
- linked_alert.maptext = MAPTEXT_TINY_UNICODE("[round((duration - world.time)/10, 1)]s")
+ linked_alert.maptext = MAPTEXT_TINY_UNICODE("[round(duration / 10, 1)]s")
// Status effect process. Handles adjusting its duration and ticks.
// If you're adding processed effects, put them in [proc/tick]
@@ -120,19 +116,23 @@
qdel(src)
return
+ if (duration != STATUS_EFFECT_PERMANENT)
+ duration = max(0, duration - (seconds_per_tick SECONDS)) // doing it first means its more up to date for ticks to read
+
+ if (tick_interval != STATUS_EFFECT_NO_TICK)
+ time_until_next_tick = max(0, time_until_next_tick - (seconds_per_tick SECONDS)) // same here
+
if(tick_interval == STATUS_EFFECT_AUTO_TICK)
tick(seconds_per_tick)
- else if(tick_interval != STATUS_EFFECT_NO_TICK && tick_interval < world.time)
- var/tick_length = (tick_interval_upperbound && tick_interval_lowerbound) ? rand(tick_interval_lowerbound, tick_interval_upperbound) : initial(tick_interval)
- tick(tick_length / (1 SECONDS))
- tick_interval = world.time + tick_length
+ else if(tick_interval != STATUS_EFFECT_NO_TICK && time_until_next_tick <= 0)
+ time_until_next_tick = tick_interval // same here as well
+ tick(tick_interval / 10)
if(QDELING(src))
- // tick deleted us, no need to continue
- return
+ return // tick deleted us, no need to continue
if(duration != STATUS_EFFECT_PERMANENT)
- if(duration < world.time)
+ if(duration <= 0)
qdel(src)
return
update_shown_duration()
@@ -185,10 +185,7 @@
/// Called when a status effect of status_type STATUS_EFFECT_REFRESH
/// has its duration refreshed in apply_status_effect - is passed New() args
/datum/status_effect/proc/refresh(effect, ...)
- var/original_duration = initial(duration)
- if(original_duration == STATUS_EFFECT_PERMANENT)
- return
- duration = world.time + original_duration
+ duration = initial(duration)
/// Adds nextmove modifier multiplicatively to the owner while applied
/datum/status_effect/proc/nextmove_modifier()
@@ -208,13 +205,14 @@
if(!heal_flag_necessary || (heal_flags & heal_flag_necessary))
qdel(src)
-/// Remove [seconds] of duration from the status effect, qdeling / ending if we eclipse the current world time.
+/// Removes [seconds] of duration from the status effect.
+/// Returns whether or not the status effect was qdeleted due to running out of duration.
/datum/status_effect/proc/remove_duration(seconds)
if(duration == STATUS_EFFECT_PERMANENT) // Infinite duration
return FALSE
- duration -= seconds
- if(duration <= world.time)
+ duration -= (seconds SECONDS)
+ if(duration <= 0)
qdel(src)
return TRUE
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index d51ebe144ff..75edc87b4ec 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -236,7 +236,7 @@
var/skill_level_boost = (new_owner.mind?.get_skill_level(/datum/skill/athletics) - 1) * 2 SECONDS
bonus_time = (bonus_time + food_boost + skill_level_boost) * modifier
- var/exhaustion_limit = new_owner.mind?.get_skill_modifier(/datum/skill/athletics, SKILL_VALUE_MODIFIER) + world.time
+ var/exhaustion_limit = new_owner.mind?.get_skill_modifier(/datum/skill/athletics, SKILL_VALUE_MODIFIER)
if(duration + bonus_time >= exhaustion_limit)
duration = exhaustion_limit
to_chat(new_owner, span_userdanger("Your muscles are exhausted! Might be a good idea to sleep..."))
@@ -274,7 +274,7 @@
id = "Hippocratic Oath"
status_type = STATUS_EFFECT_UNIQUE
duration = STATUS_EFFECT_PERMANENT
- tick_interval = 2.5 SECONDS
+ tick_interval = 2.6 SECONDS
alert_type = null
var/datum/component/aura_healing/aura_healing
diff --git a/code/datums/status_effects/buffs/stop_drop_roll.dm b/code/datums/status_effects/buffs/stop_drop_roll.dm
index e7ff393a1dd..8618aea5bc3 100644
--- a/code/datums/status_effects/buffs/stop_drop_roll.dm
+++ b/code/datums/status_effects/buffs/stop_drop_roll.dm
@@ -8,8 +8,7 @@
if(!iscarbon(owner))
return FALSE
- var/actual_interval = initial(tick_interval)
- if(!owner.Knockdown(actual_interval * 2, ignore_canstun = TRUE) || owner.body_position != LYING_DOWN)
+ if(!owner.Knockdown(tick_interval * 2, ignore_canstun = TRUE) || owner.body_position != LYING_DOWN)
to_chat(owner, span_warning("You try to stop, drop, and roll - but you can't get on the ground!"))
return FALSE
@@ -40,12 +39,11 @@
qdel(src)
return
- var/actual_interval = initial(tick_interval)
- if(!owner.Knockdown(actual_interval * 1.2, ignore_canstun = TRUE))
+ if(!owner.Knockdown(tick_interval * 1.2, ignore_canstun = TRUE))
stop_rolling()
return
- owner.spin(spintime = actual_interval, speed = actual_interval / 4)
+ owner.spin(spintime = tick_interval, speed = tick_interval / 4)
if(!reduce_firestacks(1))
return
diff --git a/code/datums/status_effects/debuffs/choke.dm b/code/datums/status_effects/debuffs/choke.dm
index e8af4b1b94d..05f41dcd178 100644
--- a/code/datums/status_effects/debuffs/choke.dm
+++ b/code/datums/status_effects/debuffs/choke.dm
@@ -66,7 +66,7 @@
if(flaming)
ash = new(owner, /particles/smoke/ash, PARTICLE_ATTACH_MOB)
var/clear_in = rand(15 SECONDS, 25 SECONDS)
- if(duration != -1)
+ if(duration != STATUS_EFFECT_PERMANENT)
clear_in = min(duration, clear_in)
addtimer(CALLBACK(src, PROC_REF(clear_flame)), clear_in)
return TRUE
diff --git a/code/datums/status_effects/debuffs/dizziness.dm b/code/datums/status_effects/debuffs/dizziness.dm
index b47083aa5e3..547ba44df50 100644
--- a/code/datums/status_effects/debuffs/dizziness.dm
+++ b/code/datums/status_effects/debuffs/dizziness.dm
@@ -26,7 +26,7 @@
/datum/status_effect/dizziness/tick(seconds_between_ticks)
// How much time is left, in seconds
- var/amount = (duration - world.time) / 10
+ var/amount = duration / 10
if(amount <= 0)
return
diff --git a/code/datums/status_effects/debuffs/dna_transformation.dm b/code/datums/status_effects/debuffs/dna_transformation.dm
index 46e125a391e..bd2a079d6f8 100644
--- a/code/datums/status_effects/debuffs/dna_transformation.dm
+++ b/code/datums/status_effects/debuffs/dna_transformation.dm
@@ -82,10 +82,10 @@
if(duration == STATUS_EFFECT_PERMANENT)
return // Already paused
- time_before_pause = duration - world.time
+ time_before_pause = duration
duration = STATUS_EFFECT_PERMANENT
// Resume if we're none of the above and also were paused
else if(time_before_pause != -1)
- duration = time_before_pause + world.time
+ duration = time_before_pause
time_before_pause = -1
diff --git a/code/datums/status_effects/debuffs/hallucination.dm b/code/datums/status_effects/debuffs/hallucination.dm
index e5528a66382..9c40203c1dd 100644
--- a/code/datums/status_effects/debuffs/hallucination.dm
+++ b/code/datums/status_effects/debuffs/hallucination.dm
@@ -94,7 +94,7 @@
var/lower_cd = lower_tick_interval
var/upper_cd = upper_tick_interval
if(variable_tier)
- var/seconds_left = (duration - world.time) / 10
+ var/seconds_left = duration / 10
switch(seconds_left)
if(0 to 20)
max_hallucination_tier = HALLUCINATION_TIER_COMMON
diff --git a/code/datums/status_effects/debuffs/jitteriness.dm b/code/datums/status_effects/debuffs/jitteriness.dm
index ad9a7625133..50bb8b5e09f 100644
--- a/code/datums/status_effects/debuffs/jitteriness.dm
+++ b/code/datums/status_effects/debuffs/jitteriness.dm
@@ -25,7 +25,7 @@
owner.update_offsets()
/datum/status_effect/jitter/get_examine_text()
- switch(duration - world.time)
+ switch(duration)
if(5 MINUTES to INFINITY)
return span_boldwarning("[owner.p_They()] [owner.p_are()] convulsing violently!")
if(3 MINUTES to 5 MINUTES)
@@ -47,7 +47,7 @@
if(owner.resting && remove_duration(4 * seconds_between_ticks))
return
- var/time_left_in_seconds = (duration - world.time) / 10
+ var/time_left_in_seconds = duration / 10
owner.do_jitter_animation(time_left_in_seconds)
/// Helper proc that causes the mob to do a jittering animation by jitter_amount.
diff --git a/code/datums/status_effects/debuffs/screen_blur.dm b/code/datums/status_effects/debuffs/screen_blur.dm
index acdce13b5a0..ee893610c12 100644
--- a/code/datums/status_effects/debuffs/screen_blur.dm
+++ b/code/datums/status_effects/debuffs/screen_blur.dm
@@ -49,7 +49,7 @@
game_plane_master_controller.remove_filter("eye_blur")
return
- var/time_left_in_seconds = (duration - world.time) / (1 SECONDS)
+ var/time_left_in_seconds = duration / 10
var/amount_of_blur = clamp(time_left_in_seconds * BLUR_DURATION_TO_INTENSITY, 0.6, 3)
game_plane_master_controller.add_filter("eye_blur", 1, gauss_blur_filter(amount_of_blur))
diff --git a/code/datums/status_effects/debuffs/staggered.dm b/code/datums/status_effects/debuffs/staggered.dm
index 9919a156c31..4d640b64331 100644
--- a/code/datums/status_effects/debuffs/staggered.dm
+++ b/code/datums/status_effects/debuffs/staggered.dm
@@ -51,7 +51,7 @@
/datum/status_effect/dazed
id = "dazed"
status_type = STATUS_EFFECT_UNIQUE
- tick_interval = 0.5 SECONDS
+ tick_interval = 0.6 SECONDS
alert_type = null
remove_on_fullheal = TRUE
/// Our visual cue for the vulnerable state this status effect puts us in.
@@ -121,9 +121,9 @@
/// Only applied by shoving someone to paralyze them
/datum/status_effect/no_side_kick
id = "no side kick"
- duration = 3.5 SECONDS
+ duration = 3.6 SECONDS
status_type = STATUS_EFFECT_UNIQUE
- tick_interval = 0.5 SECONDS
+ tick_interval = 0.6 SECONDS
alert_type = null
remove_on_fullheal = TRUE
diff --git a/code/datums/status_effects/stacking_effect.dm b/code/datums/status_effects/stacking_effect.dm
index 287419028b0..327c614f8de 100644
--- a/code/datums/status_effects/stacking_effect.dm
+++ b/code/datums/status_effects/stacking_effect.dm
@@ -123,7 +123,7 @@
on_threshold_drop()
if(stacks_added > 0)
- tick_interval += delay_before_decay // refreshes time until decay
+ time_until_next_tick += delay_before_decay // refreshes time until decay
if (status_overlay)
status_overlay.icon_state = "[overlay_state][stacks]"
diff --git a/code/modules/antagonists/changeling/powers/darkness_adaptation.dm b/code/modules/antagonists/changeling/powers/darkness_adaptation.dm
index 47fe31c215b..4d8e8a7e474 100644
--- a/code/modules/antagonists/changeling/powers/darkness_adaptation.dm
+++ b/code/modules/antagonists/changeling/powers/darkness_adaptation.dm
@@ -51,7 +51,7 @@
/// Makes the user harder to see in the dark (and makes the user see in the dark easier)
/datum/status_effect/darkness_adapted
id = "darkness_adapted"
- tick_interval = 0.5 SECONDS
+ tick_interval = 0.6 SECONDS
alert_type = null
/// Threshold before the dark color is applied
var/dark_color_threshold = 70
diff --git a/code/modules/antagonists/heretic/magic/fire_blast.dm b/code/modules/antagonists/heretic/magic/fire_blast.dm
index 873f131c28f..36ed3b4caab 100644
--- a/code/modules/antagonists/heretic/magic/fire_blast.dm
+++ b/code/modules/antagonists/heretic/magic/fire_blast.dm
@@ -157,7 +157,7 @@
id = "fire_blasted"
alert_type = null
duration = 5 SECONDS
- tick_interval = 0.5 SECONDS
+ tick_interval = 0.6 SECONDS
/// How much fire / stam to do per tick (stamina damage is doubled this)
var/tick_damage = 1
/// How long does the animation of the appearance last? If 0 or negative, we make no overlay
diff --git a/code/modules/mining/lavaland/cain_and_abel/dagger_status_effects.dm b/code/modules/mining/lavaland/cain_and_abel/dagger_status_effects.dm
index 0f535f31951..a6cb6c04e67 100644
--- a/code/modules/mining/lavaland/cain_and_abel/dagger_status_effects.dm
+++ b/code/modules/mining/lavaland/cain_and_abel/dagger_status_effects.dm
@@ -1,8 +1,8 @@
///status effect applied to us when we're wildly swinging
/datum/status_effect/dagger_swinging
id = "dagger swinging"
- tick_interval = 0.25 SECONDS
- duration = 1.75 SECONDS
+ tick_interval = 0.2 SECONDS
+ duration = 1.6 SECONDS
alert_type = null
///base damage we apply to mobs near us
var/base_damage = 5
diff --git a/code/modules/mob/living/basic/farm_animals/goose/goose_vomit.dm b/code/modules/mob/living/basic/farm_animals/goose/goose_vomit.dm
index 2b347903e22..2d3f2b6df20 100644
--- a/code/modules/mob/living/basic/farm_animals/goose/goose_vomit.dm
+++ b/code/modules/mob/living/basic/farm_animals/goose/goose_vomit.dm
@@ -124,7 +124,7 @@
/// Stop fucking around and get the rest of it out
/datum/status_effect/goose_vomit/proc/vomit_finale()
- tick_interval = 0.1 SECONDS
+ tick_interval = 0.2 SECONDS
owner.set_jitter_if_lower(1 SECONDS)
hurl_item(vomit_strongly = TRUE)
@@ -176,7 +176,7 @@
/datum/status_effect/goose_choking/on_remove()
UnregisterSignal(owner, COMSIG_LIVING_DEATH)
- if (duration >= world.time)
+ if (duration > 0)
return // Saved by something, although probably by dying early
owner.death_message = "lets out one final oxygen-deprived honk before [owner.p_they()] go[owner.p_es()] limp and lifeless.."
owner.death()
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index 58677d64bef..867c975ad21 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -36,7 +36,7 @@
/mob/living/proc/AmountStun() //How many deciseconds remain in our stun
var/datum/status_effect/incapacitating/stun/S = IsStun()
if(S)
- return S.duration - world.time
+ return S.duration
return 0
/mob/living/proc/Stun(amount, ignore_canstun = FALSE) //Can't go below remaining duration
@@ -46,7 +46,7 @@
return
var/datum/status_effect/incapacitating/stun/S = IsStun()
if(S)
- S.duration = max(world.time + amount, S.duration)
+ S.duration = max(amount, S.duration)
else if(amount > 0)
S = apply_status_effect(/datum/status_effect/incapacitating/stun, amount)
return S
@@ -62,7 +62,7 @@
qdel(S)
else
if(S)
- S.duration = world.time + amount
+ S.duration = amount
else
S = apply_status_effect(/datum/status_effect/incapacitating/stun, amount)
return S
@@ -86,7 +86,7 @@
/mob/living/proc/AmountKnockdown() //How many deciseconds remain in our knockdown
var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown()
if(K)
- return K.duration - world.time
+ return K.duration
return 0
/mob/living/proc/Knockdown(amount, daze_amount = 0, ignore_canstun = FALSE) //Can't go below remaining duration
@@ -96,7 +96,7 @@
return
var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown()
if(K)
- K.duration = max(world.time + amount, K.duration)
+ K.duration = max(amount, K.duration)
else if(amount > 0)
K = apply_status_effect(/datum/status_effect/incapacitating/knockdown, amount)
if(daze_amount > 0)
@@ -114,7 +114,7 @@
qdel(K)
else
if(K)
- K.duration = world.time + amount
+ K.duration = amount
else
K = apply_status_effect(/datum/status_effect/incapacitating/knockdown, amount)
return K
@@ -140,7 +140,7 @@
/mob/living/proc/AmountImmobilized() //How many deciseconds remain in our Immobilized status effect
var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized()
if(I)
- return I.duration - world.time
+ return I.duration
return 0
/mob/living/proc/Immobilize(amount, ignore_canstun = FALSE) //Can't go below remaining duration
@@ -150,7 +150,7 @@
return
var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized()
if(I)
- I.duration = max(world.time + amount, I.duration)
+ I.duration = max(amount, I.duration)
else if(amount > 0)
I = apply_status_effect(/datum/status_effect/incapacitating/immobilized, amount)
return I
@@ -166,7 +166,7 @@
qdel(I)
else
if(I)
- I.duration = world.time + amount
+ I.duration = amount
else
I = apply_status_effect(/datum/status_effect/incapacitating/immobilized, amount)
return I
@@ -190,7 +190,7 @@
/mob/living/proc/AmountParalyzed() //How many deciseconds remain in our Paralyzed status effect
var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE)
if(P)
- return P.duration - world.time
+ return P.duration
return 0
/mob/living/proc/Paralyze(amount, ignore_canstun = FALSE) //Can't go below remaining duration
@@ -200,7 +200,7 @@
return
var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE)
if(P)
- P.duration = max(world.time + amount, P.duration)
+ P.duration = max(amount, P.duration)
else if(amount > 0)
P = apply_status_effect(/datum/status_effect/incapacitating/paralyzed, amount)
return P
@@ -216,7 +216,7 @@
qdel(P)
else
if(P)
- P.duration = world.time + amount
+ P.duration = amount
else
P = apply_status_effect(/datum/status_effect/incapacitating/paralyzed, amount)
return P
@@ -240,7 +240,7 @@
/mob/living/proc/amount_incapacitated()
var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated)
if (incapacitated_status_effect)
- return incapacitated_status_effect.duration - world.time
+ return incapacitated_status_effect.duration
else
return 0
@@ -256,7 +256,7 @@
return
var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated)
if(incapacitated_status_effect)
- incapacitated_status_effect.duration = max(world.time + amount, incapacitated_status_effect.duration)
+ incapacitated_status_effect.duration = max(amount, incapacitated_status_effect.duration)
else if(amount > 0)
incapacitated_status_effect = apply_status_effect(/datum/status_effect/incapacitating/incapacitated, amount)
return incapacitated_status_effect
@@ -278,7 +278,7 @@
qdel(incapacitated_status_effect)
else
if(incapacitated_status_effect)
- incapacitated_status_effect.duration = world.time + amount
+ incapacitated_status_effect.duration = amount
else
incapacitated_status_effect = apply_status_effect(/datum/status_effect/incapacitating/incapacitated, amount)
return incapacitated_status_effect
@@ -333,7 +333,7 @@
/mob/living/proc/AmountUnconscious() //How many deciseconds remain in our unconsciousness
var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious()
if(U)
- return U.duration - world.time
+ return U.duration
return 0
/mob/living/proc/Unconscious(amount, ignore_canstun = FALSE) //Can't go below remaining duration
@@ -343,7 +343,7 @@
return
var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious()
if(U)
- U.duration = max(world.time + amount, U.duration)
+ U.duration = max(amount, U.duration)
else if(amount > 0)
U = apply_status_effect(/datum/status_effect/incapacitating/unconscious, amount)
return U
@@ -358,7 +358,7 @@
if(U)
qdel(U)
else if(U)
- U.duration = world.time + amount
+ U.duration = amount
else
U = apply_status_effect(/datum/status_effect/incapacitating/unconscious, amount)
return U
@@ -383,7 +383,7 @@
/mob/living/proc/AmountSleeping() //How many deciseconds remain in our sleep
var/datum/status_effect/incapacitating/sleeping/S = IsSleeping()
if(S)
- return S.duration - world.time
+ return S.duration
return 0
/mob/living/proc/Sleeping(amount) //Can't go below remaining duration
@@ -393,7 +393,7 @@
return
var/datum/status_effect/incapacitating/sleeping/S = IsSleeping()
if(S)
- S.duration = max(world.time + amount, S.duration)
+ S.duration = max(amount, S.duration)
else if(amount > 0)
S = apply_status_effect(/datum/status_effect/incapacitating/sleeping, amount)
return S
@@ -408,7 +408,7 @@
if(S)
qdel(S)
else if(S)
- S.duration = world.time + amount
+ S.duration = amount
else
S = apply_status_effect(/datum/status_effect/incapacitating/sleeping, amount)
return S
@@ -622,13 +622,12 @@
if(isnum(max_duration) && duration > 0)
// Check the duration remaining on the existing status effect
// If it's greater than / equal to our passed max duration, we don't need to do anything
- var/remaining_duration = existing.duration - world.time
- if(remaining_duration >= max_duration)
+ if(existing.duration >= max_duration)
return
// Otherwise, add duration up to the max (max_duration - remaining_duration),
// or just add duration if it doesn't exceed our max at all
- existing.duration += min(max_duration - remaining_duration, duration)
+ existing.duration += min(max_duration - existing.duration, duration)
else
existing.duration += duration
@@ -636,7 +635,7 @@
// If the duration was decreased and is now less 0 seconds,
// qdel it / clean up the status effect immediately
// (rather than waiting for the process tick to handle it)
- if(existing.duration <= world.time)
+ if(existing.duration <= 0)
qdel(existing)
else if(duration > 0)
@@ -669,12 +668,11 @@
if(only_if_higher)
// If the existing status effect has a higher remaining duration
// than what we aim to set it to, don't downgrade it - do nothing (return)
- var/remaining_duration = existing.duration - world.time
- if(remaining_duration >= duration)
+ if(existing.duration >= duration)
return
// Set the duration accordingly
- existing.duration = world.time + duration
+ existing.duration = duration
else if(duration > 0)
apply_status_effect(effect, duration)
@@ -697,7 +695,7 @@
if(existing.duration == STATUS_EFFECT_PERMANENT)
return INFINITY
- return existing.duration - world.time
+ return existing.duration
/**
* Adjust the "drunk value" the mob is currently experiencing,
diff --git a/code/modules/projectiles/ammunition/ballistic/rifle.dm b/code/modules/projectiles/ammunition/ballistic/rifle.dm
index 2ec850950b0..ab4e38dbb95 100644
--- a/code/modules/projectiles/ammunition/ballistic/rifle.dm
+++ b/code/modules/projectiles/ammunition/ballistic/rifle.dm
@@ -154,7 +154,7 @@
if (can_sleep && (owner.mob_biotypes & MOB_ORGANIC))
owner.adjust_drowsiness(drowsy_per_second * seconds_per_tick)
var/datum/status_effect/drowsiness/drowsiness = owner.has_status_effect(/datum/status_effect/drowsiness)
- if (drowsiness?.duration - world.time >= drowsy_knockout)
+ if (drowsiness?.duration >= drowsy_knockout)
owner.Sleeping(3 SECONDS)
if (casing.heals_left <= 0)
fall_out()
diff --git a/code/modules/spells/spell_types/cone/cone_of_cold.dm b/code/modules/spells/spell_types/cone/cone_of_cold.dm
index 9327e2c2fad..3b8f880a842 100644
--- a/code/modules/spells/spell_types/cone/cone_of_cold.dm
+++ b/code/modules/spells/spell_types/cone/cone_of_cold.dm
@@ -50,7 +50,7 @@
if(ispath(frozen_status_effect_path) && unfreeze_mob_duration > 0 SECONDS) // 0 duration = don't apply the status effect
var/datum/status_effect/freeze = target_mob.apply_status_effect(frozen_status_effect_path)
if(unfreeze_mob_duration != INFINITY)
- freeze.duration = world.time + unfreeze_mob_duration
+ freeze.duration = unfreeze_mob_duration
if(on_freeze_brute_damage || on_freeze_burn_damage)
target_mob.take_overall_damage(on_freeze_brute_damage, on_freeze_burn_damage)
diff --git a/code/modules/unit_tests/burning.dm b/code/modules/unit_tests/burning.dm
index daf99875f28..a23d5423c27 100644
--- a/code/modules/unit_tests/burning.dm
+++ b/code/modules/unit_tests/burning.dm
@@ -10,8 +10,8 @@
TEST_ASSERT(dummy.on_fire, "Dummy is not on fire despite having 20 fire stacks and being ignited.")
// Manually tick it a few times
var/datum/status_effect/fire_handler/fire_stacks/handler = locate() in dummy.status_effects
+ handler.tick_interval = STATUS_EFFECT_AUTO_TICK
for(var/i in 1 to 5)
- handler.tick_interval = world.time - 1
- handler.process()
+ handler.process(1)
TEST_ASSERT(dummy.fire_stacks < 20, "Dummy should have decayed firestacks, but did not. (Dummy stacks: [dummy.fire_stacks]).")
TEST_ASSERT(dummy.bodytemperature > initial_temp, "Dummy did not heat up despite being on fire. (Dummy temp: [dummy.bodytemperature], initial temp: [initial_temp])")
diff --git a/code/modules/unit_tests/mob_damage.dm b/code/modules/unit_tests/mob_damage.dm
index 1cc63466bc9..5567e3aa454 100644
--- a/code/modules/unit_tests/mob_damage.dm
+++ b/code/modules/unit_tests/mob_damage.dm
@@ -608,7 +608,7 @@
TEST_ASSERT_NOTNULL(tox_effect, "Dummy didn't get tox_vomit status effect despite at [dummy.getToxLoss()] toxin damage (Method: SET)!")
// Clear the toxin damage away, and force a status effect tick: It should delete itself
dummy.setToxLoss(0)
- tox_effect.tick(initial(tox_effect.tick_interval))
+ tox_effect.tick(tox_effect.tick_interval)
TEST_ASSERT(QDELETED(tox_effect), "Dummy still has tox_vomit status effect despite at [dummy.getToxLoss()] toxin damage (Method: SET)!")
// Test another method of gaining tox damage, use an entirely clean slate just to be sure
var/mob/living/carbon/human/dummy_two = allocate(/mob/living/carbon/human/consistent)
diff --git a/code/modules/unit_tests/status_effect_validity.dm b/code/modules/unit_tests/status_effect_validity.dm
index 44c7b391610..e9c7cd70c90 100644
--- a/code/modules/unit_tests/status_effect_validity.dm
+++ b/code/modules/unit_tests/status_effect_validity.dm
@@ -8,23 +8,95 @@
var/tick_speed = initial(checking.tick_interval)
if(tick_speed == STATUS_EFFECT_NO_TICK)
continue
+ if(tick_speed < 0)
+ TEST_FAIL("Status effect [checking] has tick_interval set to a negative value other than STATUS_EFFECT_NO_TICK, this is not how you prevent ticks - use tick_interval = STATUS_EFFECT_NO_TICK instead.")
+ continue
if(tick_speed == INFINITY)
TEST_FAIL("Status effect [checking] has tick_interval set to INFINITY, this is not how you prevent ticks - use tick_interval = STATUS_EFFECT_NO_TICK instead.")
continue
switch(initial(checking.processing_speed))
if(STATUS_EFFECT_FAST_PROCESS)
- if(tick_speed < SSfastprocess.wait && tick_speed != STATUS_EFFECT_AUTO_TICK)
- TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is faster than SSfastprocess can tick ([SSfastprocess.wait]).")
+ if(tick_speed % SSfastprocess.wait != 0 && tick_speed != STATUS_EFFECT_AUTO_TICK)
+ TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is not a multiple of SSfastprocess wait time ([SSfastprocess.wait]).")
if(STATUS_EFFECT_NORMAL_PROCESS)
- if(tick_speed < SSprocessing.wait && tick_speed != STATUS_EFFECT_AUTO_TICK)
- TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is faster than SSprocessing can tick ([SSprocessing.wait]).")
+ if(tick_speed % SSprocessing.wait != 0 && tick_speed != STATUS_EFFECT_AUTO_TICK)
+ TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is not a multiple of SSprocessing wait time ([SSprocessing.wait]).")
if(STATUS_EFFECT_PRIORITY)
var/priority_wait = world.tick_lag * SSpriority_effects.wait // SSpriority_effects has the SS_TICKER flag, so its wait is in ticks, so we have to convert it to deciseconds.
- if(tick_speed < priority_wait && tick_speed != STATUS_EFFECT_AUTO_TICK)
- TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is faster than SSpriority_effects can tick ([priority_wait]).")
+ if(tick_speed % priority_wait != 0 && tick_speed != STATUS_EFFECT_AUTO_TICK)
+ TEST_FAIL("Status effect [checking] has tick_interval set to [tick_speed], which is not a multiple of SSpriority_effects wait time ([priority_wait]).")
else
TEST_FAIL("Invalid processing speed for status effect [checking] : [initial(checking.processing_speed)]")
+/// Validates status effect duration setup
+/datum/unit_test/status_effect_duration
+
+/datum/unit_test/status_effect_duration/Run()
+ for(var/datum/status_effect/checking as anything in subtypesof(/datum/status_effect))
+ if(initial(checking.id) == STATUS_EFFECT_ID_ABSTRACT)
+ continue
+ var/duration = initial(checking.duration)
+ if(duration == STATUS_EFFECT_PERMANENT)
+ continue
+ if(duration == INFINITY) // for some god forsaken reason, this is allowed
+ continue
+ if(duration < 0)
+ TEST_FAIL("Status effect [checking] has duration set to a negative value other than STATUS_EFFECT_PERMANENT, this is not how you make effects last forever - use duration = STATUS_EFFECT_PERMANENT instead.")
+ continue
+ switch(initial(checking.processing_speed))
+ if(STATUS_EFFECT_FAST_PROCESS)
+ if(duration % SSfastprocess.wait != 0)
+ TEST_FAIL("Status effect [checking] has duration set to [duration], which is not a multiple of SSfastprocess wait time ([SSfastprocess.wait]).")
+ if(STATUS_EFFECT_NORMAL_PROCESS)
+ if(duration % SSprocessing.wait != 0)
+ TEST_FAIL("Status effect [checking] has duration set to [duration], which is not a multiple of SSprocessing wait time ([SSprocessing.wait]).")
+ if(STATUS_EFFECT_PRIORITY)
+ var/priority_wait = world.tick_lag * SSpriority_effects.wait // SSpriority_effects has the SS_TICKER flag, so its wait is in ticks, so we have to convert it to deciseconds.
+ if(duration % priority_wait != 0)
+ TEST_FAIL("Status effect [checking] has duration set to [duration], which is not a multiple of SSpriority_effects wait time ([priority_wait]).")
+ else
+ TEST_FAIL("Invalid processing speed for status effect [checking] : [initial(checking.processing_speed)]")
+
+/// Validates that status effect tick counts are directly proportional to duration, and that seconds_between_ticks added up is equal to duration.
+/datum/unit_test/status_effect_tick_counts
+
+/datum/unit_test/status_effect_tick_counts/Run()
+ var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
+
+ var/datum/status_effect/unit_test_tick_counter/counter = user.apply_status_effect(/datum/status_effect/unit_test_tick_counter)
+
+ // The 0.2 here and in the for loop is arbitrary and can be any value that divides evenly into the duration and tick interval of the unit test status effect.
+ // I chose 0.2 specifically because it just happens to be the SSfastprocess.wait in seconds, which basically simulates this status effect running on SSfastprocess.
+ var/ticks_required = counter.duration / 10 / 0.2 + 1
+
+ for (var/i in 1 to ticks_required)
+ if (!QDELETED(counter))
+ counter.process(0.2)
+
+ var/expected_tick_count = initial(counter.duration) / counter.tick_interval
+ if (abs(counter.total_tick_count - expected_tick_count) > 0.01)
+ TEST_FAIL("Status effect tick count is not directly proportional to duration. Expected [expected_tick_count] ticks, got [counter.total_tick_count] ticks.")
+
+ var/expected_seconds = initial(counter.duration) / 10
+ if (abs(counter.total_seconds - expected_seconds) > 0.01)
+ TEST_FAIL("Status effect seconds_between_ticks accumulated together does not equal duration. Expected [expected_seconds] seconds, got [counter.total_seconds] seconds.")
+
+ QDEL_NULL(counter)
+
+/datum/status_effect/unit_test_tick_counter
+ duration = 10 SECONDS
+ tick_interval = 0.4 SECONDS
+
+ id = "unit_test_tick_counter"
+ alert_type = null
+
+ var/total_tick_count = 0
+ var/total_seconds = 0
+
+/datum/status_effect/unit_test_tick_counter/tick(seconds_between_ticks)
+ total_tick_count++
+ total_seconds += seconds_between_ticks
+
/// Validates status effect alert type setup
/datum/unit_test/status_effect_alert