diff --git a/code/__defines/alerts.dm b/code/__defines/alerts.dm
new file mode 100644
index 0000000000..3ff75d197f
--- /dev/null
+++ b/code/__defines/alerts.dm
@@ -0,0 +1 @@
+#define ALERT_FIRE "fire"
diff --git a/code/__defines/dcs/signals.dm b/code/__defines/dcs/signals.dm
index 78c0a86c81..206a9a416c 100644
--- a/code/__defines/dcs/signals.dm
+++ b/code/__defines/dcs/signals.dm
@@ -343,7 +343,7 @@
///from base of mob/living/resist() (/mob/living)
#define COMSIG_LIVING_RESIST "living_resist"
-///from base of mob/living/IgniteMob() (/mob/living)
+///from base of mob/living/ignite_mob() (/mob/living)
#define COMSIG_LIVING_IGNITED "living_ignite"
///from base of mob/living/ExtinguishMob() (/mob/living)
#define COMSIG_LIVING_EXTINGUISHED "living_extinguished"
diff --git a/code/__defines/dcs/signals/signals_atom/signals_atom_main.dm b/code/__defines/dcs/signals/signals_atom/signals_atom_main.dm
index 52a5559ec0..08ea5a00bb 100644
--- a/code/__defines/dcs/signals/signals_atom/signals_atom_main.dm
+++ b/code/__defines/dcs/signals/signals_atom/signals_atom_main.dm
@@ -7,3 +7,6 @@
///When the transform or an atom is varedited through vv topic.
#define COMSIG_ATOM_VV_MODIFY_TRANSFORM "atom_vv_modify_transform"
+
+/// from base of [/atom/proc/extinguish]
+#define COMSIG_ATOM_EXTINGUISH "atom_extinguish"
diff --git a/code/__defines/dcs/signals/signals_mob/signals_mob_main.dm b/code/__defines/dcs/signals/signals_mob/signals_mob_main.dm
index 7ca76e4d5f..16e7b5a20f 100644
--- a/code/__defines/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__defines/dcs/signals/signals_mob/signals_mob_main.dm
@@ -10,3 +10,14 @@
/// from /mob/living/proc/apply_damage(): (damage, damagetype, def_zone, blocked, soaked, sharp, edge, /obj/used_weapon, projectile)
/// works like above but after the damage is actually inflicted
#define COMSIG_MOB_AFTER_APPLY_DAMAGE "mob_after_apply_damage"
+///from end of revival_healing_action(): ()
+#define COMSIG_LIVING_AHEAL "living_post_aheal"
+
+/// From /datum/status_effect/proc/on_creation() : (datum/status_effect/effect)
+#define COMSIG_LIVING_STATUS_APPLIED "living_status_applied"
+
+/// From /datum/status_effect/proc/Destroy() : (datum/status_effect/effect)
+#define COMSIG_LIVING_STATUS_REMOVED "living_status_removed"
+
+///from /datum/species/handle_fire. Called when the human is set on fire and burning clothes and stuff
+#define COMSIG_HUMAN_BURNING "human_burning"
diff --git a/code/__defines/particles.dm b/code/__defines/particles.dm
new file mode 100644
index 0000000000..5657566a63
--- /dev/null
+++ b/code/__defines/particles.dm
@@ -0,0 +1,5 @@
+// /obj/effect/abstract/particle_holder/var/particle_flags
+// Flags that effect how a particle holder displays something
+
+/// If we're inside something inside a mob, display off that mob too
+#define PARTICLE_ATTACH_MOB (1<<0)
diff --git a/code/__defines/status_effects.dm b/code/__defines/status_effects.dm
new file mode 100644
index 0000000000..903a67443e
--- /dev/null
+++ b/code/__defines/status_effects.dm
@@ -0,0 +1,202 @@
+///if it allows multiple instances of the effect
+#define STATUS_EFFECT_MULTIPLE 0
+///if it allows only one, preventing new instances
+#define STATUS_EFFECT_UNIQUE 1
+///if it allows only one, but new instances replace
+#define STATUS_EFFECT_REPLACE 2
+/// if it only allows one, and new instances just instead refresh the timer
+#define STATUS_EFFECT_REFRESH 3
+
+/// Use in status effect "duration" to make it last forever
+#define STATUS_EFFECT_PERMANENT -1
+/// Use in status effect "tick_interval" to prevent it from calling tick()
+#define STATUS_EFFECT_NO_TICK -1
+/// Use in status effect "tick_interval" to guarantee that tick() gets called on every process()
+#define STATUS_EFFECT_AUTO_TICK 0
+
+/// Indicates this status effect is an abstract type, ie not instantiated
+/// Doesn't actually do anything in practice, primarily just a marker / used in unit tests,
+/// so don't worry if your abstract status effect doesn't actually set this
+#define STATUS_EFFECT_ID_ABSTRACT "abstract"
+
+///Processing flags - used to define the speed at which the status will work
+/// This is fast - 0.2s between ticks (I believe!)
+#define STATUS_EFFECT_FAST_PROCESS 0
+/// This is slower and better for more intensive status effects - 1s between ticks
+#define STATUS_EFFECT_NORMAL_PROCESS 1
+/// Similar speed to STATUS_EFFECT_FAST_PROCESS, but uses a high priority subsystem (SSpriority_effects)
+#define STATUS_EFFECT_PRIORITY 2
+
+//several flags for the Necropolis curse status effect
+///makes the edges of the target's screen obscured
+// #define CURSE_BLINDING (1<<0)
+///causes gradual damage
+// #define CURSE_WASTING (1<<1)
+///hands reach out from the sides of the screen, doing damage and stunning if they hit the target
+// #define CURSE_GRASPING (1<<2)
+
+//Incapacitated status effect flags
+/// If the mob is normal incapacitated. Should never need this, just avoids issues if we ever overexpand this
+// #define TRADITIONAL_INCAPACITATED (1<<0)
+/// If the incapacitated status effect is being caused by restraints (handcuffs)
+// #define INCAPABLE_RESTRAINTS (1<<1)
+/// If the incapacitated status effect is being caused by stasis (stasis beds)
+// #define INCAPABLE_STASIS (1<<2)
+/// If the incapacitated status effect is being caused by being agressively grabbed
+// #define INCAPABLE_GRAB (1<<3)
+
+/// Checks to see if a mob would be incapacitated even while ignoring some types
+/// Does this by inverting the passed in flags and seeing if we're still incapacitated
+// #define INCAPACITATED_IGNORING(mob, flags) (mob.incapacitated & ~(flags))
+
+/// Max amounts of fire stacks a mob can get
+#define MAX_FIRE_STACKS 20
+/// If a mob has a higher threshold than this, the icon shown will be increased to the big fire icon.
+#define MOB_BIG_FIRE_STACK_THRESHOLD 3
+
+// Grouped effect sources, see also code/__DEFINES/traits.dm
+
+#define STASIS_MACHINE_EFFECT "stasis_machine"
+#define STASIS_CHEMICAL_EFFECT "stasis_chemical"
+#define STASIS_SHAPECHANGE_EFFECT "stasis_shapechange"
+#define STASIS_ADMIN "stasis_admin"
+#define STASIS_LEGION_EATEN "stasis_eaten"
+#define STASIS_SLIME_BZ "stasis_slime_bz"
+#define STASIS_ELDRITCH_ETHER "stasis_eldritch_ether"
+
+#define STASIS_NETPOD_EFFECT "stasis_netpod"
+
+/// Causes the mob to become blind via the passed source
+// #define become_blind(source) apply_status_effect(/datum/status_effect/grouped/blindness, source)
+/// Cures the mob's blindness from the passed source, removing blindness wholesale if no sources are left
+// #define cure_blind(source) remove_status_effect(/datum/status_effect/grouped/blindness, source)
+
+/// Is the mob blind?
+// #define is_blind(...) has_status_effect(/datum/status_effect/grouped/blindness)
+/// Is the mob blind from the passed source or sources?
+// #define is_blind_from(sources) has_status_effect_from_source(/datum/status_effect/grouped/blindness, sources)
+
+/// We are not nearsighted right now.
+// #define NEARSIGHTED_DISABLED 0
+/// Something is correcting our vision, but we are still a bit nearsighted.
+// #define NEARSIGHTED_CORRECTED 1
+/// We are fully nearsighted.
+// #define NEARSIGHTED_ENABLED 2
+
+/// Simplified macro that causes the mob to become nearsighted (with correction possible) via the passed source.
+// #define become_nearsighted(source) apply_status_effect(/datum/status_effect/grouped/nearsighted, source)
+/// Causes the mob to become nearsighted from the passed source by a severity, which may be corrected with glasses.
+// #define assign_nearsightedness(source, amount, correctable) apply_status_effect(/datum/status_effect/grouped/nearsighted, source, amount, correctable)
+/// Cures the mob's nearsightedness from the passed source, removing nearsighted wholesale if no sources are left.
+// #define cure_nearsighted(source) remove_status_effect(/datum/status_effect/grouped/nearsighted, source)
+
+/// Is the mob nearsighted?
+// #define is_nearsighted(...) has_status_effect(/datum/status_effect/grouped/nearsighted)
+/// Is the mob nearsigthed from the passed source or sources?
+// #define is_nearsighted_from(sources) has_status_effect_from_source(/datum/status_effect/grouped/nearsighted, sources)
+/// Is the mob nearsighted CURRENTLY?
+/// This check fails if the mob is nearsighted but is wearing glasses,
+/// While is_nearsighted will always succeed even if they are wearing glasses.
+// /mob/proc/is_nearsighted_currently()
+// var/datum/status_effect/grouped/nearsighted/nearsight = has_status_effect(/datum/status_effect/grouped/nearsighted)
+// if(isnull(nearsight))
+// return FALSE
+// return (nearsight.should_be_nearsighted() > NEARSIGHTED_CORRECTED)
+
+// Status effect application helpers.
+// These are macros for easier use of adjust_timed_status_effect and set_timed_status_effect.
+//
+// adjust_x:
+// - Adds duration to a status effect
+// - Removes duration if a negative duration is passed.
+// - Ex: adjust_stutter(10 SECONDS) adds ten seconds of stuttering.
+// - Ex: adjust_jitter(-5 SECONDS) removes five seconds of jittering, or just removes jittering if less than five seconds exist.
+//
+// adjust_x_up_to:
+// - Will only add (or remove) duration of a status effect up to the second parameter
+// - If the duration will result in going beyond the second parameter, it will stop exactly at that parameter
+// - The second parameter cannot be negative.
+// - Ex: adjust_stutter_up_to(20 SECONDS, 10 SECONDS) adds ten seconds of stuttering.
+//
+// set_x:
+// - Set the duration of a status effect to the exact number.
+// - Setting duration to zero seconds is effectively the same as just using remove_status_effect, or qdelling the effect.
+// - Ex: set_stutter(10 SECONDS) sets the stuttering to ten seconds, regardless of whether they had more or less existing stutter.
+//
+// set_x_if_lower:
+// - Will only set the duration of that effect IF any existing duration is lower than what was passed.
+// - Ex: set_stutter_if_lower(10 SECONDS) will set stuttering to ten seconds if no stuttering or less than ten seconds of stuttering exists
+// - Ex: set_jitter_if_lower(20 SECONDS) will do nothing if more than twenty seconds of jittering already exists
+
+// #define adjust_stutter(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter)
+// #define adjust_stutter_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter, up_to)
+// #define set_stutter(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter)
+// #define set_stutter_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter, TRUE)
+
+// #define adjust_derpspeech(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech)
+// #define adjust_derpspeech_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech, up_to)
+// #define set_derpspeech(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech)
+// #define set_derpspeech_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/stutter/derpspeech, TRUE)
+
+// #define adjust_slurring(duration) adjust_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic)
+// #define adjust_slurring_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic, up_to)
+// #define set_slurring(duration) set_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic)
+// #define set_slurring_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/speech/slurring/generic, TRUE)
+
+// #define adjust_dizzy(duration) adjust_timed_status_effect(duration, /datum/status_effect/dizziness)
+// #define adjust_dizzy_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/dizziness, up_to)
+// #define set_dizzy(duration) set_timed_status_effect(duration, /datum/status_effect/dizziness)
+// #define set_dizzy_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/dizziness, TRUE)
+
+// #define adjust_staggered(duration) adjust_timed_status_effect(duration, /datum/status_effect/staggered)
+// #define adjust_staggered_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/staggered, up_to)
+// #define set_staggered(duration) set_timed_status_effect(duration, /datum/status_effect/staggered)
+// #define set_staggered_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/staggered, TRUE)
+
+// #define adjust_jitter(duration) adjust_timed_status_effect(duration, /datum/status_effect/jitter)
+// #define adjust_jitter_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/jitter, up_to)
+// #define set_jitter(duration) set_timed_status_effect(duration, /datum/status_effect/jitter)
+// #define set_jitter_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/jitter, TRUE)
+
+// #define adjust_confusion(duration) adjust_timed_status_effect(duration, /datum/status_effect/confusion)
+// #define adjust_confusion_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/confusion, up_to)
+// #define set_confusion(duration) set_timed_status_effect(duration, /datum/status_effect/confusion)
+// #define set_confusion_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/confusion, TRUE)
+
+// #define adjust_drugginess(duration) adjust_timed_status_effect(duration, /datum/status_effect/drugginess)
+// #define adjust_drugginess_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/drugginess, up_to)
+// #define set_drugginess(duration) set_timed_status_effect(duration, /datum/status_effect/drugginess)
+// #define set_drugginess_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/drugginess, TRUE)
+
+// #define adjust_silence(duration) adjust_timed_status_effect(duration, /datum/status_effect/silenced)
+// #define adjust_silence_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/silenced, up_to)
+// #define set_silence(duration) set_timed_status_effect(duration, /datum/status_effect/silenced)
+// #define set_silence_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/silenced, TRUE)
+
+// #define adjust_hallucinations(duration) adjust_timed_status_effect(duration, /datum/status_effect/hallucination)
+// #define adjust_hallucinations_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/hallucination, up_to)
+// #define set_hallucinations(duration) set_timed_status_effect(duration, /datum/status_effect/hallucination)
+// #define set_hallucinations_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/hallucination, TRUE)
+
+// #define adjust_drowsiness(duration) adjust_timed_status_effect(duration, /datum/status_effect/drowsiness)
+// #define adjust_drowsiness_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/drowsiness, up_to)
+// #define set_drowsiness(duration) set_timed_status_effect(duration, /datum/status_effect/drowsiness)
+// #define set_drowsiness_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/drowsiness, TRUE)
+
+// #define adjust_pacifism(duration) adjust_timed_status_effect(duration, /datum/status_effect/pacify)
+// #define set_pacifism(duration) set_timed_status_effect(duration, /datum/status_effect/pacify)
+
+// #define adjust_eye_blur(duration) adjust_timed_status_effect(duration, /datum/status_effect/eye_blur)
+// #define adjust_eye_blur_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/eye_blur, up_to)
+// #define set_eye_blur(duration) set_timed_status_effect(duration, /datum/status_effect/eye_blur)
+// #define set_eye_blur_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/eye_blur, TRUE)
+
+// #define adjust_temp_blindness(duration) adjust_timed_status_effect(duration, /datum/status_effect/temporary_blindness)
+// #define adjust_temp_blindness_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/temporary_blindness, up_to)
+// #define set_temp_blindness(duration) set_timed_status_effect(duration, /datum/status_effect/temporary_blindness)
+// #define set_temp_blindness_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/temporary_blindness, TRUE)
+
+// #define adjust_static_vision(duration) adjust_timed_status_effect(duration, /datum/status_effect/static_vision)
+// #define adjust_static_vision_up_to(duration, up_to) adjust_timed_status_effect(duration, /datum/status_effect/static_vision, up_to)
+// #define set_static_vision(duration) set_timed_status_effect(duration, /datum/status_effect/static_vision)
+// #define set_static_vision_if_lower(duration) set_timed_status_effect(duration, /datum/status_effect/static_vision, TRUE)
diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm
index b018d1cccd..d5ad6ab640 100644
--- a/code/__defines/subsystems.dm
+++ b/code/__defines/subsystems.dm
@@ -136,6 +136,7 @@
#define FIRE_PRIORITY_DEFAULT 50
#define FIRE_PRIORITY_TICKER 60
#define FIRE_PRIORITY_PLANETS 75
+#define FIRE_PRIORITY_PRIORITY_EFFECTS 90
#define FIRE_PRIORITY_EXPLOSIONS 90
#define FIRE_PRIORITY_MACHINES 100
#define FIRE_PRIORITY_MOBS 100
diff --git a/code/__defines/traits/declarations.dm b/code/__defines/traits/declarations.dm
index f95a6a5c28..d5446e206e 100644
--- a/code/__defines/traits/declarations.dm
+++ b/code/__defines/traits/declarations.dm
@@ -17,3 +17,18 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_ALT_CLICK_BLOCKER "no_alt_click"
#define TRAIT_INCAPACITATED "incapacitated"
+
+#define TRAIT_NOFIRE "nonflammable"
+#define TRAIT_NOFIRE_SPREAD "no_fire_spreading"
+/// Mobs that have this trait cannot be extinguished
+#define TRAIT_NO_EXTINGUISH "no_extinguish"
+/// Tells us that the mob urrently has the fire_handler/wet_stacks status effect
+#define TRAIT_IS_WET "is_wet"
+/// Mobs with this trait stay wet for longer and resist fire decaying wetness
+#define TRAIT_WET_FOR_LONGER "wet_for_longer"
+/// Mobs with this trait will be immune to slipping while also being slippery themselves when lying on the floor
+#define TRAIT_SLIPPERY_WHEN_WET "slippery_when_wet"
+/// Stops the mob from slipping on water, or banana peels, or pretty much anything that doesn't have [GALOSHES_DONT_HELP] set
+#define TRAIT_NO_SLIP_WATER "noslip_water"
+/// Owner will ignore any fire protection when calculating fire damage
+#define TRAIT_IGNORE_FIRE_PROTECTION "ignore_fire_protection"
diff --git a/code/__defines/traits/macros.dm b/code/__defines/traits/macros.dm
index d180b24442..6a0f9c8cc9 100644
--- a/code/__defines/traits/macros.dm
+++ b/code/__defines/traits/macros.dm
@@ -1,2 +1,5 @@
/// Trait applied by element
#define ELEMENT_TRAIT(source) "element_trait_[source]"
+
+/// A trait given by a specific status effect (not sure why we need both but whatever!)
+#define TRAIT_STATUS_EFFECT(effect_id) "[effect_id]-trait"
diff --git a/code/_global_vars/traits/_traits.dm b/code/_global_vars/traits/_traits.dm
index 8b98c1c686..85cb840de3 100644
--- a/code/_global_vars/traits/_traits.dm
+++ b/code/_global_vars/traits/_traits.dm
@@ -22,6 +22,9 @@ GLOBAL_LIST_INIT(traits_by_type, list(
),
/mob = list(
"TRAIT_THINKING_IN_CHARACTER" = TRAIT_THINKING_IN_CHARACTER,
+ "TRAIT_NOFIRE" = TRAIT_NOFIRE,
+ "TRAIT_NOFIRE_SPREAD" = TRAIT_NOFIRE_SPREAD,
+ "TRAIT_NO_EXTINGUISH" = TRAIT_NO_EXTINGUISH,
),
/obj = list(
"TRAIT_CLIMBABLE" = TRAIT_CLIMBABLE,
diff --git a/code/controllers/subsystems/processing/priority_effects.dm b/code/controllers/subsystems/processing/priority_effects.dm
new file mode 100644
index 0000000000..ee4fb7f7f6
--- /dev/null
+++ b/code/controllers/subsystems/processing/priority_effects.dm
@@ -0,0 +1,6 @@
+PROCESSING_SUBSYSTEM_DEF(priority_effects)
+ name = "Priority Status Effects"
+ flags = SS_TICKER | SS_KEEP_TIMING | SS_NO_INIT
+ wait = 2 // Not seconds - we're running on SS_TICKER, so this is ticks.
+ priority = FIRE_PRIORITY_PRIORITY_EFFECTS
+ stat_tag = "PEFF"
diff --git a/code/datums/diseases/advance/symptoms/fire.dm b/code/datums/diseases/advance/symptoms/fire.dm
index 037e6cdec1..e3afb0ea99 100644
--- a/code/datums/diseases/advance/symptoms/fire.dm
+++ b/code/datums/diseases/advance/symptoms/fire.dm
@@ -64,12 +64,12 @@ Bonus
to_chat(M, span_warning(pick("You feel hot.", "You hear a crackling noise.", "You smell smoke.")))
if(4)
Firestacks_stage_4(M, A)
- M.IgniteMob()
+ M.ignite_mob()
to_chat(M, span_userdanger("Your skin bursts into flames!"))
M.emote("scream")
if(5)
Firestacks_stage_5(M, A)
- M.IgniteMob()
+ M.ignite_mob()
if(M.stat != DEAD)
to_chat(M, span_userdanger("Your skin erupts into an inferno!"))
M.emote("scream")
diff --git a/code/datums/status_effects/_status_effect.dm b/code/datums/status_effects/_status_effect.dm
new file mode 100644
index 0000000000..2ac0e9fe9b
--- /dev/null
+++ b/code/datums/status_effects/_status_effect.dm
@@ -0,0 +1,254 @@
+/// Status effects are used to apply temporary or permanent effects to mobs.
+/// This file contains their code, plus code for applying and removing them.
+/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.
+ 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.
+ 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 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.
+ var/status_type = STATUS_EFFECT_UNIQUE
+ /// If TRUE, we call [proc/on_remove] when owner is deleted. Otherwise, we call [proc/be_replaced].
+ var/on_remove_on_mob_delete = FALSE
+ /// The typepath to the alert thrown by the status effect when created.
+ /// Status effect "name"s and "description"s are shown to the owner here.
+ var/alert_type = /obj/screen/alert/status_effect
+ /// The alert itself, created in [proc/on_creation] (if alert_type is specified).
+ VAR_FINAL/obj/screen/alert/status_effect/linked_alert
+ /// If TRUE, and we have an alert, we will show a duration on the alert
+ var/show_duration = FALSE
+ /// Used to define if the status effect should be using SSfastprocess or SSprocessing
+ var/processing_speed = STATUS_EFFECT_FAST_PROCESS
+ /// Do we self-terminate when a fullheal is called?
+ var/remove_on_fullheal = FALSE
+ /// If remove_on_fullheal is TRUE, what flag do we need to be removed?
+ // var/heal_flag_necessary = HEAL_STATUS
+ /// A particle effect, for things like embers - Should be set on update_particles()
+ VAR_FINAL/obj/effect/abstract/particle_holder/particle_effect
+
+/datum/status_effect/New(list/arguments)
+ on_creation(arglist(arguments))
+
+/// Called from New() with any supplied status effect arguments.
+/// Not guaranteed to exist by the end.
+/// Returning FALSE from on_apply will stop on_creation and self-delete the effect.
+/datum/status_effect/proc/on_creation(mob/living/new_owner, ...)
+ if(new_owner)
+ owner = new_owner
+ if(QDELETED(owner) || !on_apply())
+ qdel(src)
+ return
+ if(owner)
+ LAZYADD(owner.status_effects, src)
+ RegisterSignal(owner, COMSIG_LIVING_AHEAL, PROC_REF(remove_effect_on_heal))
+
+ if(duration == INFINITY)
+ // 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
+
+ if(alert_type)
+ var/obj/screen/alert/status_effect/new_alert = owner.throw_alert(id, alert_type)
+ new_alert.attached_effect = src //so the alert can reference us, if it needs to
+ 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
+ switch(processing_speed)
+ if(STATUS_EFFECT_FAST_PROCESS)
+ START_PROCESSING(SSfastprocess, src)
+ if(STATUS_EFFECT_NORMAL_PROCESS)
+ START_PROCESSING(SSprocessing, src)
+ if(STATUS_EFFECT_PRIORITY)
+ START_PROCESSING(SSpriority_effects, src)
+
+ update_particles()
+ SEND_SIGNAL(owner, COMSIG_LIVING_STATUS_APPLIED, src)
+ return TRUE
+
+/datum/status_effect/Destroy()
+ switch(processing_speed)
+ if(STATUS_EFFECT_FAST_PROCESS)
+ STOP_PROCESSING(SSfastprocess, src)
+ if(STATUS_EFFECT_NORMAL_PROCESS)
+ STOP_PROCESSING(SSprocessing, src)
+ if(STATUS_EFFECT_PRIORITY)
+ STOP_PROCESSING(SSpriority_effects, src)
+ if(owner)
+ linked_alert = null
+ owner.clear_alert(id)
+ LAZYREMOVE(owner.status_effects, src)
+ on_remove()
+ UnregisterSignal(owner, COMSIG_LIVING_AHEAL)
+ SEND_SIGNAL(owner, COMSIG_LIVING_STATUS_REMOVED, src)
+ owner = null
+ if(particle_effect)
+ QDEL_NULL(particle_effect)
+ return ..()
+
+/// Updates the status effect alert's maptext (if possible)
+/datum/status_effect/proc/update_shown_duration()
+ PRIVATE_PROC(TRUE)
+ if(!linked_alert || !show_duration)
+ return
+
+ linked_alert.maptext = MAPTEXT("[round((duration - world.time)/10, 1)]s")
+
+// Status effect process. Handles adjusting its duration and ticks.
+// If you're adding processed effects, put them in [proc/tick]
+// instead of extending / overriding the process() proc.
+/datum/status_effect/process(seconds_per_tick)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ if(QDELETED(owner))
+ qdel(src)
+ return
+
+ 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
+
+ if(QDELING(src))
+ // tick deleted us, no need to continue
+ return
+
+ if(duration != STATUS_EFFECT_PERMANENT)
+ if(duration < world.time)
+ qdel(src)
+ return
+ update_shown_duration()
+
+/// Called whenever the effect is applied in on_created
+/// Returning FALSE will cause it to delete itself during creation instead.
+/datum/status_effect/proc/on_apply()
+ return TRUE
+
+/// Gets and formats examine text associated with our status effect.
+/// Return 'null' to have no examine text appear (default behavior).
+/datum/status_effect/proc/get_examine_text()
+ return null
+
+/**
+ * Called every tick from process().
+ * This is only called of tick_interval is not -1.
+ *
+ * Note that every tick =/= every processing cycle.
+ *
+ * * seconds_between_ticks = This is how many SECONDS that elapse between ticks.
+ * This is a constant value based upon the initial tick interval set on the status effect.
+ * It is similar to seconds_per_tick, from processing itself, but adjusted to the status effect's tick interval.
+ */
+/datum/status_effect/proc/tick(seconds_between_ticks)
+ return
+
+/// Called whenever the buff expires or is removed (qdeleted)
+/// Note that at the point this is called, it is out of the
+/// owner's status_effects list, but owner is not yet null
+/datum/status_effect/proc/on_remove()
+ return
+
+/// Called instead of on_remove when a status effect
+/// of status_type STATUS_EFFECT_REPLACE is replaced by itself,
+/// or when a status effect with on_remove_on_mob_delete
+/// set to FALSE has its mob deleted
+/datum/status_effect/proc/be_replaced()
+ linked_alert = null
+ owner.clear_alert(id)
+ LAZYREMOVE(owner.status_effects, src)
+ owner = null
+ qdel(src)
+
+/// Called before being fully removed (before on_remove)
+/// Returning FALSE will cancel removal
+/datum/status_effect/proc/before_remove()
+ return TRUE
+
+/// 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
+
+/// Adds nextmove modifier multiplicatively to the owner while applied
+/datum/status_effect/proc/nextmove_modifier()
+ return 1
+
+/// Adds nextmove adjustment additiviely to the owner while applied
+/datum/status_effect/proc/nextmove_adjust()
+ return 0
+
+/// Signal proc for [COMSIG_LIVING_POST_FULLY_HEAL] to remove us on fullheal
+/datum/status_effect/proc/remove_effect_on_heal(datum/source) //, heal_flags)
+ SIGNAL_HANDLER
+
+ if(!remove_on_fullheal)
+ return
+
+ // 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.
+/datum/status_effect/proc/remove_duration(seconds)
+ if(duration == STATUS_EFFECT_PERMANENT) // Infinite duration
+ return FALSE
+
+ duration -= seconds
+ if(duration <= world.time)
+ qdel(src)
+ return TRUE
+
+ update_shown_duration()
+ return FALSE
+
+/**
+ * Updates the particles for the status effects
+ * Should be handled by subtypes!
+ */
+/datum/status_effect/proc/update_particles()
+ SHOULD_CALL_PARENT(FALSE)
+ return
+
+/datum/status_effect/vv_edit_var(var_name, var_value)
+ . = ..()
+ if(!.)
+ return
+ if(var_name == NAMEOF(src, duration))
+ if(var_value == INFINITY)
+ duration = STATUS_EFFECT_PERMANENT
+ update_shown_duration()
+
+ if(var_name == NAMEOF(src, show_duration))
+ update_shown_duration()
+
+/// Alert base type for status effect alerts
+/obj/screen/alert/status_effect
+ name = "Curse of Mundanity"
+ desc = "You don't feel any different..."
+ // maptext_y = 2
+ /// The status effect we're linked to
+ var/datum/status_effect/attached_effect
+
+/obj/screen/alert/status_effect/Destroy()
+ attached_effect = null //Don't keep a ref now
+ return ..()
diff --git a/code/datums/status_effects/_status_effect_helpers.dm b/code/datums/status_effects/_status_effect_helpers.dm
new file mode 100644
index 0000000000..a5743d2e93
--- /dev/null
+++ b/code/datums/status_effects/_status_effect_helpers.dm
@@ -0,0 +1,147 @@
+
+// Status effect helpers for living mobs
+
+/**
+ * Applies a given status effect to this mob.
+ *
+ * new_effect - TYPEPATH of a status effect to apply.
+ * Additional status effect arguments can be passed.
+ *
+ * Returns the instance of the created effected, if successful.
+ * Returns 'null' if unsuccessful.
+ */
+/mob/living/proc/apply_status_effect(datum/status_effect/new_effect, ...)
+ RETURN_TYPE(/datum/status_effect)
+
+ // The arguments we pass to the start effect. The 1st argument is this mob.
+ var/list/arguments = args.Copy()
+ arguments[1] = src
+
+ // If the status effect we're applying doesn't allow multiple effects, we need to handle it
+ if(initial(new_effect.status_type) != STATUS_EFFECT_MULTIPLE)
+ for(var/datum/status_effect/existing_effect as anything in status_effects)
+ if(existing_effect.id != initial(new_effect.id))
+ continue
+
+ switch(existing_effect.status_type)
+ // Multiple are allowed, continue as normal. (Not normally reachable)
+ if(STATUS_EFFECT_MULTIPLE)
+ break
+ // Only one is allowed of this type - early return
+ if(STATUS_EFFECT_UNIQUE)
+ return
+ // Replace the existing instance (deletes it).
+ if(STATUS_EFFECT_REPLACE)
+ existing_effect.be_replaced()
+ // Refresh the existing type, then early return
+ if(STATUS_EFFECT_REFRESH)
+ existing_effect.refresh(arglist(arguments))
+ return
+
+ // Create the status effect with our mob + our arguments
+ var/datum/status_effect/new_instance = new new_effect(arguments)
+ if(!QDELETED(new_instance))
+ return new_instance
+
+/**
+ * Removes all instances of a given status effect from this mob
+ *
+ * removed_effect - TYPEPATH of a status effect to remove.
+ * Additional status effect arguments can be passed - these are passed into before_remove.
+ *
+ * Returns TRUE if at least one was removed.
+ */
+/mob/living/proc/remove_status_effect(datum/status_effect/removed_effect, ...)
+ var/list/arguments = args.Copy(2)
+
+ . = FALSE
+ for(var/datum/status_effect/existing_effect as anything in status_effects)
+ if(existing_effect.id == initial(removed_effect.id) && existing_effect.before_remove(arglist(arguments)))
+ qdel(existing_effect)
+ . = TRUE
+
+ return .
+
+/**
+ * Checks if this mob has a status effect that shares the passed effect's ID
+ *
+ * checked_effect - TYPEPATH of a status effect to check for. Checks for its ID, not its typepath
+ *
+ * Returns an instance of a status effect, or NULL if none were found.
+ */
+/mob/proc/has_status_effect(datum/status_effect/checked_effect)
+ // Yes I'm being cringe and putting this on the mob level even though status effects only apply to the living level
+ // There's quite a few places (namely examine and, bleh, cult code) where it's easier to not need to cast to living before checking
+ // for an effect such as blindness
+ return null
+
+/mob/living/has_status_effect(datum/status_effect/checked_effect)
+ RETURN_TYPE(/datum/status_effect)
+
+ for(var/datum/status_effect/present_effect as anything in status_effects)
+ if(present_effect.id == initial(checked_effect.id))
+ return present_effect
+
+ return null
+
+///Gets every status effect of an ID and returns all of them in a list, rather than the individual 'has_status_effect'
+/mob/living/proc/get_all_status_effect_of_id(datum/status_effect/checked_effect)
+ RETURN_TYPE(/list/datum/status_effect)
+
+ var/list/all_effects_of_type = list()
+ for(var/datum/status_effect/present_effect as anything in status_effects)
+ if(present_effect.id == initial(checked_effect.id))
+ all_effects_of_type += present_effect
+
+ return all_effects_of_type
+
+/**
+ * Checks if this mob has a status effect that shares the passed effect's ID
+ * and has the passed sources are in its list of sources (ONLY works for grouped efects!)
+ *
+ * checked_effect - TYPEPATH of a status effect to check for. Checks for its ID, not its typepath
+ *
+ * Returns an instance of a status effect, or NULL if none were found.
+ */
+/mob/proc/has_status_effect_from_source(datum/status_effect/grouped/checked_effect, sources)
+ // See [/mob/proc/has_status_effect] for reason behind having this on the mob level
+ return null
+
+/mob/living/has_status_effect_from_source(datum/status_effect/grouped/checked_effect, sources)
+ RETURN_TYPE(/datum/status_effect)
+
+ if(!ispath(checked_effect))
+ CRASH("has_status_effect_from_source passed with an improper status effect path.")
+
+ if(!islist(sources))
+ sources = list(sources)
+
+ for(var/datum/status_effect/grouped/present_effect in status_effects)
+ if(present_effect.id != initial(checked_effect.id))
+ continue
+ var/list/matching_sources = present_effect.sources & sources
+ if(length(matching_sources))
+ return present_effect
+
+ return null
+
+/**
+ * Returns a list of all status effects that share the passed effect type's ID
+ *
+ * checked_effect - TYPEPATH of a status effect to check for. Checks for its ID, not its typepath
+ *
+ * Returns a list
+ */
+/mob/proc/has_status_effect_list(datum/status_effect/checked_effect)
+ // See [/mob/proc/has_status_effect] for reason behind having this on the mob level
+ return null
+
+/mob/living/has_status_effect_list(datum/status_effect/checked_effect)
+ RETURN_TYPE(/list)
+
+ var/list/effects_found = list()
+ for(var/datum/status_effect/present_effect as anything in status_effects)
+ if(present_effect.id == initial(checked_effect.id))
+ effects_found += present_effect
+
+ return effects_found
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
new file mode 100644
index 0000000000..191e3aba52
--- /dev/null
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -0,0 +1,388 @@
+/////////// BUBBER EDIT THIS WILL BE FORCED TO CONFLICT READ THIS
+/*
+ RESET THIS FILE BACK TO TG ONCE YOU GET FISH INFUSION
+ RESET THIS FILE BACK TO TG ONCE YOU GET FISH INFUSION
+ RESET THIS FILE BACK TO TG ONCE YOU GET FISH INFUSION
+ RESET THIS FILE BACK TO TG ONCE YOU GET FISH INFUSION
+
+*/
+/datum/status_effect/fire_handler
+ duration = STATUS_EFFECT_PERMANENT
+ id = STATUS_EFFECT_ID_ABSTRACT
+ alert_type = null
+ status_type = STATUS_EFFECT_REFRESH //Custom code
+ on_remove_on_mob_delete = TRUE
+ tick_interval = 2 SECONDS
+ processing_speed = STATUS_EFFECT_PRIORITY
+ /// Current amount of stacks we have
+ var/stacks
+ /// Maximum of stacks that we could possibly get
+ var/stack_limit = MAX_FIRE_STACKS
+ /// What status effect types do we remove uppon being applied. These are just deleted without any deduction from our or their stacks when forced.
+ var/list/enemy_types
+ /// What status effect types do we merge into if they exist. Ignored when forced.
+ var/list/merge_types
+ /// What status effect types do we override if they exist. These are simply deleted when forced.
+ var/list/override_types
+ /// For how much firestacks does one our stack count
+ var/stack_modifier = 1
+
+/datum/status_effect/fire_handler/refresh(mob/living/new_owner, new_stacks, forced = FALSE)
+ if(forced)
+ set_stacks(new_stacks)
+ else
+ adjust_stacks(new_stacks)
+
+/datum/status_effect/fire_handler/on_creation(mob/living/new_owner, new_stacks, forced = FALSE)
+ . = ..()
+
+ if(isanimal(owner))
+ qdel(src)
+ return
+ // if(isbasicmob(owner))
+ // if(!check_basic_mob_immunity(owner))
+ // qdel(src)
+ // return
+
+ owner = new_owner
+ set_stacks(new_stacks)
+
+ for(var/enemy_type in enemy_types)
+ var/datum/status_effect/fire_handler/enemy_effect = owner.has_status_effect(enemy_type)
+ if(enemy_effect)
+ if(forced)
+ qdel(enemy_effect)
+ continue
+
+ var/cur_stacks = stacks
+ adjust_stacks(-abs(enemy_effect.stacks * enemy_effect.stack_modifier / stack_modifier))
+ enemy_effect.adjust_stacks(-abs(cur_stacks * stack_modifier / enemy_effect.stack_modifier))
+ if(enemy_effect.stacks <= 0)
+ qdel(enemy_effect)
+
+ if(stacks <= 0)
+ qdel(src)
+ return
+
+ if(!forced)
+ var/list/merge_effects = list()
+ for(var/merge_type in merge_types)
+ var/datum/status_effect/fire_handler/merge_effect = owner.has_status_effect(merge_type)
+ if(merge_effect)
+ merge_effects += merge_effects
+
+ if(LAZYLEN(merge_effects))
+ for(var/datum/status_effect/fire_handler/merge_effect in merge_effects)
+ merge_effect.adjust_stacks(stacks * stack_modifier / merge_effect.stack_modifier / LAZYLEN(merge_effects))
+ qdel(src)
+ return
+
+ for(var/override_type in override_types)
+ var/datum/status_effect/fire_handler/override_effect = owner.has_status_effect(override_type)
+ if(override_effect)
+ if(forced)
+ qdel(override_effect)
+ continue
+
+ adjust_stacks(override_effect.stacks)
+ qdel(override_effect)
+
+/**
+ * Setter and adjuster procs for firestacks
+ *
+ * Arguments:
+ * - new_stacks
+ *
+ */
+
+/datum/status_effect/fire_handler/proc/set_stacks(new_stacks)
+ stacks = max(0, min(stack_limit, new_stacks))
+ cache_stacks()
+
+/datum/status_effect/fire_handler/proc/adjust_stacks(new_stacks)
+ stacks = max(0, min(stack_limit, stacks + new_stacks))
+ cache_stacks()
+
+/// Checks if the applicable basic mob is immune to the status effect we're trying to apply. Returns TRUE if it is, FALSE if it isn't.
+// /datum/status_effect/fire_handler/proc/check_basic_mob_immunity(mob/living/basic/basic_owner)
+// return (basic_owner.basic_mob_flags & FLAMMABLE_MOB)
+
+/**
+ * Refresher for mob's fire_stacks
+ */
+
+/datum/status_effect/fire_handler/proc/cache_stacks()
+ owner.fire_stacks = 0
+ var/was_on_fire = owner.on_fire
+ owner.on_fire = FALSE
+ for(var/datum/status_effect/fire_handler/possible_fire in owner.status_effects)
+ owner.fire_stacks += possible_fire.stacks * possible_fire.stack_modifier
+
+ if(!istype(possible_fire, /datum/status_effect/fire_handler/fire_stacks))
+ continue
+
+ var/datum/status_effect/fire_handler/fire_stacks/our_fire = possible_fire
+ if(our_fire.on_fire)
+ owner.on_fire = TRUE
+
+ if(was_on_fire && !owner.on_fire)
+ owner.clear_alert(ALERT_FIRE)
+ else if(!was_on_fire && owner.on_fire)
+ owner.throw_alert(ALERT_FIRE, /obj/screen/alert/fire)
+ // owner.update_appearance(UPDATE_OVERLAYS)
+ owner.update_fire()
+ update_particles()
+
+/datum/status_effect/fire_handler/fire_stacks
+ id = "fire_stacks" //fire_stacks and wet_stacks should have different IDs or else has_status_effect won't work
+ remove_on_fullheal = TRUE
+
+ enemy_types = list(/datum/status_effect/fire_handler/wet_stacks)
+ stack_modifier = 1
+
+ /// If we're on fire
+ var/on_fire = FALSE
+ /// Reference to the mob light emitter itself
+ var/obj/effect/dummy/lighting_obj/moblight
+ /// Type of mob light emitter we use when on fire
+ var/moblight_type = /obj/effect/dummy/lighting_obj/moblight/fire
+ /// Cached particle type
+ var/cached_state
+
+/datum/status_effect/fire_handler/fire_stacks/get_examine_text()
+ if(owner.on_fire)
+ return
+
+ var/datum/gender/T = GLOB.gender_datums[owner.get_visible_gender()]
+ return "[T.He] [T.is] covered in something flammable."
+
+// /datum/status_effect/fire_handler/fire_stacks/proc/owner_touched_sparks()
+// SIGNAL_HANDLER
+
+// ignite()
+
+/datum/status_effect/fire_handler/fire_stacks/on_creation(mob/living/new_owner, new_stacks, forced = FALSE)
+ . = ..()
+ // RegisterSignal(owner, COMSIG_ATOM_TOUCHED_SPARKS, PROC_REF(owner_touched_sparks))
+
+/datum/status_effect/fire_handler/fire_stacks/on_remove()
+ // UnregisterSignal(owner, COMSIG_ATOM_TOUCHED_SPARKS)
+ if (cached_state)
+ owner.remove_shared_particles(cached_state)
+
+/datum/status_effect/fire_handler/fire_stacks/tick(seconds_between_ticks)
+ if(stacks <= 0)
+ qdel(src)
+ return TRUE
+
+ if(!on_fire)
+ return TRUE
+
+ var/decay_multiplier = 1 // HAS_TRAIT(owner, TRAIT_HUSK) ? 2 : 1 // husks decay twice as fast
+ adjust_stacks(owner.fire_stack_decay_rate * decay_multiplier * seconds_between_ticks)
+
+ if(stacks <= 0)
+ qdel(src)
+ return TRUE
+
+ var/datum/gas_mixture/air = owner.loc.return_air()
+ if(air.gas[GAS_O2] < 1)
+ qdel(src)
+ return TRUE
+
+ deal_damage(seconds_between_ticks)
+
+/datum/status_effect/fire_handler/fire_stacks/update_particles()
+ if (!on_fire)
+ if (cached_state)
+ owner.remove_shared_particles(cached_state)
+ cached_state = null
+ return
+
+ var/particle_type = /particles/embers/minor
+ if(stacks > MOB_BIG_FIRE_STACK_THRESHOLD)
+ particle_type = /particles/embers
+
+ if (cached_state == particle_type)
+ return
+
+ if (cached_state)
+ owner.remove_shared_particles(cached_state)
+ owner.add_shared_particles(particle_type)
+ cached_state = particle_type
+
+/**
+ * Proc that handles damage dealing and all special effects
+ *
+ * Arguments:
+ * - seconds_between_ticks
+ *
+ */
+
+/datum/status_effect/fire_handler/fire_stacks/proc/deal_damage(seconds_per_tick)
+ owner.on_fire_stack(seconds_per_tick, src)
+
+ var/turf/location = get_turf(owner)
+ location.hotspot_expose(700, 25 * seconds_per_tick, TRUE)
+
+/**
+ * Used to deal damage to humans and count their protection.
+ *
+ * Arguments:
+ * - seconds_between_ticks
+ * - no_protection: When set to TRUE, fire will ignore any possible fire protection
+ *
+ */
+
+/datum/status_effect/fire_handler/fire_stacks/proc/harm_human(seconds_per_tick, no_protection = FALSE)
+ var/mob/living/carbon/human/victim = owner
+ var/thermal_protection = victim.get_heat_protection(stacks)
+
+ if(!no_protection)
+ if(thermal_protection == 1) // IMMUNE
+ return
+
+ var/fire_temp_add = (BODYTEMP_HEATING_MAX + (stacks + 15)) * (1 - thermal_protection)
+ victim.bodytemperature += fire_temp_add
+
+ // var/mob/living/carbon/human/victim = owner
+ // var/thermal_protection = victim.get_heat_protection(stacks)
+
+ // if(!no_protection)
+ // if(thermal_protection >= FIRE_IMMUNITY_MAX_TEMP_PROTECT)
+ // return
+ // if(thermal_protection >= FIRE_SUIT_MAX_TEMP_PROTECT)
+ // victim.adjust_bodytemperature(5.5 * seconds_per_tick)
+ // return
+
+ // var/amount_to_heat = (BODYTEMP_HEATING_MAX + (stacks * 12)) * 0.5 * seconds_per_tick
+ // if(owner.bodytemperature > BODYTEMP_FIRE_TEMP_SOFTCAP)
+ // // Apply dimishing returns upon temp beyond the soft cap
+ // amount_to_heat = amount_to_heat ** (BODYTEMP_FIRE_TEMP_SOFTCAP / owner.bodytemperature)
+
+ // victim.adjust_bodytemperature(amount_to_heat)
+ // if (!(HAS_TRAIT(victim, TRAIT_RESISTHEAT)))
+ // victim.add_mood_event("on_fire", /datum/mood_event/on_fire)
+ // victim.add_mob_memory(/datum/memory/was_burning)
+
+/**
+ * Handles mob ignition, should be the only way to set on_fire to TRUE
+ *
+ * Arguments:
+ * - silent: When set to TRUE, no message is displayed
+ *
+ */
+
+/datum/status_effect/fire_handler/fire_stacks/proc/ignite(silent = FALSE)
+ if(HAS_TRAIT(owner, TRAIT_NOFIRE))
+ return FALSE
+
+ on_fire = TRUE
+ if(!silent)
+ owner.visible_message(span_warning("[owner] catches fire!"), span_userdanger("You're set on fire!"))
+
+ if(moblight_type)
+ if(moblight)
+ qdel(moblight)
+ moblight = new moblight_type(owner)
+
+ cache_stacks()
+ SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
+ return TRUE
+
+/**
+ * Handles mob extinguishing, should be the only way to set on_fire to FALSE
+ */
+
+/datum/status_effect/fire_handler/fire_stacks/proc/extinguish()
+ QDEL_NULL(moblight)
+ on_fire = FALSE
+ // owner.clear_mood_event("on_fire")
+ SEND_SIGNAL(owner, COMSIG_LIVING_EXTINGUISHED, owner)
+ cache_stacks()
+ for(var/obj/item/equipped in (owner.get_equipped_items()))
+ equipped.extinguish()
+
+/datum/status_effect/fire_handler/fire_stacks/on_remove()
+ if(on_fire)
+ extinguish()
+ set_stacks(0)
+ owner.update_fire()
+ // UnregisterSignal(owner, COMSIG_MOB_UPDATE_ICONS)
+ // owner.update_appearance(UPDATE_OVERLAYS)
+ return ..()
+
+/datum/status_effect/fire_handler/fire_stacks/on_apply()
+ . = ..()
+ RegisterSignal(owner, COMSIG_ATOM_EXTINGUISH, PROC_REF(extinguish))
+ owner.update_fire()
+ // add_fire_overlay(owner)
+ // owner.update_appearance(UPDATE_OVERLAYS)
+
+/datum/status_effect/fire_handler/fire_stacks/proc/add_fire_overlay(mob/living/source)
+ SIGNAL_HANDLER
+
+ if(stacks <= 0 || !on_fire)
+ return
+
+ var/mutable_appearance/created_overlay = owner.get_fire_overlay(stacks, on_fire)
+ if(isnull(created_overlay))
+ return
+
+ source.overlays |= created_overlay
+
+
+// WET
+/datum/status_effect/fire_handler/wet_stacks
+ id = "wet_stacks"
+
+ enemy_types = list(/datum/status_effect/fire_handler/fire_stacks)
+ stack_modifier = -1
+ /// If the mob has the TRAIT_SLIPPERY_WHEN_WET trait, the mob gets this component while it's wet
+ var/datum/component/slippery/slipperiness
+
+/datum/status_effect/fire_handler/wet_stacks/on_apply()
+ . = ..()
+ RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_WET_FOR_LONGER), SIGNAL_REMOVETRAIT(TRAIT_WET_FOR_LONGER)), PROC_REF(update_wet_stack_modifier))
+ update_wet_stack_modifier()
+ RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_SLIPPERY_WHEN_WET), PROC_REF(become_slippery))
+ RegisterSignal(owner, SIGNAL_REMOVETRAIT(TRAIT_SLIPPERY_WHEN_WET), PROC_REF(no_longer_slippery))
+ if(HAS_TRAIT(owner, TRAIT_SLIPPERY_WHEN_WET))
+ become_slippery()
+ ADD_TRAIT(owner, TRAIT_IS_WET, TRAIT_STATUS_EFFECT(id))
+ owner.add_shared_particles(/particles/droplets)
+
+/datum/status_effect/fire_handler/wet_stacks/on_remove()
+ . = ..()
+ REMOVE_TRAIT(owner, TRAIT_IS_WET, TRAIT_STATUS_EFFECT(id))
+ if(HAS_TRAIT(owner, TRAIT_SLIPPERY_WHEN_WET))
+ no_longer_slippery()
+ owner.remove_shared_particles(/particles/droplets)
+
+/datum/status_effect/fire_handler/wet_stacks/proc/update_wet_stack_modifier()
+ SIGNAL_HANDLER
+ stack_modifier = HAS_TRAIT(owner, TRAIT_WET_FOR_LONGER) ? -3.5 : -1
+
+/datum/status_effect/fire_handler/wet_stacks/proc/become_slippery()
+ SIGNAL_HANDLER
+ // slipperiness = owner.AddComponent(/datum/component/slippery, 5 SECONDS, lube_flags = SLIPPERY_WHEN_LYING_DOWN|NO_SLIP_WHEN_WALKING|WEAK_SLIDE)
+ ADD_TRAIT(owner, TRAIT_NO_SLIP_WATER, TRAIT_STATUS_EFFECT(id))
+
+/datum/status_effect/fire_handler/wet_stacks/proc/no_longer_slippery()
+ SIGNAL_HANDLER
+ // QDEL_NULL(slipperiness)
+ REMOVE_TRAIT(owner, TRAIT_NO_SLIP_WATER, TRAIT_STATUS_EFFECT(id))
+
+/datum/status_effect/fire_handler/wet_stacks/get_examine_text()
+ var/datum/gender/T = GLOB.gender_datums[owner.get_visible_gender()]
+ return "[T.He] look[T.s] a little soaked."
+
+/datum/status_effect/fire_handler/wet_stacks/tick(seconds_between_ticks)
+ var/decay = HAS_TRAIT(owner, TRAIT_WET_FOR_LONGER) ? -0.035 : -0.5
+ adjust_stacks(decay * seconds_between_ticks)
+ if(stacks <= 0)
+ qdel(src)
+
+// /datum/status_effect/fire_handler/wet_stacks/check_basic_mob_immunity(mob/living/basic/basic_owner)
+// return !(basic_owner.basic_mob_flags & IMMUNE_TO_GETTING_WET)
+/// BUBBER EDIT END
diff --git a/code/datums/status_effects/grouped_effect.dm b/code/datums/status_effects/grouped_effect.dm
new file mode 100644
index 0000000000..caddfe17fc
--- /dev/null
+++ b/code/datums/status_effects/grouped_effect.dm
@@ -0,0 +1,47 @@
+/// Status effect from multiple sources, when all sources are removed, so is the effect
+/datum/status_effect/grouped
+ id = STATUS_EFFECT_ID_ABSTRACT
+ alert_type = null
+ // Grouped effects adds itself to [var/sources] and destroys itself if one exists already, there are never actually multiple
+ status_type = STATUS_EFFECT_MULTIPLE
+ /// A list of all sources applying this status effect. Sources are a list of keys
+ var/list/sources = list()
+
+/datum/status_effect/grouped/on_creation(mob/living/new_owner, source, ...)
+ //Get our supplied arguments, without new_owner
+ var/list/new_source_args = args.Copy(2)
+
+ var/datum/status_effect/grouped/existing = new_owner.has_status_effect(type)
+ if(existing)
+ existing.sources |= source
+ existing.source_added(arglist(new_source_args))
+ qdel(src)
+ return FALSE
+
+ /* We are the original */
+
+ . = ..()
+ if(.)
+ sources |= source
+ source_added(arglist(new_source_args))
+
+/**
+ * Called after a source is added to the status effect,
+ * this includes the first source added after creation.
+ */
+/datum/status_effect/grouped/proc/source_added(source, ...)
+ return
+
+/**
+ * Called after a source is removed from the status effect. \
+ * `removing` will be TRUE if this is the last source, which means
+ * the effect will be deleted.
+ */
+/datum/status_effect/grouped/proc/source_removed(source, removing)
+ return
+
+/datum/status_effect/grouped/before_remove(source)
+ sources -= source
+ var/was_last_source = !length(sources)
+ source_removed(source, was_last_source)
+ return was_last_source
diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm
index 55cdd7aab0..18898a8210 100644
--- a/code/game/atom/_atom.dm
+++ b/code/game/atom/_atom.dm
@@ -260,9 +260,23 @@
/atom/proc/emag_act(var/remaining_charges, var/mob/user, var/emag_source)
return -1
-/atom/proc/fire_act()
- return
+/**
+ * Respond to fire being used on our atom
+ *
+ * Default behaviour is to send [COMSIG_ATOM_FIRE_ACT] and return
+ */
+/atom/proc/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume)
+ SEND_SIGNAL(src, COMSIG_ATOM_FIRE_ACT, air, exposed_temperature, exposed_volume)
+ return FALSE
+/**
+ * Sends [COMSIG_ATOM_EXTINGUISH] signal, which properly removes burning component if it is present.
+ *
+ * Default behaviour is to send [COMSIG_ATOM_ACID_ACT] and return
+ */
+/atom/proc/extinguish()
+ SHOULD_CALL_PARENT(TRUE)
+ return SEND_SIGNAL(src, COMSIG_ATOM_EXTINGUISH)
// Returns an assoc list of RCD information.
// Example would be: list(RCD_VALUE_MODE = RCD_DECONSTRUCT, RCD_VALUE_DELAY = 50, RCD_VALUE_COST = RCD_SHEETS_PER_MATTER_UNIT * 4)
diff --git a/code/game/machinery/computer/arcade.dm b/code/game/machinery/computer/arcade.dm
index 6e8fe1da9b..78302073a3 100644
--- a/code/game/machinery/computer/arcade.dm
+++ b/code/game/machinery/computer/arcade.dm
@@ -417,7 +417,7 @@
if(emagged)
var/mob/living/M = user
M.adjust_fire_stacks(5)
- M.IgniteMob() //flew into a star, so you're on fire
+ M.ignite_mob() //flew into a star, so you're on fire
to_chat(user,span_danger(span_large("You feel an immense wave of heat emanate from \the [src]. Your skin bursts into flames.")))
dat += "
OK...
"
diff --git a/code/game/machinery/cryo.dm b/code/game/machinery/cryo.dm
index 9919c50420..cd89a98c9d 100644
--- a/code/game/machinery/cryo.dm
+++ b/code/game/machinery/cryo.dm
@@ -301,7 +301,7 @@
M.client.eye = src
M.stop_pulling()
M.loc = src
- M.ExtinguishMob()
+ M.extinguish_mob()
if(M.health > -100 && (M.health < 0 || M.sleeping))
to_chat(M, span_boldnotice("You feel a cold liquid surround you. Your skin starts to freeze up."))
occupant = M
diff --git a/code/game/objects/effects/effect_system.dm b/code/game/objects/effects/effect_system.dm
index b32ac7d8b7..da4223334b 100644
--- a/code/game/objects/effects/effect_system.dm
+++ b/code/game/objects/effects/effect_system.dm
@@ -318,7 +318,8 @@ steam.start() -- spawns the effect
/obj/effect/effect/smoke/elemental/fire/affect(mob/living/L)
L.inflict_heat_damage(strength)
- L.add_modifier(/datum/modifier/fire, 6 SECONDS) // Around 15 damage per stack.
+ L.adjust_fire_stacks(10)
+ L.ignite_mob()
/obj/effect/effect/smoke/elemental/frost
name = "freezing cloud"
diff --git a/code/game/objects/effects/particle_holder.dm b/code/game/objects/effects/particle_holder.dm
new file mode 100644
index 0000000000..7e9594a6b1
--- /dev/null
+++ b/code/game/objects/effects/particle_holder.dm
@@ -0,0 +1,73 @@
+///objects can only have one particle on them at a time, so we use these abstract effects to hold and display the effects. You know, so multiple particle effects can exist at once.
+///also because some objects do not display particles due to how their visuals are built
+/obj/effect/abstract/particle_holder
+ name = "particle holder"
+ desc = "How are you reading this? Please make a bug report :)"
+ appearance_flags = KEEP_APART|KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE|RESET_COLOR //movable appearance_flags plus KEEP_APART and KEEP_TOGETHER
+ vis_flags = VIS_INHERIT_PLANE
+ layer = ABOVE_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ anchored = TRUE
+ /// Holds info about how this particle emitter works
+ /// See \code\__DEFINES\particles.dm
+ var/particle_flags = NONE
+
+ var/atom/parent
+
+/obj/effect/abstract/particle_holder/Initialize(mapload, particle_path = /particles/smoke, particle_flags = NONE)
+ . = ..()
+ if(!loc)
+ stack_trace("particle holder was created with no loc!")
+ return INITIALIZE_HINT_QDEL
+
+ if(loc.plane == TURF_PLANE)
+ vis_flags &= ~VIS_INHERIT_PLANE // don't yoink the floor plane. we'll just sit on game plane, it's fine
+
+ // We nullspace ourselves because some objects use their contents (e.g. storage) and some items may drop everything in their contents on deconstruct.
+ parent = loc
+ loc = null
+
+ // Mouse opacity can get set to opaque by some objects when placed into the object's contents (storage containers).
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ src.particle_flags = particle_flags
+ particles = new particle_path()
+ // /atom doesn't have vis_contents, /turf and /atom/movable do
+ var/atom/movable/lie_about_areas = parent
+ lie_about_areas.vis_contents += src
+ RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_deleted))
+
+ if(particle_flags & PARTICLE_ATTACH_MOB)
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ on_move(parent, null, NORTH)
+
+/obj/effect/abstract/particle_holder/Destroy(force)
+ QDEL_NULL(particles)
+ parent = null
+ return ..()
+
+/// Non movables don't delete contents on destroy, so we gotta do this
+/obj/effect/abstract/particle_holder/proc/parent_deleted(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// signal called when a parent that's been hooked into this moves
+/// does a variety of checks to ensure overrides work out properly
+/obj/effect/abstract/particle_holder/proc/on_move(atom/movable/attached, atom/oldloc, direction)
+ SIGNAL_HANDLER
+
+ if(!(particle_flags & PARTICLE_ATTACH_MOB))
+ return
+
+ //remove old
+ if(ismob(oldloc))
+ var/mob/particle_mob = oldloc
+ particle_mob.vis_contents -= src
+
+ // If we're sitting in a mob, we want to emit from it too, for vibes and shit
+ if(ismob(attached.loc))
+ var/mob/particle_mob = attached.loc
+ particle_mob.vis_contents += src
+
+/// Sets the particles position to the passed coordinates
+/obj/effect/abstract/particle_holder/proc/set_particle_position(x = 0, y = 0, z = 0)
+ particles.position = list(x, y, z)
diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm
new file mode 100644
index 0000000000..481849c00e
--- /dev/null
+++ b/code/game/objects/effects/particles/fire.dm
@@ -0,0 +1,56 @@
+// Fire related particles.
+/particles/bonfire
+ icon = 'icons/effects/particles/bonfire.dmi'
+ icon_state = "bonfire"
+ width = 100
+ height = 100
+ count = 1000
+ spawning = 4
+ lifespan = 0.7 SECONDS
+ fade = 1 SECONDS
+ grow = -0.01
+ velocity = list(0, 0)
+ position = generator(GEN_CIRCLE, 0, 16, NORMAL_RAND)
+ drift = generator(GEN_VECTOR, list(0, -0.2), list(0, 0.2))
+ gravity = list(0, 0.95)
+ scale = generator(GEN_VECTOR, list(0.3, 0.3), list(1,1), NORMAL_RAND)
+ rotation = 30
+ spin = generator(GEN_NUM, -20, 20)
+
+/particles/embers
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot" = 4,"cross" = 1,"curl" = 1)
+ width = 64
+ height = 96
+ count = 500
+ spawning = 5
+ lifespan = 3 SECONDS
+ fade = 1 SECONDS
+ color = 0
+ color_change = 0.05
+ gradient = list("#FBAF4D", "#FCE6B6", "#FD481C")
+ position = generator(GEN_BOX, list(-12,-16,0), list(12,16,0), NORMAL_RAND)
+ drift = generator(GEN_VECTOR, list(-0.1,0), list(0.1,0.025), UNIFORM_RAND)
+ spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND)
+ scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND)
+
+/particles/embers/minor
+ spawning = 1
+
+/particles/embers/spark
+ count = 3
+ spawning = 2
+ gradient = list("#FBAF4D", "#FCE6B6", "#FFFFFF")
+ lifespan = 1.5 SECONDS
+ fade = 1 SECONDS
+ fadein = 0.1 SECONDS
+ grow = -0.1
+ velocity = generator(GEN_CIRCLE, 3, 3, SQUARE_RAND)
+ position = generator(GEN_SPHERE, 0, 0, LINEAR_RAND)
+ scale = generator(GEN_VECTOR, list(0.5, 0.5), list(1,1), NORMAL_RAND)
+ drift = list(0)
+
+/particles/embers/spark/severe
+ count = 10
+ spawning = 5
+ gradient = list("#FCE6B6", "#FFFFFF")
diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm
new file mode 100644
index 0000000000..323e4ffbdc
--- /dev/null
+++ b/code/game/objects/effects/particles/smoke.dm
@@ -0,0 +1,129 @@
+// All the smoke variant particles.
+/particles/smoke
+ icon = 'icons/effects/particles/smoke.dmi'
+ icon_state = list("smoke_1" = 1, "smoke_2" = 1, "smoke_3" = 2)
+ width = 100
+ height = 100
+ count = 1000
+ spawning = 4
+ lifespan = 1.5 SECONDS
+ fade = 1 SECONDS
+ velocity = list(0, 0.4, 0)
+ position = list(6, 0, 0)
+ drift = generator(GEN_SPHERE, 0, 2, NORMAL_RAND)
+ friction = 0.2
+ gravity = list(0, 0.95)
+ grow = 0.05
+
+/particles/smoke/burning
+ position = list(0, 0, 0)
+
+/particles/smoke/burning/small
+ spawning = 1
+ scale = list(0.8, 0.8)
+ velocity = list(0, 0.4, 0)
+
+/particles/smoke/steam
+ icon_state = list("steam_1" = 1, "steam_2" = 1, "steam_3" = 2)
+ fade = 1.5 SECONDS
+
+/particles/smoke/steam/mild
+ spawning = 1
+ velocity = list(0, 0.3, 0)
+ friction = 0.25
+
+/particles/smoke/steam/bad
+ icon_state = list("steam_1" = 1, "smoke_1" = 1, "smoke_2" = 1, "smoke_3" = 1)
+ spawning = 2
+ velocity = list(0, 0.25, 0)
+
+/particles/smoke/steam/mald
+ icon_state = list("steam_1" = 1, "steam_2" = 1, "steam_3" = 2)
+ velocity = list(0, 0.25, 0)
+ lifespan = 1 SECONDS
+ fade = 0.5 SECONDS
+ position = list(-1, 12, 0)
+
+/particles/smoke/cig
+ icon_state = list("steam_1" = 2, "steam_2" = 1, "steam_3" = 1)
+ count = 1
+ spawning = 0.05 // used to pace it out roughly in time with breath ticks
+ position = list(-6, -2, 0)
+ gravity = list(0, 0.75, 0)
+ lifespan = 0.75 SECONDS
+ fade = 0.75 SECONDS
+ velocity = list(0, 0.2, 0)
+ scale = 0.5
+ grow = 0.01
+ friction = 0.5
+ color = "#d0d0d09d"
+
+/particles/smoke/cig/big
+ icon_state = list("steam_1" = 1, "steam_2" = 2, "steam_3" = 2)
+ gravity = list(0, 0.5, 0)
+ velocity = list(0, 0.1, 0)
+ lifespan = 1 SECONDS
+ fade = 1 SECONDS
+ grow = 0.1
+ scale = 0.75
+ spawning = 1
+ friction = 0.75
+
+/particles/smoke/ash
+ icon_state = list("ash_1" = 2, "ash_2" = 2, "ash_3" = 1, "smoke_1" = 3, "smoke_2" = 2)
+ count = 500
+ spawning = 1
+ lifespan = 1 SECONDS
+ fade = 0.2 SECONDS
+ fadein = 0.7 SECONDS
+ position = generator(GEN_VECTOR, list(-3, 5, 0), list(3, 6.5, 0), NORMAL_RAND)
+ velocity = generator(GEN_VECTOR, list(-0.1, 0.4, 0), list(0.1, 0.5, 0), NORMAL_RAND)
+
+/particles/fog
+ icon = 'icons/effects/particles/smoke.dmi'
+ icon_state = list("chill_1" = 2, "chill_2" = 2, "chill_3" = 1)
+
+/particles/fog/breath
+ count = 1
+ spawning = 1
+ lifespan = 1 SECONDS
+ fade = 0.5 SECONDS
+ grow = 0.05
+ spin = 2
+ color = "#fcffff77"
+
+/particles/smoke/cyborg
+ count = 5
+ spawning = 1
+ lifespan = 1 SECONDS
+ fade = 1.8 SECONDS
+ position = list(0, 0, 0)
+ scale = list(0.5, 0.5)
+ grow = 0.1
+
+/particles/smoke/cyborg/heavy_damage
+ lifespan = 0.8 SECONDS
+ fade = 0.8 SECONDS
+
+/particles/hotspring_steam
+ icon = 'icons/effects/particles/smoke.dmi'
+ icon_state = list(
+ "steam_cloud_1" = 1,
+ "steam_cloud_2" = 1,
+ "steam_cloud_3" = 1,
+ "steam_cloud_4" = 1,
+ "steam_cloud_5" = 1,
+ )
+ color = "#FFFFFF8A"
+ count = 5
+ spawning = 0.3
+ lifespan = 3 SECONDS
+ fade = 1.2 SECONDS
+ fadein = 0.4 SECONDS
+ position = generator(GEN_BOX, list(-17,-15,0), list(24,15,0), NORMAL_RAND)
+ scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
+ drift = generator(GEN_SPHERE, list(-0.01,0), list(0.01,0.01), UNIFORM_RAND)
+ spin = generator(GEN_NUM, list(-2,2), NORMAL_RAND)
+ gravity = list(0.05, 0.28)
+ friction = 0.3
+ grow = 0.037
diff --git a/code/game/objects/effects/particles/water.dm b/code/game/objects/effects/particles/water.dm
new file mode 100644
index 0000000000..88e0ef542e
--- /dev/null
+++ b/code/game/objects/effects/particles/water.dm
@@ -0,0 +1,14 @@
+// Water related particles.
+/particles/droplets
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot"=2,"drop"=1)
+ width = 32
+ height = 36
+ count = 5
+ spawning = 0.2
+ lifespan = 1 SECONDS
+ fade = 0.5 SECONDS
+ color = "#549EFF"
+ position = generator(GEN_BOX, list(-9,-9,0), list(9,18,0), NORMAL_RAND)
+ scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
+ gravity = list(0, -0.9)
diff --git a/code/game/objects/effects/prop/columnblast.dm b/code/game/objects/effects/prop/columnblast.dm
index e0698006d5..b254669a41 100644
--- a/code/game/objects/effects/prop/columnblast.dm
+++ b/code/game/objects/effects/prop/columnblast.dm
@@ -42,7 +42,7 @@
Target.hotspot_expose(1000, 50, 1)
for(var/mob/living/L in Target)
- L.fire_stacks += 2
- L.add_modifier(/datum/modifier/fire/stack_managed/intense, 30 SECONDS)
+ L.adjust_fire_stacks(2)
+ L.ignite_mob()
return TRUE
diff --git a/code/game/objects/effects/shared_particle_holder.dm b/code/game/objects/effects/shared_particle_holder.dm
new file mode 100644
index 0000000000..a97812a28b
--- /dev/null
+++ b/code/game/objects/effects/shared_particle_holder.dm
@@ -0,0 +1,100 @@
+#define SHARED_PARTICLE_HOLDER_INDEX 1
+#define SHARED_PARTICLE_USER_NUM_INDEX 2
+// Assoc list of particle type/key -> list(list of particle holders, number of particle users)
+GLOBAL_LIST_EMPTY(shared_particles)
+
+//A more abstract version of particle holder not bound to a specific object
+/obj/effect/abstract/shared_particle_holder
+ name = "shared particle holder"
+ desc = "How are you reading this? Please make a bug report :)"
+ appearance_flags = KEEP_APART|KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE|RESET_COLOR
+ vis_flags = VIS_INHERIT_PLANE
+ layer = ABOVE_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ anchored = TRUE
+ /// Holds info about how this particle emitter works
+ /// See \code\__DEFINES\particles.dm
+ var/particle_flags = NONE
+
+/obj/effect/abstract/shared_particle_holder/Initialize(mapload, particle_path = /particles/smoke, particle_flags = NONE)
+ . = ..()
+ // Shouldn't exist outside of nullspace
+ loc = null
+ src.particle_flags = particle_flags
+ particles = new particle_path()
+
+/obj/effect/abstract/shared_particle_holder/Destroy(force)
+ QDEL_NULL(particles)
+ return ..()
+
+/* Adds (or creates and adds) a shared particle holder
+ * Shared particle holders are held in nullspace and added to vis_contents of all atoms using it
+ * in order to save clientside performance by making clients only render 3-5 particle holders
+ * for 400 objects using them. This should be prioritized over normal particles when possible if it is known
+ * that there will be a lot of objects using certain particles.
+ * custom_key can be used to create a new pool of already existing particle type in case you're planning to edit holder's color or properties
+ * pool_size controls how many particle holders per type are created. Any objects over this cap will pick an existing holder from the pool.
+ *
+ * Now, this code seems fucked up, that's because this is meant to support both objects (and mobs) and turfs, *however* areas are special
+ * and don't have vis_contents, so to avoid copypaste code we do this weirdness
+ */
+/atom/proc/add_shared_particles(particle_type, custom_key = null, particle_flags = NONE, pool_size = 3)
+ var/atom/movable/play_pretend = src
+ var/particle_key = custom_key || "[particle_type]"
+ if (!GLOB.shared_particles[particle_key])
+ GLOB.shared_particles[particle_key] = list(list(new /obj/effect/abstract/shared_particle_holder(null, particle_type, particle_flags)), 1)
+ play_pretend.vis_contents += GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX][1]
+ return GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX][1]
+
+ var/list/type_holders = GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX]
+ for (var/obj/effect/abstract/shared_particle_holder/particle_holder as anything in type_holders)
+ if (particle_holder in play_pretend.vis_contents)
+ return particle_holder
+
+ if (length(type_holders) < pool_size)
+ var/obj/effect/abstract/shared_particle_holder/new_holder = new(null, particle_type, particle_flags)
+ type_holders += new_holder
+ play_pretend.vis_contents += new_holder
+ GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] += 1
+ return new_holder
+
+ var/obj/effect/abstract/shared_particle_holder/particle_holder = pick(type_holders)
+ play_pretend.vis_contents += particle_holder
+ GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] += 1
+ return particle_holder
+
+/area/add_shared_particles(particle_type, custom_key = null, particle_flags = NONE, pool_size = 3)
+ CRASH("add_shared_particles was called on an area [src] ([type]) trying to add [particle_type]! Only turfs and movables support shared particles.")
+
+/* Removes shared particles from object's vis_contents and disposes of it if nothing uses that type/key of particle
+ * particle_key can be either a type (if no custom_key was passed) or said custom_key
+ */
+/atom/proc/remove_shared_particles(particle_key, delete_on_empty = TRUE)
+ if (!particle_key)
+ return
+
+ if (ispath(particle_key))
+ particle_key = "[particle_key]"
+
+ if (!GLOB.shared_particles[particle_key])
+ return
+
+ var/atom/movable/play_pretend = src
+ var/list/type_holders = GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX]
+ for (var/obj/effect/abstract/shared_particle_holder/particle_holder as anything in type_holders)
+ if (!(particle_holder in play_pretend.vis_contents))
+ continue
+
+ play_pretend.vis_contents -= particle_holder
+ GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] -= 1
+
+ if (delete_on_empty && GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] <= 0)
+ QDEL_LIST(type_holders)
+ GLOB.shared_particles -= particle_key
+ return
+
+/area/remove_shared_particles(particle_key, delete_on_empty = TRUE)
+ CRASH("remove_shared_particles was called on an area [src] ([type]) trying to add [particle_key]! Only turfs and movables support shared particles.")
+
+#undef SHARED_PARTICLE_HOLDER_INDEX
+#undef SHARED_PARTICLE_USER_NUM_INDEX
diff --git a/code/game/objects/items/weapons/cigs_lighters.dm b/code/game/objects/items/weapons/cigs_lighters.dm
index 152c72ffb2..e4704e766e 100644
--- a/code/game/objects/items/weapons/cigs_lighters.dm
+++ b/code/game/objects/items/weapons/cigs_lighters.dm
@@ -38,7 +38,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
/obj/item/flame/match/process()
if(isliving(loc))
var/mob/living/M = loc
- M.IgniteMob()
+ M.ignite_mob()
var/turf/location = get_turf(src)
smoketime--
if(smoketime < 1)
@@ -650,7 +650,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
return
if(lit == 1)
- M.IgniteMob()
+ M.ignite_mob()
add_attack_logs(user,M,"Lit on fire with [src]")
if(istype(M.wear_mask, /obj/item/clothing/mask/smokable/cigarette) && user.zone_sel.selecting == O_MOUTH && lit)
@@ -850,7 +850,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
return
if(lit == 1)
- M.IgniteMob()
+ M.ignite_mob()
add_attack_logs(user,M,"Lit on fire with [src]")
if(istype(M.wear_mask, /obj/item/clothing/mask/smokable/cigarette) && user.zone_sel.selecting == O_MOUTH && lit)
@@ -921,7 +921,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
return
if(lit == 1)
- M.IgniteMob()
+ M.ignite_mob()
add_attack_logs(user,M,"Lit on fire with [src]")
if(istype(M.wear_mask, /obj/item/clothing/mask/smokable/cigarette) && user.zone_sel.selecting == O_MOUTH && lit)
@@ -1087,7 +1087,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM
return
if (lit == 1)
- M.IgniteMob()
+ M.ignite_mob()
add_attack_logs(user, M, "Lit on fire with [src]")
if (istype(M.wear_mask, /obj/item/clothing/mask/smokable/cigarette) && user.zone_sel.selecting == O_MOUTH && lit)
diff --git a/code/game/objects/items/weapons/tools/weldingtool.dm b/code/game/objects/items/weapons/tools/weldingtool.dm
index 5b7b3c2866..8909ca8c42 100644
--- a/code/game/objects/items/weapons/tools/weldingtool.dm
+++ b/code/game/objects/items/weapons/tools/weldingtool.dm
@@ -166,7 +166,7 @@
var/turf/location = get_turf(user)
if(isliving(O))
var/mob/living/L = O
- L.IgniteMob()
+ L.ignite_mob()
if (istype(location, /turf))
location.hotspot_expose(700, 50, 1)
/obj/item/weldingtool/attack_self(mob/user)
diff --git a/code/game/objects/items/weapons/towels.dm b/code/game/objects/items/weapons/towels.dm
index a200c49c41..a30f8d7d8d 100644
--- a/code/game/objects/items/weapons/towels.dm
+++ b/code/game/objects/items/weapons/towels.dm
@@ -25,9 +25,7 @@
user.visible_message(span_notice("[user] uses [src] to towel themselves off."))
playsound(src, 'sound/weapons/towelwipe.ogg', 25, 1)
if(user.fire_stacks > 0)
- user.fire_stacks = (max(0, user.fire_stacks - 1.5))
- else if(user.fire_stacks < 0)
- user.fire_stacks = (min(0, user.fire_stacks + 1.5))
+ user.adjust_fire_stacks(-1.5)
/obj/item/towel/random/Initialize(mapload)
. = ..()
diff --git a/code/game/objects/structures/bonfire.dm b/code/game/objects/structures/bonfire.dm
index 168abf2d0e..dfeecd0230 100644
--- a/code/game/objects/structures/bonfire.dm
+++ b/code/game/objects/structures/bonfire.dm
@@ -166,7 +166,8 @@
return TRUE
-/obj/structure/bonfire/proc/extinguish()
+/obj/structure/bonfire/extinguish()
+ . = ..()
if(burning)
burning = FALSE
update_icon()
@@ -193,7 +194,7 @@
var/mob/living/L = A
if(!(L.is_incorporeal()))
L.adjust_fire_stacks(get_fuel_amount() / 4)
- L.IgniteMob()
+ L.ignite_mob()
/obj/structure/bonfire/update_icon()
cut_overlays()
@@ -360,7 +361,8 @@
return FALSE
return TRUE
-/obj/structure/fireplace/proc/extinguish()
+/obj/structure/fireplace/extinguish()
+ . = ..()
if(burning)
burning = FALSE
update_icon()
diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm
index ff04b13f04..cb714ddafb 100644
--- a/code/game/objects/structures/watercloset.dm
+++ b/code/game/objects/structures/watercloset.dm
@@ -253,8 +253,8 @@
if(isliving(O))
var/mob/living/L = O
- L.ExtinguishMob()
- L.fire_stacks = -20 //Douse ourselves with water to avoid fire more easily
+ L.extinguish_mob()
+ L.adjust_fire_stacks(-20) //Douse ourselves with water to avoid fire more easily
if(iscarbon(O))
//flush away reagents on the skin
diff --git a/code/game/turfs/simulated/water.dm b/code/game/turfs/simulated/water.dm
index 419859363d..70e046c76f 100644
--- a/code/game/turfs/simulated/water.dm
+++ b/code/game/turfs/simulated/water.dm
@@ -184,10 +184,10 @@
return
/mob/living/water_act(amount)
- adjust_fire_stacks(-amount * 5)
+ // adjust_fire_stacks(-amount * 5)
+ adjust_wet_stacks(amount * 5)
for(var/atom/movable/AM in contents)
AM.water_act(amount)
- remove_modifiers_of_type(/datum/modifier/fire)
inflict_water_damage(20 * amount) // Only things vulnerable to water will actually be harmed (slimes/prommies).
var/list/shoreline_icon_cache = list()
diff --git a/code/modules/admin/player_effects.dm b/code/modules/admin/player_effects.dm
index 1a2b1286bc..7f88f92376 100644
--- a/code/modules/admin/player_effects.dm
+++ b/code/modules/admin/player_effects.dm
@@ -73,7 +73,7 @@
if(!istype(Tar))
return
Tar.adjust_fire_stacks(10)
- Tar.IgniteMob()
+ Tar.ignite_mob()
Tar.visible_message(span_danger("[target] bursts into flames!"))
if("lightning_strike")
diff --git a/code/modules/admin/verbs/smite.dm b/code/modules/admin/verbs/smite.dm
index 4380df9bd7..f743fe9b09 100644
--- a/code/modules/admin/verbs/smite.dm
+++ b/code/modules/admin/verbs/smite.dm
@@ -36,7 +36,7 @@
if(SMITE_SPONTANEOUSCOMBUSTION)
target.adjust_fire_stacks(10)
- target.IgniteMob()
+ target.ignite_mob()
target.visible_message(span_danger("[target] bursts into flames!"))
if(SMITE_LIGHTNINGBOLT)
diff --git a/code/modules/blob2/overmind/types/pressurized_slime.dm b/code/modules/blob2/overmind/types/pressurized_slime.dm
index 98a78440bf..5ecba15b56 100644
--- a/code/modules/blob2/overmind/types/pressurized_slime.dm
+++ b/code/modules/blob2/overmind/types/pressurized_slime.dm
@@ -52,5 +52,5 @@
/datum/blob_type/pressurized_slime/on_chunk_use(obj/item/blobcore_chunk/B, mob/living/user) // Drenches you in water.
if(user)
- user.ExtinguishMob()
- user.fire_stacks = CLAMP(user.fire_stacks - 1, -25, 25)
+ user.adjust_wet_stacks(2.5)
+ user.extinguish_mob()
diff --git a/code/modules/detectivework/tools/rag.dm b/code/modules/detectivework/tools/rag.dm
index 7d92f9d73e..74161bbb62 100644
--- a/code/modules/detectivework/tools/rag.dm
+++ b/code/modules/detectivework/tools/rag.dm
@@ -108,7 +108,7 @@
if(on_fire) //Check if rag is on fire, if so igniting them and stopping.
user.visible_message(span_danger("\The [user] hits [target] with [src]!"),)
user.do_attack_animation(src)
- M.IgniteMob()
+ M.ignite_mob()
else if(user.zone_sel.selecting == O_MOUTH) //Check player target location, provided the rag is not on fire. Then check if mouth is exposed.
if(ishuman(target)) //Added this since player species process reagents in majority of cases.
var/mob/living/carbon/human/H = target
@@ -198,7 +198,8 @@
update_name()
update_icon()
-/obj/item/reagent_containers/glass/rag/proc/extinguish()
+/obj/item/reagent_containers/glass/rag/extinguish()
+ . = ..()
STOP_PROCESSING(SSobj, src)
set_light(0)
on_fire = 0
@@ -220,7 +221,7 @@
//copied from matches
if(isliving(loc))
var/mob/living/M = loc
- M.IgniteMob()
+ M.ignite_mob()
var/turf/location = get_turf(src)
if(location)
location.hotspot_expose(700, 5)
diff --git a/code/modules/food/food/drinks/bottle.dm b/code/modules/food/food/drinks/bottle.dm
index 994e14b356..3658fbc291 100644
--- a/code/modules/food/food/drinks/bottle.dm
+++ b/code/modules/food/food/drinks/bottle.dm
@@ -83,7 +83,7 @@
if(rag && rag.on_fire && isliving(against))
rag.forceMove(loc)
var/mob/living/L = against
- L.IgniteMob()
+ L.ignite_mob()
playsound(src, "shatter", 70, 1)
src.transfer_fingerprints_to(B)
diff --git a/code/modules/mob/_modifiers/fire.dm b/code/modules/mob/_modifiers/fire.dm
deleted file mode 100644
index 31f6a4f820..0000000000
--- a/code/modules/mob/_modifiers/fire.dm
+++ /dev/null
@@ -1,42 +0,0 @@
-// Ignition, but confined to the modifier system.
-// This makes it more predictable and thus, easier to balance.
-/datum/modifier/fire
- name = "on fire"
- desc = "You are on fire! You will be harmed until the fire goes out or you extinguish it with water."
- mob_overlay_state = "on_fire"
-
- on_created_text = span_danger("You burst into flames!")
- on_expired_text = span_warning("The fire starts to fade.")
- stacks = MODIFIER_STACK_ALLOWED // Multiple instances will hurt a lot.
- var/damage_per_tick = 5
-
-/datum/modifier/fire/intense
- mob_overlay_state = "on_fire_intense"
- damage_per_tick = 10
-
-/datum/modifier/fire/tick()
- holder.inflict_heat_damage(damage_per_tick)
-
-/datum/modifier/fire/weak
- damage_per_tick = 1
-
-/*
- * Modifier used by projectiles, like the flamethrower, that rely heavily on fire_stacks to persist.
- */
-
-/datum/modifier/fire/stack_managed/tick()
- ..()
-
- if(!holder.fire_stacks || holder.fire_stacks < 0)
- if(prob(10))
- expire()
-
- else if(holder.fire_stacks > 0)
- holder.fire_stacks -= 0.5
-
-/datum/modifier/fire/stack_managed/intense
- mob_overlay_state = "on_fire_intense"
- damage_per_tick = 10
-
-/datum/modifier/fire/stack_managed/weak
- damage_per_tick = 1
diff --git a/code/modules/mob/living/carbon/alien/life.dm b/code/modules/mob/living/carbon/alien/life.dm
index 8640e65de7..83d272d31c 100644
--- a/code/modules/mob/living/carbon/alien/life.dm
+++ b/code/modules/mob/living/carbon/alien/life.dm
@@ -156,8 +156,5 @@
else
clear_alert("alien_fire")
-/mob/living/carbon/alien/handle_fire()
- if(..())
- return
- bodytemperature += BODYTEMP_HEATING_MAX //If you're on fire, you heat up!
- return
+/mob/living/carbon/alien/on_fire_stack(seconds_per_tick, datum/status_effect/fire_handler/fire_stacks/fire_handler)
+ bodytemperature += BODYTEMP_HEATING_MAX
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 362f4b897c..311ecc6a6e 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -244,7 +244,7 @@
src.adjust_fire_stacks(-0.5)
if (prob(10) && (M.fire_stacks <= 0))
M.adjust_fire_stacks(1)
- M.IgniteMob()
+ M.ignite_mob()
if (M.on_fire)
M.visible_message(span_danger("The fire spreads from [src] to [M]!"),
span_danger("The fire spreads to you as well!"))
@@ -253,8 +253,7 @@
if (src.fire_stacks <= 0)
M.visible_message(span_warning("[M] successfully pats out [src]'s flames."),
span_warning("You successfully pat out [src]'s flames."))
- src.ExtinguishMob()
- src.fire_stacks = 0
+ src.extinguish_mob()
else
if (ishuman(src) && src:w_uniform)
var/mob/living/carbon/human/H = src
@@ -284,7 +283,7 @@
src.adjust_fire_stacks(1)
M.adjust_fire_stacks(-1)
if(M.on_fire)
- src.IgniteMob()
+ src.ignite_mob()
M.resting = 0 //Hoist yourself up up off the ground. No para/stunned/weakened removal.
update_canmove()
else if(istype(hugger))
@@ -296,7 +295,7 @@
src.adjust_fire_stacks(1)
M.adjust_fire_stacks(-1)
if(M.on_fire)
- src.IgniteMob()
+ src.ignite_mob()
AdjustParalysis(-3)
AdjustStunned(-3)
AdjustWeakened(-3)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 62cf2755a9..1083054f89 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -280,7 +280,7 @@
if(AM.is_incorporeal())
return
- spread_fire(AM)
+ spreadFire(AM)
..() // call parent because we moved behavior to parent
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 42900a4424..4651198e68 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -652,10 +652,9 @@ emp_act
return CLAMP(1-converted_protection, 0, 1)
/mob/living/carbon/human/water_act(amount)
- adjust_fire_stacks(-amount * 5)
+ adjust_wet_stacks(amount * 5)
for(var/atom/movable/AM in contents)
AM.water_act(amount)
- remove_modifiers_of_type(/datum/modifier/fire)
species.handle_water_damage(src, amount)
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index 4d364b1b57..1b027a45ce 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -2236,26 +2236,13 @@
hud_updateflag = 0
-/mob/living/carbon/human/handle_fire()
- if(..())
- return
-
- var/thermal_protection = get_heat_protection(fire_stacks * 1500) // Arbitrary but below firesuit max temp when below 20 stacks.
-
- if(thermal_protection == 1) // Immune.
- return
- else
- var/fire_temp_add = (BODYTEMP_HEATING_MAX + (fire_stacks * 15)) * (1-thermal_protection)
- //This is to prevent humans from heating up indefinitely. A human being on fire (fat burns at 250C) can't magically
- // increase your body temperature beyond 250C, but it's possible something else (atmos) has heated us up beyond it,
- // so don't worry about the firestacks at that point. Really, we should be cooling the room down, because it has
- // to expend energy to heat our body up! But let's not worry about that.
-
- // This whole section above is ABSOLUTELY STUPID and makes no sense and this would prevent too-high-heat from even being able to hurt someone. No. We will heat up for as long as needed.
- //if((bodytemperature + fire_temp_add) > HUMAN_COMBUSTION_TEMP)
- // return
-
- bodytemperature += fire_temp_add
+/mob/living/carbon/human/on_fire_stack(seconds_per_tick, datum/status_effect/fire_handler/fire_stacks/fire_handler)
+ SEND_SIGNAL(src, COMSIG_HUMAN_BURNING)
+ // burn_clothing(seconds_per_tick, fire_handler.stacks)
+ var/no_protection = FALSE
+ if(HAS_TRAIT(src, TRAIT_IGNORE_FIRE_PROTECTION))
+ no_protection = TRUE
+ fire_handler.harm_human(seconds_per_tick, no_protection)
/mob/living/carbon/human/rejuvenate()
restore_blood()
diff --git a/code/modules/mob/living/carbon/resist.dm b/code/modules/mob/living/carbon/resist.dm
index c3e1a0ee65..da0e0e8444 100644
--- a/code/modules/mob/living/carbon/resist.dm
+++ b/code/modules/mob/living/carbon/resist.dm
@@ -12,7 +12,7 @@
span_danger("[src] has successfully extinguished themselves!"),
span_notice("You extinguish yourself.")
)
- ExtinguishMob()
+ extinguish_mob()
return TRUE
/mob/living/carbon/resist_restraints()
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index c6ade8d909..2a9047fd94 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -192,8 +192,6 @@
if(agony) apply_effect(agony, AGONY, blocked)
if(flammable) adjust_fire_stacks(flammable)
if(ignite)
- if(ignite >= 3)
- add_modifier(/datum/modifier/fire/stack_managed/intense, 60 SECONDS)
- else
- add_modifier(/datum/modifier/fire/stack_managed, 45 * ignite SECONDS)
+ adjust_fire_stacks(ignite)
+ ignite_mob()
return 1
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 788c33bb8c..dc87a27d33 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -59,9 +59,6 @@
if(environment)
handle_environment(environment)
- //Check if we're on fire
- handle_fire()
-
if(client) // Handle re-running ambience to mobs if they've remained in an area, AND have an active client assigned to them, and do not have repeating ambience disabled.
handle_ambience()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index bb86d8738e..d10e7e3037 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -725,12 +725,11 @@
BITSET(hud_updateflag, HEALTH_HUD)
BITSET(hud_updateflag, STATUS_HUD)
BITSET(hud_updateflag, LIFE_HUD)
- ExtinguishMob()
- fire_stacks = 0
if(ai_holder) // AI gets told to sleep when killed. Since they're not dead anymore, wake it up.
ai_holder.go_wake()
SEND_SIGNAL(src, COMSIG_HUMAN_DNA_FINALIZED)
+ SEND_SIGNAL(src, COMSIG_LIVING_AHEAL)
/mob/living/proc/rejuvenate()
if(reagents)
@@ -1176,10 +1175,6 @@
return FALSE
return TRUE
-// Gets the correct icon_state for being on fire. See OnFire.dmi for the icons.
-/mob/living/proc/get_fire_icon_state()
- return "generic"
-
// Called by job_controller.
/mob/living/proc/equip_post_job()
return
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 7617cfc70f..de33f3d1bd 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -419,85 +419,6 @@
spawn(1) updatehealth()
return 1
-/mob/living/proc/IgniteMob()
- if(fire_stacks > 0 && !on_fire)
- on_fire = 1
- new/obj/effect/dummy/lighting_obj/moblight/fire(src)
- throw_alert("fire", /obj/screen/alert/fire)
- update_fire()
- firesoundloop.start()
-
-/mob/living/proc/ExtinguishMob()
- if(on_fire)
- on_fire = 0
- fire_stacks = 0
- for(var/obj/effect/dummy/lighting_obj/moblight/fire/F in src)
- qdel(F)
- clear_alert("fire")
- update_fire()
- firesoundloop.stop()
-
- if(has_modifier_of_type(/datum/modifier/fire))
- remove_modifiers_of_type(/datum/modifier/fire)
-
-/mob/living/proc/update_fire()
- return
-
-/mob/living/proc/adjust_fire_stacks(add_fire_stacks) //Adjusting the amount of fire_stacks we have on person
- fire_stacks = CLAMP(fire_stacks + add_fire_stacks, FIRE_MIN_STACKS, FIRE_MAX_STACKS)
-
-/mob/living/proc/handle_fire()
- if(fire_stacks < 0)
- fire_stacks = min(0, ++fire_stacks) //If we've doused ourselves in water to avoid fire, dry off slowly
-
- if(fire_stacks > 0)
- fire_stacks = max(0, (fire_stacks-0.1)) //Should slowly burn out
-
- if(!on_fire)
- return 1
- else if(fire_stacks <= 0)
- ExtinguishMob() //Fire's been put out.
- return 1
-
- var/datum/gas_mixture/G = loc.return_air() // Check if we're standing in an oxygenless environment
- if(G.gas[GAS_O2] < 1)
- ExtinguishMob() //If there's no oxygen in the tile we're on, put out the fire
- return 1
-
- var/turf/location = get_turf(src)
- location.hotspot_expose(fire_burn_temperature(), 50, 1)
-
-//altered this to cap at the temperature of the fire causing it, using the same 1:1500 value as /mob/living/carbon/human/handle_fire() in human/life.dm
-/mob/living/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume)
- if(exposed_temperature)
- if(fire_stacks < exposed_temperature/1500) // Subject to balance
- adjust_fire_stacks(2)
- else
- adjust_fire_stacks(2)
- IgniteMob()
-
-//Share fire evenly between the two mobs
-//Called in MobCollide() and Crossed()
-/mob/living/proc/spread_fire(mob/living/L)
- return
-// This is commented out pending discussion on Polaris. If you're a downsteam and you want people to spread fire by touching each other, feel free to uncomment this.
-/*
- if(!istype(L))
- return
- var/L_old_on_fire = L.on_fire
-
- if(on_fire) //Only spread fire stacks if we're on fire
- fire_stacks /= 2
- L.fire_stacks += fire_stacks
- if(L.IgniteMob())
- message_admins("[key_name(src)] bumped into [key_name(L)] and set them on fire.")
-
- if(L_old_on_fire) //Only ignite us and gain their stacks if they were onfire before we bumped them
- L.fire_stacks /= 2
- fire_stacks += L.fire_stacks
- IgniteMob()
-*/
-
/mob/living/proc/get_cold_protection()
return 0
@@ -534,8 +455,7 @@
// Called when touching a lava tile.
// Does roughly 70 damage (30 instantly, up to ~40 over time) to unprotected mobs, and 10 to fully protected mobs.
/mob/living/lava_act()
- adjust_fire_stacks(1)
- add_modifier(/datum/modifier/fire/stack_managed/intense, 8 SECONDS) // Around 40 total if left to burn and without fire protection per stack.
+ adjust_fire_stacks(4)
inflict_heat_damage(20) // Another 20, however this is instantly applied to unprotected mobs.
adjustFireLoss(10) // Lava cannot be 100% resisted with fire protection.
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 5dcaa13342..e40f60544c 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -42,8 +42,12 @@
var/tod = null // Time of death
var/update_slimes = 1
var/silent = null // Can't talk. Value goes down every life proc.
- var/on_fire = 0 //The "Are we on fire?" var
+
+ /// Helper vars for quick access to firestacks, these should be updated every time firestacks are adjusted
+ var/on_fire = 0
var/fire_stacks
+ /// Rate at which fire stacks should decay from this mob
+ var/fire_stack_decay_rate = -0.05
var/failed_last_breath = 0 //This is used to determine if the mob failed a breath. If they did fail a brath, they will attempt to breathe each tick, otherwise just once per 4 ticks.
var/lastpuke = 0
@@ -115,3 +119,6 @@
var/ooc_notes_favs = null
var/ooc_notes_maybes = null
var/ooc_notes_style = FALSE
+
+ ///a list of all status effects the mob has
+ var/list/status_effects
diff --git a/code/modules/mob/living/living_fire.dm b/code/modules/mob/living/living_fire.dm
new file mode 100644
index 0000000000..b8cf958297
--- /dev/null
+++ b/code/modules/mob/living/living_fire.dm
@@ -0,0 +1,179 @@
+//Mobs on Fire
+
+//altered this to cap at the temperature of the fire causing it, using the same 1:1500 value as /mob/living/carbon/human/handle_fire() in human/life.dm
+/mob/living/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume)
+ if(exposed_temperature)
+ if(fire_stacks < exposed_temperature/1500) // Subject to balance
+ adjust_fire_stacks(2)
+ else
+ adjust_fire_stacks(2)
+ ignite_mob()
+
+
+/// Global list that containes cached fire overlays for mobs
+GLOBAL_LIST_EMPTY(fire_appearances)
+
+/mob/living/proc/ignite_mob(silent)
+ if(fire_stacks <= 0)
+ return FALSE
+
+ var/datum/status_effect/fire_handler/fire_stacks/fire_status = has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+ if(!fire_status || fire_status.on_fire)
+ return FALSE
+
+ return fire_status.ignite(silent)
+
+/**
+ * Extinguish all fire on the mob
+ *
+ * This removes all fire stacks, fire effects, alerts, and moods
+ * Signals the extinguishing.
+ */
+/mob/living/proc/extinguish_mob()
+ if(HAS_TRAIT(src, TRAIT_NO_EXTINGUISH)) //The everlasting flames will not be extinguished
+ return
+ var/datum/status_effect/fire_handler/fire_stacks/fire_status = has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+ if(!fire_status || !fire_status.on_fire)
+ return
+ remove_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+
+/mob/living/proc/update_fire()
+ return
+
+/**
+ * Adjust the amount of fire stacks on a mob
+ *
+ * This modifies the fire stacks on a mob.
+ *
+ * Vars:
+ * * stacks: int The amount to modify the fire stacks
+ * * fire_type: type Type of fire status effect that we apply, should be subtype of /datum/status_effect/fire_handler/fire_stacks
+ */
+
+/mob/living/proc/adjust_fire_stacks(stacks, fire_type = /datum/status_effect/fire_handler/fire_stacks)
+ if(stacks < 0)
+ if(HAS_TRAIT(src, TRAIT_NO_EXTINGUISH)) //You can't reduce fire stacks of the everlasting flames
+ return
+ stacks = max(-fire_stacks, stacks)
+ apply_status_effect(fire_type, stacks)
+
+/mob/living/proc/adjust_wet_stacks(stacks, wet_type = /datum/status_effect/fire_handler/wet_stacks)
+ if(HAS_TRAIT(src, TRAIT_NO_EXTINGUISH)) //The everlasting flames will not be extinguished
+ return
+ if(stacks < 0)
+ stacks = max(fire_stacks, stacks)
+ apply_status_effect(wet_type, stacks)
+
+/**
+ * Set the fire stacks on a mob
+ *
+ * This sets the fire stacks on a mob, stacks are clamped between -20 and 20.
+ * If the fire stacks are reduced to 0 then we will extinguish the mob.
+ *
+ * Vars:
+ * * stacks: int The amount to set fire_stacks to
+ * * fire_type: type Type of fire status effect that we apply, should be subtype of /datum/status_effect/fire_handler/fire_stacks
+ * * remove_wet_stacks: bool If we remove all wet stacks upon doing this
+ */
+
+/mob/living/proc/set_fire_stacks(stacks, fire_type = /datum/status_effect/fire_handler/fire_stacks, remove_wet_stacks = TRUE)
+ if(stacks < 0) //Shouldn't happen, ever
+ CRASH("set_fire_stacks received negative [stacks] fire stacks")
+
+ if(remove_wet_stacks)
+ remove_status_effect(/datum/status_effect/fire_handler/wet_stacks)
+
+ if(stacks == 0)
+ remove_status_effect(fire_type)
+ return
+
+ apply_status_effect(fire_type, stacks, TRUE)
+
+/mob/living/proc/set_wet_stacks(stacks, wet_type = /datum/status_effect/fire_handler/wet_stacks, remove_fire_stacks = TRUE)
+ if(stacks < 0)
+ CRASH("set_wet_stacks received negative [stacks] wet stacks")
+
+ if(remove_fire_stacks)
+ remove_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+
+ if(stacks == 0)
+ remove_status_effect(wet_type)
+ return
+
+ apply_status_effect(wet_type, stacks, TRUE)
+
+//Share fire evenly between the two mobs
+//Called in MobBump() and Crossed()
+/mob/living/proc/spreadFire(mob/living/spread_to)
+ if(!istype(spread_to))
+ return
+
+ // can't spread fire to mobs that don't catch on fire
+ if(HAS_TRAIT(spread_to, TRAIT_NOFIRE_SPREAD) || HAS_TRAIT(src, TRAIT_NOFIRE_SPREAD))
+ return
+
+ var/datum/status_effect/fire_handler/fire_stacks/fire_status = has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+ var/datum/status_effect/fire_handler/fire_stacks/their_fire_status = spread_to.has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
+ if(fire_status && fire_status.on_fire)
+ if(their_fire_status && their_fire_status.on_fire)
+ var/firesplit = (fire_stacks + spread_to.fire_stacks) / 2
+ var/fire_type = (spread_to.fire_stacks > fire_stacks) ? their_fire_status.type : fire_status.type
+ set_fire_stacks(firesplit, fire_type)
+ spread_to.set_fire_stacks(firesplit, fire_type)
+ return
+
+ adjust_fire_stacks(-fire_stacks / 2, fire_status.type)
+ spread_to.adjust_fire_stacks(fire_stacks, fire_status.type)
+ if(spread_to.ignite_mob())
+ message_admins("[key_name(src)] bumped into [key_name(spread_to)] and set them on fire.")
+ return
+
+ if(!their_fire_status || !their_fire_status.on_fire)
+ return
+
+ spread_to.adjust_fire_stacks(-spread_to.fire_stacks / 2, their_fire_status.type)
+ adjust_fire_stacks(spread_to.fire_stacks, their_fire_status.type)
+ ignite_mob()
+
+/**
+ * Gets the fire overlay to use for this mob
+ *
+ * Args:
+ * * stacks: Current amount of fire_stacks
+ * * on_fire: If we're lit on fire
+ *
+ * Return a mutable appearance, the overlay that will be applied.
+ */
+
+// Gets the correct icon_state for being on fire. See OnFire.dmi for the icons.
+/mob/living/proc/get_fire_icon_state()
+ return "generic"
+
+/mob/living/proc/get_fire_overlay(stacks, on_fire)
+ RETURN_TYPE(/mutable_appearance)
+ var/fire_icon = get_fire_icon_state()
+
+ if(!GLOB.fire_appearances[fire_icon])
+ var/mutable_appearance/new_fire_overlay = mutable_appearance(
+ 'icons/mob/OnFire.dmi',
+ fire_icon,
+ FIRE_LAYER,
+ appearance_flags = RESET_COLOR|KEEP_APART,
+ )
+ GLOB.fire_appearances[fire_icon] = new_fire_overlay
+
+ return GLOB.fire_appearances[fire_icon]
+
+/**
+ * Handles effects happening when mob is on normal fire
+ *
+ * Vars:
+ * * seconds_per_tick
+ * * times_fired
+ * * fire_handler: Current fire status effect that called the proc
+ */
+
+/mob/living/proc/on_fire_stack(seconds_per_tick, datum/status_effect/fire_handler/fire_stacks/fire_handler)
+ return
+
+//Mobs on Fire end
diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm
index bcf2fc1b69..10b1d3ea0e 100644
--- a/code/modules/mob/living/living_movement.dm
+++ b/code/modules/mob/living/living_movement.dm
@@ -55,7 +55,7 @@ default behaviour is:
var/mob/living/tmob = AM
//Even if we don't push/swap places, we "touched" them, so spread fire
- spread_fire(tmob)
+ spreadFire(tmob)
for(var/mob/living/M in range(tmob, 1))
if(tmob.pinned.len || ((M.pulling == tmob && ( tmob.restrained() && !( M.restrained() ) && M.stat == 0)) || locate(/obj/item/grab, tmob.grabbed_by.len)) )
diff --git a/code/modules/mob/living/silicon/robot/life.dm b/code/modules/mob/living/silicon/robot/life.dm
index 181ea0b478..8392fb1707 100644
--- a/code/modules/mob/living/silicon/robot/life.dm
+++ b/code/modules/mob/living/silicon/robot/life.dm
@@ -372,15 +372,15 @@
canmove = FALSE
return canmove
+/mob/living/silicon/robot/fire_act()
+ if(!on_fire) //Silicons don't gain stacks from hotspots, but hotspots can ignite them
+ ignite_mob()
+
/mob/living/silicon/robot/update_fire()
cut_overlay(image(icon = 'icons/mob/OnFire.dmi', icon_state = get_fire_icon_state()))
if(on_fire)
add_overlay(image(icon = 'icons/mob/OnFire.dmi', icon_state = get_fire_icon_state()))
-/mob/living/silicon/robot/fire_act()
- if(!on_fire) //Silicons don't gain stacks from hotspots, but hotspots can ignite them
- IgniteMob()
-
/mob/living/silicon/robot/handle_light()
if(lights_on)
set_light(integrated_light_power, 1, robot_light_col)
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index b063d114ff..ffeffd679c 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -28,6 +28,7 @@
var/sensor_type = 0 //VOREStation add - silicon omni "is sensor on or nah"
var/hudmode = null
+ fire_stack_decay_rate = -0.55
/mob/living/silicon/Initialize(mapload, is_decoy = FALSE)
. = ..()
diff --git a/code/modules/mob/living/simple_mob/defense.dm b/code/modules/mob/living/simple_mob/defense.dm
index d5d6bc8d1a..45ced044d8 100644
--- a/code/modules/mob/living/simple_mob/defense.dm
+++ b/code/modules/mob/living/simple_mob/defense.dm
@@ -176,17 +176,6 @@
. = 1 - .
. = min(., 1.0)
-
-// Fire stuff. Not really exciting at the moment.
-/mob/living/simple_mob/handle_fire()
- return
-/mob/living/simple_mob/update_fire()
- return
-/mob/living/simple_mob/IgniteMob()
- return
-/mob/living/simple_mob/ExtinguishMob()
- return
-
/mob/living/simple_mob/get_heat_protection()
. = heat_resist
. = 1 - . // Invert from 1 = immunity to 0 = immunity.
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/space/mouse_army.dm b/code/modules/mob/living/simple_mob/subtypes/animal/space/mouse_army.dm
index 8add800d27..d7577bda91 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/space/mouse_army.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/space/mouse_army.dm
@@ -184,7 +184,7 @@
visible_message(span_critical("\The [src]'s tank ruptures!"))
ruptured = 1
adjust_fire_stacks(2)
- IgniteMob()
+ ignite_mob()
return ..()
//Ammo Mouse
diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/hivebot/ranged_damage.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/hivebot/ranged_damage.dm
index 854e58ae12..956537ffc0 100644
--- a/code/modules/mob/living/simple_mob/subtypes/mechanical/hivebot/ranged_damage.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/hivebot/ranged_damage.dm
@@ -79,15 +79,13 @@
name = "ember"
icon = 'icons/effects/effects.dmi'
icon_state = "explosion_particle"
- modifier_type_to_apply = /datum/modifier/fire
- modifier_duration = 6 SECONDS // About 15 damage per stack, as Life() ticks every two seconds.
+ incendiary = 3
damage = 0
nodamage = TRUE
impact_effect_type = /obj/effect/temp_visual/impact_effect
hitsound_wall = 'sound/weapons/effects/searwall.ogg'
-
// Close to mid-ranged shooter that arcs over other things, ideal if allies are in front of it.
// Difference from siege hivebots is that siege hivebots have limited charges for their attacks, are very long range, and
// the projectiles have an AoE component, where as backline hivebots do not.
diff --git a/code/modules/mob/living/simple_mob/subtypes/slime/xenobio/subtypes.dm b/code/modules/mob/living/simple_mob/subtypes/slime/xenobio/subtypes.dm
index eab5013eb9..dd866959c2 100644
--- a/code/modules/mob/living/simple_mob/subtypes/slime/xenobio/subtypes.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/slime/xenobio/subtypes.dm
@@ -50,7 +50,7 @@
to_chat(L, span_danger("You've been burned by \the [src]!"))
L.adjust_fire_stacks(1)
if(prob(12))
- L.IgniteMob()
+ L.ignite_mob()
/mob/living/simple_mob/slime/xenobio/blue
desc = "This slime produces 'cryotoxin' and uses it against their foes. Very deadly to other slimes."
diff --git a/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm b/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
index b8b9466b28..a1f343133f 100644
--- a/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/vore/bigdragon.dm
@@ -887,7 +887,7 @@ I think I covered everything.
if(iscarbon(target))
var/mob/living/carbon/M = target
M.adjust_fire_stacks(fire_stacks)
- M.IgniteMob()
+ M.ignite_mob()
else
. = ..()
@@ -1010,7 +1010,7 @@ I think I covered everything.
for(var/RG in to_inject)
if(!P.reagents.has_reagent(RG))
P.reagents.add_reagent(RG, 10)
- L.ExtinguishMob()
+ L.extinguish_mob()
return //Don't attack people if we're on help intent
return .=..()
diff --git a/code/modules/paperwork/paperplane.dm b/code/modules/paperwork/paperplane.dm
index 56766feb5f..bb8d46bd28 100644
--- a/code/modules/paperwork/paperplane.dm
+++ b/code/modules/paperwork/paperplane.dm
@@ -68,7 +68,7 @@
span_userdanger("You miss the [src] and accidentally light yourself on fire!"))
user.unEquip(P)
user.adjust_fire_stacks(1)
- user.IgniteMob()
+ user.ignite_mob()
return
if(!(in_range(user, src))) //to prevent issues as a result of telepathically lighting a paper
diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm
index f08afa01bb..9190a5b912 100644
--- a/code/modules/projectiles/projectile/bullets.dm
+++ b/code/modules/projectiles/projectile/bullets.dm
@@ -351,10 +351,8 @@
/obj/item/projectile/bullet/incendiary/flamethrower/tiny
damage = 2
- incendiary = 0
+ incendiary = 10
flammability = 2
- modifier_type_to_apply = /datum/modifier/fire/stack_managed/weak
- modifier_duration = 20 SECONDS
range = 6
agony = 0
hud_state = "flame"
diff --git a/code/modules/reagents/reagents/core.dm b/code/modules/reagents/reagents/core.dm
index df0934458d..3a3fd8b381 100644
--- a/code/modules/reagents/reagents/core.dm
+++ b/code/modules/reagents/reagents/core.dm
@@ -309,7 +309,7 @@
// Then extinguish people on fire.
var/needed = max(0,L.fire_stacks) * 5
if(amount > needed)
- L.ExtinguishMob()
+ L.extinguish_mob()
L.water_act(amount / 25) // Div by 25, as water_act multiplies it by 5 in order to calculate firestack modification.
remove_self(needed)
// Put out cigarettes if splashed.
diff --git a/code/modules/reagents/reagents/other.dm b/code/modules/reagents/reagents/other.dm
index f5fe6fa8f5..fb18605be9 100644
--- a/code/modules/reagents/reagents/other.dm
+++ b/code/modules/reagents/reagents/other.dm
@@ -212,7 +212,7 @@
M.SetConfused(0)
M.SetSleeping(0)
M.radiation = 0
- M.ExtinguishMob()
+ M.extinguish_mob()
M.fire_stacks = 0
M.add_chemical_effect(CE_ANTIBIOTIC, ANTIBIO_SUPER)
M.add_chemical_effect(CE_STABLE, 15)
diff --git a/code/modules/reagents/reagents/other_vr.dm b/code/modules/reagents/reagents/other_vr.dm
index f1cfa6ec97..4140b57a48 100644
--- a/code/modules/reagents/reagents/other_vr.dm
+++ b/code/modules/reagents/reagents/other_vr.dm
@@ -102,7 +102,7 @@
S.visible_message(span_warning("[S]'s flesh sizzles where the foam touches it!"), span_danger("Your flesh burns in the foam!"))
if(istype(M))
M.adjust_fire_stacks(-reac_volume)
- M.ExtinguishMob()
+ M.extinguish_mob()
/datum/reagent/liquid_protean
name = REAGENT_LIQUIDPROTEAN
diff --git a/code/modules/reagents/reagents/toxins.dm b/code/modules/reagents/reagents/toxins.dm
index 6cd6725165..c7e80c3b3b 100644
--- a/code/modules/reagents/reagents/toxins.dm
+++ b/code/modules/reagents/reagents/toxins.dm
@@ -135,7 +135,7 @@
if(prob(10))
to_chat(M, span_critical("You feel something boiling within you!"))
spawn(rand(30, 60))
- M.IgniteMob()
+ M.ignite_mob()
/datum/reagent/toxin/lead
name = REAGENT_LEAD
@@ -584,7 +584,7 @@
to_chat(M, span_warning("Your veins feel like they're on fire!"))
M.adjust_fire_stacks(0.1)
else if(prob(5))
- M.IgniteMob()
+ M.ignite_mob()
to_chat(M, span_critical("Some of your veins rupture, the exposed blood igniting!"))
/datum/reagent/condensedcapsaicin/venom
diff --git a/code/modules/spells/targeted/ethereal_jaunt.dm b/code/modules/spells/targeted/ethereal_jaunt.dm
index 09b9a633df..705845e7e0 100644
--- a/code/modules/spells/targeted/ethereal_jaunt.dm
+++ b/code/modules/spells/targeted/ethereal_jaunt.dm
@@ -30,7 +30,7 @@
animation.plane = MOB_PLANE
animation.layer = ABOVE_MOB_LAYER
animation.master = holder
- target.ExtinguishMob()
+ target.extinguish_mob()
if(target.buckled)
target.buckled.unbuckle_mob( target, TRUE)
jaunt_disappear(animation, target)
diff --git a/icons/effects/particles/bonfire.dmi b/icons/effects/particles/bonfire.dmi
new file mode 100644
index 0000000000..e8e2e36346
Binary files /dev/null and b/icons/effects/particles/bonfire.dmi differ
diff --git a/icons/effects/particles/echo.dmi b/icons/effects/particles/echo.dmi
new file mode 100644
index 0000000000..60a243a8a7
Binary files /dev/null and b/icons/effects/particles/echo.dmi differ
diff --git a/icons/effects/particles/generic.dmi b/icons/effects/particles/generic.dmi
new file mode 100644
index 0000000000..41776efdbf
Binary files /dev/null and b/icons/effects/particles/generic.dmi differ
diff --git a/icons/effects/particles/goop.dmi b/icons/effects/particles/goop.dmi
new file mode 100644
index 0000000000..673c1a7ad5
Binary files /dev/null and b/icons/effects/particles/goop.dmi differ
diff --git a/icons/effects/particles/notes/note.dmi b/icons/effects/particles/notes/note.dmi
new file mode 100644
index 0000000000..766a29e6a8
Binary files /dev/null and b/icons/effects/particles/notes/note.dmi differ
diff --git a/icons/effects/particles/notes/note_harm.dmi b/icons/effects/particles/notes/note_harm.dmi
new file mode 100644
index 0000000000..0fe386e1ed
Binary files /dev/null and b/icons/effects/particles/notes/note_harm.dmi differ
diff --git a/icons/effects/particles/notes/note_holy.dmi b/icons/effects/particles/notes/note_holy.dmi
new file mode 100644
index 0000000000..f817e8253d
Binary files /dev/null and b/icons/effects/particles/notes/note_holy.dmi differ
diff --git a/icons/effects/particles/notes/note_light.dmi b/icons/effects/particles/notes/note_light.dmi
new file mode 100644
index 0000000000..3474edebc2
Binary files /dev/null and b/icons/effects/particles/notes/note_light.dmi differ
diff --git a/icons/effects/particles/notes/note_null.dmi b/icons/effects/particles/notes/note_null.dmi
new file mode 100644
index 0000000000..14c941f9e4
Binary files /dev/null and b/icons/effects/particles/notes/note_null.dmi differ
diff --git a/icons/effects/particles/notes/note_sleepy.dmi b/icons/effects/particles/notes/note_sleepy.dmi
new file mode 100644
index 0000000000..21030914a9
Binary files /dev/null and b/icons/effects/particles/notes/note_sleepy.dmi differ
diff --git a/icons/effects/particles/pollen.dmi b/icons/effects/particles/pollen.dmi
new file mode 100644
index 0000000000..559c4d1846
Binary files /dev/null and b/icons/effects/particles/pollen.dmi differ
diff --git a/icons/effects/particles/smoke.dmi b/icons/effects/particles/smoke.dmi
new file mode 100644
index 0000000000..7e271e51f0
Binary files /dev/null and b/icons/effects/particles/smoke.dmi differ
diff --git a/icons/effects/particles/stink.dmi b/icons/effects/particles/stink.dmi
new file mode 100644
index 0000000000..29b92acbe6
Binary files /dev/null and b/icons/effects/particles/stink.dmi differ
diff --git a/icons/effects/particles/voidwalker.dmi b/icons/effects/particles/voidwalker.dmi
new file mode 100644
index 0000000000..621e468ac9
Binary files /dev/null and b/icons/effects/particles/voidwalker.dmi differ
diff --git a/modular_chomp/code/datums/crafting/items.dm b/modular_chomp/code/datums/crafting/items.dm
index 7390d425ce..45a6c2c0d8 100644
--- a/modular_chomp/code/datums/crafting/items.dm
+++ b/modular_chomp/code/datums/crafting/items.dm
@@ -173,7 +173,8 @@
/obj/item/material/sword/rapier/solar/apply_hit_effect(mob/living/target, mob/living/user, var/hit_zone)
. = ..()
- target.add_modifier(/datum/modifier/fire/weak, 12 SECONDS) //should be 12 damage?
+ target.adjust_fire_stacks(10)
+ target.ignite_mob()
//Icicle
/obj/item/gun/energy/icelauncher
diff --git a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/ants.dm b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/ants.dm
index 8dbee90bfe..fe4192d94c 100644
--- a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/ants.dm
+++ b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/ants.dm
@@ -360,7 +360,11 @@ ANT STRUCTURES
/obj/effect/ant_structure/trap/burn
icon_state = "burn_trap"
- modifiertype = /datum/modifier/fire/weak
+ //No modifier.
+
+/obj/effect/ant_structure/trap/burn/attack_mob(mob/living/L)
+ L.adjust_fire_stacks(5)
+ L.ignite_mob()
/obj/effect/ant_structure/trap/slowdown
icon_state = "slow_trap"
diff --git a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/oddities.dm b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/oddities.dm
index 84017a664a..895462a4ee 100644
--- a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/oddities.dm
+++ b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/animal/tyr/oddities.dm
@@ -175,7 +175,8 @@
/mob/living/simple_mob/animal/tyr/explode_beetle/apply_melee_effects(var/atom/A)
if(isliving(A))
var/mob/living/L = A
- L.add_modifier(/datum/modifier/fire, 3 SECONDS)
+ L.adjust_fire_stacks(3)
+ L.ignite_mob()
/mob/living/simple_mob/animal/tyr/glowing_beetle
name = "glowing beetle"
diff --git a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/humanoid/eclipse/heads.dm b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/humanoid/eclipse/heads.dm
index baac16eb8a..78ce5c7886 100644
--- a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/humanoid/eclipse/heads.dm
+++ b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/humanoid/eclipse/heads.dm
@@ -53,11 +53,16 @@
armor_penetration = 40 //Large pointy crystal
damage_type = BRUTE
check_armour = "bullet"
- modifier_type_to_apply = /datum/modifier/fire/weak
- modifier_duration = 0.05 MINUTE
range = 12
hud_state = "laser_sniper"
+/obj/item/projectile/energy/flamecrystal/on_hit(atom/target, blocked = 0, def_zone)
+ . = ..()
+ if(isliving(target))
+ var/mob/living/L = target
+ L.adjust_fire_stacks(5)
+ L.ignite_mob()
+
/obj/item/projectile/bullet/flamegun
use_submunitions = 1
only_submunitions = 1
diff --git a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/slimess/feral.dm b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/slimess/feral.dm
index 6fc0d2b184..8d26b98f9e 100644
--- a/modular_chomp/code/modules/mob/living/simple_mob/subtypes/slimess/feral.dm
+++ b/modular_chomp/code/modules/mob/living/simple_mob/subtypes/slimess/feral.dm
@@ -32,7 +32,8 @@
/mob/living/simple_mob/slime/feral/orange/apply_melee_effects(var/atom/A)
if(isliving(A))
var/mob/living/L = A
- L.add_modifier(/datum/modifier/fire, 5 SECONDS)
+ L.adjust_fire_stacks(5)
+ L.ignite_mob()
/mob/living/simple_mob/slime/feral/blue
diff --git a/modular_chomp/code/modules/projectiles/mob.dm b/modular_chomp/code/modules/projectiles/mob.dm
index ec5d2598a7..70a65a1712 100644
--- a/modular_chomp/code/modules/projectiles/mob.dm
+++ b/modular_chomp/code/modules/projectiles/mob.dm
@@ -74,11 +74,16 @@
armor_penetration = 15
my_chems = list(REAGENT_ID_FUEL, REAGENT_ID_MOLD)
flammability = 0.25
- modifier_type_to_apply = /datum/modifier/fire
- modifier_duration = 6 SECONDS
color = "#38b9ff"
speed = 3.2
+/obj/item/projectile/energy/blob/moth/on_hit(atom/target, blocked = 0, def_zone)
+ . = ..()
+ if(isliving(target))
+ var/mob/living/L = target
+ L.adjust_fire_stacks(10)
+ L.ignite_mob()
+
/obj/item/projectile/bullet/pistol/medium/ap/eclipse
ricochets = 1
ricochets_max = 8
diff --git a/modular_chomp/code/modules/reagents/reagents/food_drinks.dm b/modular_chomp/code/modules/reagents/reagents/food_drinks.dm
index 0b396ba5c6..267f60a96e 100644
--- a/modular_chomp/code/modules/reagents/reagents/food_drinks.dm
+++ b/modular_chomp/code/modules/reagents/reagents/food_drinks.dm
@@ -650,7 +650,7 @@
/datum/reagent/ethanol/mauna_loa/affect_ingest(var/mob/living/carbon/M, var/alien, var/removed)
if(prob(10))
M.adjust_fire_stacks(5*removed)
- M.IgniteMob()
+ M.ignite_mob()
/datum/reagent/ethanol/hiveminderaser
name = REAGENT_HIVEMINDERASER
diff --git a/modular_chomp/code/modules/reagents/reagents/medicine.dm b/modular_chomp/code/modules/reagents/reagents/medicine.dm
index 0b870fed48..b35048a50c 100644
--- a/modular_chomp/code/modules/reagents/reagents/medicine.dm
+++ b/modular_chomp/code/modules/reagents/reagents/medicine.dm
@@ -339,7 +339,7 @@
M.remove_a_modifier_of_type(/datum/modifier/grievous_wounds)
M.remove_a_modifier_of_type(/datum/modifier/deep_wounds)
M.remove_a_modifier_of_type(/datum/modifier/hivebot_weaken)
- M.remove_a_modifier_of_type(/datum/modifier/fire)
+ M.extinguish_mob()
M.remove_a_modifier_of_type(/datum/modifier/berserk_exhaustion)
M.remove_a_modifier_of_type(/datum/modifier/entangled)
M.remove_a_modifier_of_type(/datum/modifier/wizfire)
diff --git a/vorestation.dme b/vorestation.dme
index a4aea84647..6a1f84bbee 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -50,6 +50,7 @@
#include "code\__defines\admin_ch.dm"
#include "code\__defines\admin_verb.dm"
#include "code\__defines\airlock_control.dm"
+#include "code\__defines\alerts.dm"
#include "code\__defines\ammunition.dm"
#include "code\__defines\appearance.dm"
#include "code\__defines\assemblies.dm"
@@ -141,6 +142,7 @@
#include "code\__defines\overlay_ch.dm"
#include "code\__defines\overmap.dm"
#include "code\__defines\paicard.dm"
+#include "code\__defines\particles.dm"
#include "code\__defines\pda.dm"
#include "code\__defines\persistence.dm"
#include "code\__defines\phobias.dm"
@@ -188,6 +190,7 @@
#include "code\__defines\stack_trace.dm"
#include "code\__defines\stat_tracking.dm"
#include "code\__defines\statpanel.dm"
+#include "code\__defines\status_effects.dm"
#include "code\__defines\subsystems.dm"
#include "code\__defines\supply.dm"
#include "code\__defines\talksounds.dm"
@@ -525,6 +528,7 @@
#include "code\controllers\subsystems\processing\instruments.dm"
#include "code\controllers\subsystems\processing\obj.dm"
#include "code\controllers\subsystems\processing\obj_tab_items.dm"
+#include "code\controllers\subsystems\processing\priority_effects.dm"
#include "code\controllers\subsystems\processing\processing.dm"
#include "code\controllers\subsystems\processing\projectiles.dm"
#include "code\controllers\subsystems\processing\turfs.dm"
@@ -833,6 +837,10 @@
#include "code\datums\repositories\repository.dm"
#include "code\datums\repositories\unique.dm"
#include "code\datums\roundstats\_defines_local.dm"
+#include "code\datums\status_effects\_status_effect.dm"
+#include "code\datums\status_effects\_status_effect_helpers.dm"
+#include "code\datums\status_effects\grouped_effect.dm"
+#include "code\datums\status_effects\debuffs\fire_stacks.dm"
#include "code\datums\supplypacks\atmospherics.dm"
#include "code\datums\supplypacks\contraband.dm"
#include "code\datums\supplypacks\costumes.dm"
@@ -1468,8 +1476,10 @@
#include "code\game\objects\effects\misc.dm"
#include "code\game\objects\effects\motion_echo.dm"
#include "code\game\objects\effects\overlays.dm"
+#include "code\game\objects\effects\particle_holder.dm"
#include "code\game\objects\effects\portals.dm"
#include "code\game\objects\effects\semirandom_mobs_vr.dm"
+#include "code\game\objects\effects\shared_particle_holder.dm"
#include "code\game\objects\effects\spiders.dm"
#include "code\game\objects\effects\spiders_vr.dm"
#include "code\game\objects\effects\step_triggers.dm"
@@ -1510,6 +1520,9 @@
#include "code\game\objects\effects\map_effects\radiation_emitter.dm"
#include "code\game\objects\effects\map_effects\screen_shaker.dm"
#include "code\game\objects\effects\map_effects\sound_emitter.dm"
+#include "code\game\objects\effects\particles\fire.dm"
+#include "code\game\objects\effects\particles\smoke.dm"
+#include "code\game\objects\effects\particles\water.dm"
#include "code\game\objects\effects\prop\columnblast.dm"
#include "code\game\objects\effects\prop\snake.dm"
#include "code\game\objects\effects\spawners\bombspawner.dm"
@@ -3278,7 +3291,6 @@
#include "code\modules\mob\_modifiers\cloning.dm"
#include "code\modules\mob\_modifiers\crusher_mark.dm"
#include "code\modules\mob\_modifiers\feysight.dm"
-#include "code\modules\mob\_modifiers\fire.dm"
#include "code\modules\mob\_modifiers\horror.dm"
#include "code\modules\mob\_modifiers\medical.dm"
#include "code\modules\mob\_modifiers\modifiers.dm"
@@ -3332,6 +3344,7 @@
#include "code\modules\mob\living\living_defense.dm"
#include "code\modules\mob\living\living_defines.dm"
#include "code\modules\mob\living\living_defines_vr.dm"
+#include "code\modules\mob\living\living_fire.dm"
#include "code\modules\mob\living\living_movement.dm"
#include "code\modules\mob\living\living_powers.dm"
#include "code\modules\mob\living\living_vr.dm"