diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index bd20ba610c1..398e1bebd4c 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -25,6 +25,7 @@
#define COLOR_VIVID_RED "#FF3232"
#define COLOR_LIGHT_GRAYISH_RED "#E4C7C5"
#define COLOR_SOFT_RED "#FA8282"
+#define COLOR_CULT_RED "#960000"
#define COLOR_BUBBLEGUM_RED "#950A0A"
#define COLOR_YELLOW "#FFFF00"
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 80fdfe933b5..413de14d122 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -768,3 +768,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// radiation processing as if this wasn't already irradiated.
/// Basically, without this, COMSIG_IN_RANGE_OF_IRRADIATION won't fire once the object is irradiated.
#define TRAIT_BYPASS_EARLY_IRRADIATED_CHECK "radiation_bypass_early_irradiated_check"
+
+// Traits to heal for
+
+/// This mob heals from carp rifts.
+#define TRAIT_HEALS_FROM_CARP_RIFTS "heals_from_carp_rifts"
+
+/// This mob heals from cult pylons.
+#define TRAIT_HEALS_FROM_CULT_PYLONS "heals_from_cult_pylons"
diff --git a/code/controllers/subsystem/processing/aura_healing.dm b/code/controllers/subsystem/processing/aura_healing.dm
new file mode 100644
index 00000000000..ad3fa6f4278
--- /dev/null
+++ b/code/controllers/subsystem/processing/aura_healing.dm
@@ -0,0 +1,5 @@
+/// The subsystem used to tick [/datum/component/aura_healing] instances.
+PROCESSING_SUBSYSTEM_DEF(aura_healing)
+ name = "Aura Healing"
+ flags = SS_NO_INIT | SS_BACKGROUND | SS_KEEP_TIMING
+ wait = 0.3 SECONDS
diff --git a/code/datums/components/aura_healing.dm b/code/datums/components/aura_healing.dm
new file mode 100644
index 00000000000..9127d8e4680
--- /dev/null
+++ b/code/datums/components/aura_healing.dm
@@ -0,0 +1,147 @@
+#define HEAL_EFFECT_COOLDOWN (1 SECONDS)
+
+/// Applies healing to those in the area.
+/// Will provide them with an alert while they're in range, as well as
+/// give them a healing particle.
+/// Can be applied to those only with a trait conditionally.
+/datum/component/aura_healing
+ /// The range of which to heal
+ var/range
+
+ /// Whether or not you must be a visible object of the parent
+ var/requires_visibility = TRUE
+
+ /// Brute damage to heal over a second
+ var/brute_heal = 0
+
+ /// Burn damage to heal over a second
+ var/burn_heal = 0
+
+ /// Toxin damage to heal over a second
+ var/toxin_heal = 0
+
+ /// Suffocation damage to heal over a second
+ var/suffocation_heal = 0
+
+ /// Stamina damage to heal over a second
+ var/stamina_heal = 0
+
+ /// Amount of cloning damage to heal over a second
+ var/clone_heal = 0
+
+ /// Amount of blood to heal over a second
+ var/blood_heal = 0
+
+ /// Map of organ (such as ORGAN_SLOT_BRAIN) to damage heal over a second
+ var/list/organ_healing = null
+
+ /// Amount of damage to heal on simple mobs over a second
+ var/simple_heal = 0
+
+ /// Trait to limit healing to, if set
+ var/limit_to_trait = null
+
+ /// The color to give the healing visual
+ var/healing_color = COLOR_GREEN
+
+ /// A list of being healed to active alerts
+ var/list/current_alerts = list()
+
+ COOLDOWN_DECLARE(last_heal_effect_time)
+
+/datum/component/aura_healing/Initialize(
+ range,
+ requires_visibility = TRUE,
+ brute_heal = 0,
+ burn_heal = 0,
+ toxin_heal = 0,
+ suffocation_heal = 0,
+ stamina_heal = 0,
+ clone_heal = 0,
+ blood_heal = 0,
+ organ_healing = null,
+ simple_heal = 0,
+ limit_to_trait = null,
+ healing_color = COLOR_GREEN,
+)
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ START_PROCESSING(SSaura_healing, src)
+
+ src.range = range
+ src.requires_visibility = requires_visibility
+ src.brute_heal = brute_heal
+ src.burn_heal = burn_heal
+ src.toxin_heal = toxin_heal
+ src.suffocation_heal = suffocation_heal
+ src.stamina_heal = stamina_heal
+ src.clone_heal = clone_heal
+ src.blood_heal = blood_heal
+ src.organ_healing = organ_healing
+ src.simple_heal = simple_heal
+ src.limit_to_trait = limit_to_trait
+ src.healing_color = healing_color
+
+/datum/component/aura_healing/Destroy(force, silent)
+ STOP_PROCESSING(SSaura_healing, src)
+
+ QDEL_LIST_ASSOC_VAL(current_alerts)
+
+ return ..()
+
+/datum/component/aura_healing/process(delta_time)
+ var/should_show_effect = COOLDOWN_FINISHED(src, last_heal_effect_time)
+ if (should_show_effect)
+ COOLDOWN_START(src, last_heal_effect_time, HEAL_EFFECT_COOLDOWN)
+
+ var/list/remove_alerts_from = current_alerts.Copy()
+
+ var/alert_category = "aura_healing_[REF(src)]"
+
+ for (var/mob/living/candidate in (requires_visibility ? view(range, parent) : range(range, parent)))
+ if (!isnull(limit_to_trait) && !HAS_TRAIT(candidate, limit_to_trait))
+ continue
+
+ remove_alerts_from -= candidate
+
+ if (!(candidate in current_alerts))
+ var/atom/movable/screen/alert/aura_healing/alert = candidate.throw_alert(alert_category, /atom/movable/screen/alert/aura_healing, new_master = parent)
+ alert.desc = "You are being healed by [parent]."
+ current_alerts[candidate] = alert
+
+ if (should_show_effect && candidate.health < candidate.maxHealth)
+ new /obj/effect/temp_visual/heal(get_turf(candidate), healing_color)
+
+ if (iscarbon(candidate) || issilicon(candidate))
+ candidate.adjustBruteLoss(-brute_heal * delta_time, updating_health = FALSE)
+ candidate.adjustFireLoss(-burn_heal * delta_time, updating_health = FALSE)
+
+ if (iscarbon(candidate))
+ // Toxin healing is forced for slime people
+ candidate.adjustToxLoss(-toxin_heal * delta_time, updating_health = FALSE, forced = TRUE)
+
+ candidate.adjustOxyLoss(-suffocation_heal * delta_time, updating_health = FALSE)
+ candidate.adjustStaminaLoss(-stamina_heal * delta_time, updating_health = FALSE)
+ candidate.adjustCloneLoss(-clone_heal * delta_time, updating_health = FALSE)
+
+ for (var/organ in organ_healing)
+ candidate.adjustOrganLoss(organ, -organ_healing[organ] * delta_time)
+ else if (isanimal(candidate))
+ var/mob/living/simple_animal/simple_candidate = candidate
+ simple_candidate.adjustHealth(-simple_heal * delta_time, updating_health = FALSE)
+
+ if (candidate.blood_volume < BLOOD_VOLUME_NORMAL)
+ candidate.blood_volume += blood_heal * delta_time
+
+ candidate.updatehealth()
+
+ for (var/mob/remove_alert_from as anything in remove_alerts_from)
+ remove_alert_from.clear_alert(alert_category)
+ current_alerts -= remove_alert_from
+
+/atom/movable/screen/alert/aura_healing
+ name = "Aura Healing"
+ icon_state = "template"
+
+#undef HEAL_EFFECT_COOLDOWN
diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm
index 3111a40b95c..b667000177d 100644
--- a/code/datums/status_effects/buffs.dm
+++ b/code/datums/status_effects/buffs.dm
@@ -212,10 +212,30 @@
tick_interval = 25
examine_text = "They seem to have an aura of healing and helpfulness about them."
alert_type = null
+
+ var/datum/component/aura_healing/aura_healing
var/hand
var/deathTick = 0
/datum/status_effect/hippocratic_oath/on_apply()
+ var/static/list/organ_healing = list(
+ ORGAN_SLOT_BRAIN = 1.4,
+ )
+
+ aura_healing = owner.AddComponent( \
+ /datum/component/aura_healing, \
+ range = 7, \
+ brute_heal = 1.4, \
+ burn_heal = 1.4, \
+ toxin_heal = 1.4, \
+ suffocation_heal = 1.4, \
+ stamina_heal = 1.4, \
+ clone_heal = 0.4, \
+ simple_heal = 1.4, \
+ organ_healing = organ_healing, \
+ healing_color = "#375637", \
+ )
+
//Makes the user passive, it's in their oath not to harm!
ADD_TRAIT(owner, TRAIT_PACIFISM, HIPPOCRATIC_OATH_TRAIT)
var/datum/atom_hud/H = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED]
@@ -223,6 +243,7 @@
return ..()
/datum/status_effect/hippocratic_oath/on_remove()
+ QDEL_NULL(aura_healing)
REMOVE_TRAIT(owner, TRAIT_PACIFISM, HIPPOCRATIC_OATH_TRAIT)
var/datum/atom_hud/H = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED]
H.remove_hud_from(owner)
@@ -273,24 +294,6 @@
itemUser.adjustStaminaLoss(-1.5)
itemUser.adjustOrganLoss(ORGAN_SLOT_BRAIN, -1.5)
itemUser.adjustCloneLoss(-0.5) //Becasue apparently clone damage is the bastion of all health
- //Heal all those around you, unbiased
- for(var/mob/living/L in view(7, owner))
- if(L.health < L.maxHealth)
- new /obj/effect/temp_visual/heal(get_turf(L), "#375637")
- if(iscarbon(L))
- L.adjustBruteLoss(-3.5)
- L.adjustFireLoss(-3.5)
- L.adjustToxLoss(-3.5, forced = TRUE) //Because Slime People are people too
- L.adjustOxyLoss(-3.5)
- L.adjustStaminaLoss(-3.5)
- L.adjustOrganLoss(ORGAN_SLOT_BRAIN, -3.5)
- L.adjustCloneLoss(-1) //Becasue apparently clone damage is the bastion of all health
- else if(issilicon(L))
- L.adjustBruteLoss(-3.5)
- L.adjustFireLoss(-3.5)
- else if(isanimal(L))
- var/mob/living/simple_animal/SM = L
- SM.adjustHealth(-3.5, forced = TRUE)
/datum/status_effect/hippocratic_oath/proc/consume_owner()
owner.visible_message(span_notice("[owner]'s soul is absorbed into the rod, relieving the previous snake of its duty."))
diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm
index 43db3103326..5ed6656de6e 100644
--- a/code/modules/antagonists/cult/cult.dm
+++ b/code/modules/antagonists/cult/cult.dm
@@ -69,6 +69,13 @@
if(cult_team.blood_target && cult_team.blood_target_image && current.client)
current.client.images += cult_team.blood_target_image
+ ADD_TRAIT(current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
+
+/datum/antagonist/cult/on_removal()
+ REMOVE_TRAIT(owner.current, TRAIT_HEALS_FROM_CULT_PYLONS, CULT_TRAIT)
+
+ return ..()
+
/datum/antagonist/cult/get_preview_icon()
var/icon/icon = render_preview_outfit(preview_outfit)
diff --git a/code/modules/antagonists/cult/cult_structures.dm b/code/modules/antagonists/cult/cult_structures.dm
index d74d4bcac8b..0c22bdd5ea2 100644
--- a/code/modules/antagonists/cult/cult_structures.dm
+++ b/code/modules/antagonists/cult/cult_structures.dm
@@ -179,13 +179,25 @@
light_color = COLOR_SOFT_RED
break_sound = 'sound/effects/glassbr2.ogg'
break_message = "The blood-red crystal falls to the floor and shatters!"
- var/heal_delay = 25
var/last_heal = 0
var/corrupt_delay = 50
var/last_corrupt = 0
/obj/structure/destructible/cult/pylon/Initialize(mapload)
. = ..()
+
+ AddComponent( \
+ /datum/component/aura_healing, \
+ range = 5, \
+ brute_heal = 0.4, \
+ burn_heal = 0.4, \
+ blood_heal = 0.4, \
+ simple_heal = 1.2, \
+ requires_visibility = FALSE, \
+ limit_to_trait = TRAIT_HEALS_FROM_CULT_PYLONS, \
+ healing_color = COLOR_CULT_RED, \
+ )
+
START_PROCESSING(SSfastprocess, src)
/obj/structure/destructible/cult/pylon/Destroy()
@@ -195,23 +207,6 @@
/obj/structure/destructible/cult/pylon/process()
if(!anchored)
return
- if(last_heal <= world.time)
- last_heal = world.time + heal_delay
- for(var/mob/living/L in range(5, src))
- if(IS_CULTIST(L) || isshade(L) || isconstruct(L))
- if(L.health != L.maxHealth)
- new /obj/effect/temp_visual/heal(get_turf(src), "#960000")
- if(ishuman(L))
- L.adjustBruteLoss(-1, 0)
- L.adjustFireLoss(-1, 0)
- L.updatehealth()
- if(isshade(L) || isconstruct(L))
- var/mob/living/simple_animal/M = L
- if(M.health < M.maxHealth)
- M.adjustHealth(-3)
- if(ishuman(L) && L.blood_volume < BLOOD_VOLUME_NORMAL)
- L.blood_volume += 1.0
- CHECK_TICK
if(last_corrupt <= world.time)
var/list/validturfs = list()
var/list/cultturfs = list()
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index 9220aa12b2f..6726d69f697 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -48,6 +48,7 @@
/mob/living/simple_animal/hostile/construct/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_flying)
+ ADD_TRAIT(src, TRAIT_HEALS_FROM_CULT_PYLONS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
var/spellnum = 1
for(var/spell in construct_spells)
diff --git a/code/modules/mob/living/simple_animal/hostile/carp.dm b/code/modules/mob/living/simple_animal/hostile/carp.dm
index 7b47716e271..1559ebaba32 100644
--- a/code/modules/mob/living/simple_animal/hostile/carp.dm
+++ b/code/modules/mob/living/simple_animal/hostile/carp.dm
@@ -73,6 +73,7 @@
set_greyscale(new_config=/datum/greyscale_config/carp)
carp_randomify(rarechance)
. = ..()
+ ADD_TRAIT(src, TRAIT_HEALS_FROM_CARP_RIFTS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
add_cell_sample()
if(ai_controller)
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
index 1fc2a47cbcc..b2a8ddcd703 100644
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
@@ -568,6 +568,15 @@
/obj/structure/carp_rift/Initialize(mapload)
. = ..()
+
+ AddComponent( \
+ /datum/component/aura_healing, \
+ range = 0, \
+ simple_heal = 5, \
+ limit_to_trait = TRAIT_HEALS_FROM_CARP_RIFTS, \
+ healing_color = COLOR_BLUE, \
+ )
+
START_PROCESSING(SSobj, src)
// Carp rifts always take heavy explosion damage. Discourages the use of maxcaps
@@ -598,13 +607,6 @@
return ..()
/obj/structure/carp_rift/process(delta_time)
- // Heal carp on our loc.
- for(var/mob/living/simple_animal/hostile/hostilehere in loc)
- if("carp" in hostilehere.faction)
- hostilehere.adjustHealth(-5 * delta_time)
- var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal(get_turf(hostilehere))
- H.color = "#0000FF"
-
// If we're fully charged, just start mass spawning carp and move around.
if(charge_state == CHARGE_COMPLETED)
if(DT_PROB(1.25, delta_time))
diff --git a/code/modules/mob/living/simple_animal/shade.dm b/code/modules/mob/living/simple_animal/shade.dm
index 8b9038cb730..b95bdf99b02 100644
--- a/code/modules/mob/living/simple_animal/shade.dm
+++ b/code/modules/mob/living/simple_animal/shade.dm
@@ -37,6 +37,7 @@
/mob/living/simple_animal/shade/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_flying)
+ ADD_TRAIT(src, TRAIT_HEALS_FROM_CULT_PYLONS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
diff --git a/tgstation.dme b/tgstation.dme
index d6043364841..ea700f35bae 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -462,6 +462,7 @@
#include "code\controllers\subsystem\processing\ai_basic_avoidance.dm"
#include "code\controllers\subsystem\processing\ai_behaviors.dm"
#include "code\controllers\subsystem\processing\ai_movement.dm"
+#include "code\controllers\subsystem\processing\aura_healing.dm"
#include "code\controllers\subsystem\processing\clock_component.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\fields.dm"
@@ -605,6 +606,7 @@
#include "code\datums\components\area_sound_manager.dm"
#include "code\datums\components\areabound.dm"
#include "code\datums\components\armor_plate.dm"
+#include "code\datums\components\aura_healing.dm"
#include "code\datums\components\bakeable.dm"
#include "code\datums\components\beetlejuice.dm"
#include "code\datums\components\bloodysoles.dm"