diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm
new file mode 100644
index 00000000000..9f4819de797
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_guardian.dm
@@ -0,0 +1,7 @@
+/// Sent when a guardian is manifested
+#define COMSIG_GUARDIAN_MANIFESTED "guardian_manifested"
+/// Sent when a guardian is recalled
+#define COMSIG_GUARDIAN_RECALLED "guardian_recalled"
+
+/// Sent when an assassin guardian is forced to exit stealth
+#define COMSIG_GUARDIAN_ASSASSIN_REVEALED "guardian_assassin_revealed"
diff --git a/code/__DEFINES/guardian_defines.dm b/code/__DEFINES/guardian_defines.dm
index e7961368fee..ae2c3175b4a 100644
--- a/code/__DEFINES/guardian_defines.dm
+++ b/code/__DEFINES/guardian_defines.dm
@@ -3,5 +3,28 @@
#define GUARDIAN_THEME_CARP "carp"
#define GUARDIAN_THEME_MINER "miner"
-#define GUARDIAN_COLOR_LAYER 1
-#define GUARDIAN_TOTAL_LAYERS 1
+#define GUARDIAN_MAGIC "magic"
+#define GUARDIAN_TECH "tech"
+
+#define GUARDIAN_ASSASSIN "assassin"
+#define GUARDIAN_CHARGER "charger"
+#define GUARDIAN_DEXTROUS "dextrous"
+#define GUARDIAN_EXPLOSIVE "explosive"
+#define GUARDIAN_GASEOUS "gaseous"
+#define GUARDIAN_GRAVITOKINETIC "gravitokinetic"
+#define GUARDIAN_LIGHTNING "lightning"
+#define GUARDIAN_PROTECTOR "protector"
+#define GUARDIAN_RANGED "ranged"
+#define GUARDIAN_STANDARD "standard"
+#define GUARDIAN_SUPPORT "support"
+
+/// List of all guardians currently extant
+GLOBAL_LIST_EMPTY(parasites)
+
+/// Assoc list of guardian theme singletons
+GLOBAL_LIST_INIT(guardian_themes, list(
+ GUARDIAN_THEME_TECH = new /datum/guardian_fluff/tech,
+ GUARDIAN_THEME_MAGIC = new /datum/guardian_fluff,
+ GUARDIAN_THEME_CARP = new /datum/guardian_fluff/carp,
+ GUARDIAN_THEME_MINER = new /datum/guardian_fluff/miner,
+))
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 5676a6aaa8e..e9963ca1f6a 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -188,7 +188,7 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list(
#define isregalrat(A) (istype(A, /mob/living/basic/regal_rat))
-#define isguardian(A) (istype(A, /mob/living/simple_animal/hostile/guardian))
+#define isguardian(A) (istype(A, /mob/living/basic/guardian))
#define ismegafauna(A) (istype(A, /mob/living/simple_animal/hostile/megafauna))
diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm
index 76ab9ccf841..ab8ff969e30 100644
--- a/code/__DEFINES/span.dm
+++ b/code/__DEFINES/span.dm
@@ -23,6 +23,7 @@
#define span_bold(str) ("" + str + "")
#define span_boldannounce(str) ("" + str + "")
#define span_bolddanger(str) ("" + str + "")
+#define span_bolditalic(str) ("" + str + "")
#define span_boldnicegreen(str) ("" + str + "")
#define span_boldnotice(str) ("" + str + "")
#define span_boldwarning(str) ("" + str + "")
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index d72ca6fb5c9..1e775dd49ae 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -10,6 +10,7 @@ GLOBAL_LIST_EMPTY(stealthminID) //reference list with IDs that store ckeys, for
GLOBAL_LIST_INIT(abstract_mob_types, list(
/mob/living/basic/blob_minion,
/mob/living/basic/construct,
+ /mob/living/basic/guardian,
/mob/living/basic/heretic_summon,
/mob/living/basic/mining,
/mob/living/basic/pet,
@@ -26,7 +27,6 @@ GLOBAL_LIST_INIT(abstract_mob_types, list(
/mob/living/simple_animal/bot,
/mob/living/simple_animal/hostile/asteroid/elite,
/mob/living/simple_animal/hostile/asteroid,
- /mob/living/simple_animal/hostile/guardian,
/mob/living/simple_animal/hostile/megafauna,
/mob/living/simple_animal/hostile/mimic, // Cannot exist if spawned without being passed an item reference
/mob/living/simple_animal/hostile/retaliate,
diff --git a/code/_globalvars/lists/names.dm b/code/_globalvars/lists/names.dm
index b2662b5e804..52af61db88a 100644
--- a/code/_globalvars/lists/names.dm
+++ b/code/_globalvars/lists/names.dm
@@ -25,6 +25,9 @@ GLOBAL_LIST_INIT(megacarp_first_names, world.file2list("strings/names/megacarp1.
GLOBAL_LIST_INIT(megacarp_last_names, world.file2list("strings/names/megacarp2.txt"))
GLOBAL_LIST_INIT(cyberauth_names, world.file2list("strings/names/cyberauth.txt"))
GLOBAL_LIST_INIT(syndicate_monkey_names, world.file2list("strings/names/syndicate_monkey.txt"))
+GLOBAL_LIST_INIT(guardian_first_names, world.file2list("strings/names/guardian_descriptions.txt"))
+GLOBAL_LIST_INIT(guardian_tech_surnames, world.file2list("strings/names/guardian_gamepieces.txt"))
+GLOBAL_LIST_INIT(guardian_fantasy_surnames, world.file2list("strings/names/guardian_tarot.txt"))
GLOBAL_LIST_INIT(verbs, world.file2list("strings/names/verbs.txt"))
GLOBAL_LIST_INIT(ing_verbs, world.file2list("strings/names/ing_verbs.txt"))
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index a00ff5f3483..5bb8b4bf314 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -57,7 +57,7 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
/mob/living/carbon/alien,
/mob/living/simple_animal/slime,
)),
- "anime" = typecacheof(list(/mob/living/simple_animal/hostile/guardian)),
+ "anime" = typecacheof(list(/mob/living/basic/guardian)),
"birds" = typecacheof(list(
/mob/living/basic/chick,
/mob/living/basic/chicken,
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 8fd711279a5..4e83823b4d9 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -85,6 +85,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_BLOOD_DEFICIENCY" = TRAIT_BLOOD_DEFICIENCY,
"TRAIT_JOLLY" = TRAIT_JOLLY,
"TRAIT_NO_GLIDE" = TRAIT_NO_GLIDE,
+ "TRAIT_NO_FLOATING_ANIM" = TRAIT_NO_FLOATING_ANIM,
"TRAIT_NOCRITDAMAGE" = TRAIT_NOCRITDAMAGE,
"TRAIT_NO_SLIP_WATER" = TRAIT_NO_SLIP_WATER,
"TRAIT_NO_SLIP_ICE" = TRAIT_NO_SLIP_ICE,
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index f68776f5ec9..3e6790aee6b 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -607,19 +607,13 @@ or shoot a gun to move around via Newton's 3rd Law of Motion."
//GUARDIANS
-/atom/movable/screen/alert/cancharge
- name = "Charge Ready"
- desc = "You are ready to charge at a location!"
- icon_state = "guardian_charge"
- alerttooltipstyle = "parasite"
-
/atom/movable/screen/alert/canstealth
name = "Stealth Ready"
desc = "You are ready to enter stealth!"
icon_state = "guardian_canstealth"
alerttooltipstyle = "parasite"
-/atom/movable/screen/alert/instealth
+/atom/movable/screen/alert/status_effect/instealth
name = "In Stealth"
desc = "You are in stealth and your next attack will do bonus damage!"
icon_state = "guardian_instealth"
diff --git a/code/_onclick/hud/guardian.dm b/code/_onclick/hud/guardian.dm
index f9963c1fa3a..ba1d8f4565e 100644
--- a/code/_onclick/hud/guardian.dm
+++ b/code/_onclick/hud/guardian.dm
@@ -1,7 +1,7 @@
/datum/hud/guardian
ui_style = 'icons/hud/guardian.dmi'
-/datum/hud/guardian/New(mob/living/simple_animal/hostile/guardian/owner)
+/datum/hud/guardian/New(mob/living/basic/guardian/owner)
..()
var/atom/movable/screen/using
@@ -34,10 +34,10 @@
using.screen_loc = ui_back
static_inventory += using
-/datum/hud/dextrous/guardian/New(mob/living/simple_animal/hostile/guardian/owner) //for a dextrous guardian
+/datum/hud/dextrous/guardian/New(mob/living/basic/guardian/owner) //for a dextrous guardian
..()
var/atom/movable/screen/using
- if(istype(owner, /mob/living/simple_animal/hostile/guardian/dextrous))
+ if(istype(owner, /mob/living/basic/guardian/dextrous))
var/atom/movable/screen/inventory/inv_box
inv_box = new /atom/movable/screen/inventory(null, src)
@@ -86,8 +86,8 @@
/datum/hud/dextrous/guardian/persistent_inventory_update()
if(!mymob)
return
- if(istype(mymob, /mob/living/simple_animal/hostile/guardian/dextrous))
- var/mob/living/simple_animal/hostile/guardian/dextrous/dex_guardian = mymob
+ if(istype(mymob, /mob/living/basic/guardian/dextrous))
+ var/mob/living/basic/guardian/dextrous/dex_guardian = mymob
if(hud_shown)
if(dex_guardian.internal_storage)
@@ -109,7 +109,7 @@
/atom/movable/screen/guardian/manifest/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.manifest()
@@ -120,7 +120,7 @@
/atom/movable/screen/guardian/recall/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.recall()
/atom/movable/screen/guardian/toggle_mode
@@ -130,7 +130,7 @@
/atom/movable/screen/guardian/toggle_mode/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.toggle_modes()
/atom/movable/screen/guardian/toggle_mode/inactive
@@ -153,7 +153,7 @@
/atom/movable/screen/guardian/communicate/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.communicate()
@@ -164,5 +164,5 @@
/atom/movable/screen/guardian/toggle_light/Click()
if(isguardian(usr))
- var/mob/living/simple_animal/hostile/guardian/user = usr
+ var/mob/living/basic/guardian/user = usr
user.toggle_light()
diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm
index a7e0603461a..ed4309c36e1 100644
--- a/code/datums/actions/cooldown_action.dm
+++ b/code/datums/actions/cooldown_action.dm
@@ -177,6 +177,8 @@
/// Starts a cooldown time for other abilities that share a cooldown with this. Has some niche usage with more complicated attack ai!
/// Will use default cooldown time if an override is not specified
/datum/action/cooldown/proc/StartCooldownOthers(override_cooldown_time)
+ if(!length(owner.actions))
+ return // Possible if they have an action they don't control
for(var/datum/action/cooldown/shared_ability in owner.actions - src)
if(!(shared_cooldown & shared_ability.shared_cooldown))
continue
diff --git a/code/datums/actions/mobs/charge.dm b/code/datums/actions/mobs/charge.dm
index 7fb56209777..0c73145770e 100644
--- a/code/datums/actions/mobs/charge.dm
+++ b/code/datums/actions/mobs/charge.dm
@@ -156,17 +156,23 @@
SSexplosions.med_mov_atom += target
INVOKE_ASYNC(src, PROC_REF(DestroySurroundings), source)
- hit_target(source, target, charge_damage)
-
-/datum/action/cooldown/mob_cooldown/charge/proc/hit_target(atom/movable/source, atom/target, damage_dealt)
- if(!isliving(target))
- return
- var/mob/living/living_target = target
- living_target.visible_message("[source] slams into [living_target]!", "[source] tramples you into the ground!")
- source.forceMove(get_turf(living_target))
- living_target.apply_damage(damage_dealt, BRUTE, wound_bonus = CANT_WOUND)
- playsound(get_turf(living_target), 'sound/effects/meteorimpact.ogg', 100, TRUE)
- shake_camera(living_target, 4, 3)
+ try_hit_target(source, target)
+
+/// Attempt to hit someone with our charge
+/datum/action/cooldown/mob_cooldown/charge/proc/try_hit_target(atom/movable/source, atom/target)
+ if (can_hit_target(source, target))
+ hit_target(source, target, charge_damage)
+
+/// Returns true if we're allowed to charge into this target
+/datum/action/cooldown/mob_cooldown/charge/proc/can_hit_target(atom/movable/source, atom/target)
+ return isliving(target)
+
+/// Actually hit someone
+/datum/action/cooldown/mob_cooldown/charge/proc/hit_target(atom/movable/source, mob/living/target, damage_dealt)
+ target.visible_message(span_danger("[source] slams into [target]!"), span_userdanger("[source] tramples you into the ground!"))
+ target.apply_damage(damage_dealt, BRUTE, wound_bonus = CANT_WOUND)
+ playsound(get_turf(target), 'sound/effects/meteorimpact.ogg', 100, TRUE)
+ shake_camera(target, 4, 3)
shake_camera(source, 2, 3)
/datum/action/cooldown/mob_cooldown/charge/basic_charge
@@ -187,18 +193,21 @@
/datum/action/cooldown/mob_cooldown/charge/basic_charge/do_charge_indicator(atom/charger, atom/charge_target)
charger.Shake(shake_pixel_shift, shake_pixel_shift, shake_duration)
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/can_hit_target(atom/movable/source, atom/target)
+ if(!isliving(target))
+ if(!target.density || target.CanPass(source, get_dir(target, source)))
+ return FALSE
+ return TRUE
+ return ..()
+
/datum/action/cooldown/mob_cooldown/charge/basic_charge/hit_target(atom/movable/source, atom/target, damage_dealt)
var/mob/living/living_source
if(isliving(source))
living_source = source
if(!isliving(target))
- if(!target.density || target.CanPass(source, get_dir(target, source)))
- return
source.visible_message(span_danger("[source] smashes into [target]!"))
- if(!living_source)
- return
- living_source.Stun(recoil_duration, ignore_canstun = TRUE)
+ living_source?.Stun(recoil_duration, ignore_canstun = TRUE)
return
var/mob/living/living_target = target
@@ -208,10 +217,9 @@
living_source.Stun(recoil_duration, ignore_canstun = TRUE)
return
- living_target.visible_message(span_danger("[source] charges on [living_target]!"), span_userdanger("[source] charges into you!"))
+ living_target.visible_message(span_danger("[source] charges into [living_target]!"), span_userdanger("[source] charges into you!"))
living_target.Knockdown(knockdown_duration)
-
/datum/status_effect/tired_post_charge
id = "tired_post_charge"
duration = 1 SECONDS
diff --git a/code/datums/components/damage_chain.dm b/code/datums/components/damage_chain.dm
new file mode 100644
index 00000000000..be61ec68a33
--- /dev/null
+++ b/code/datums/components/damage_chain.dm
@@ -0,0 +1,112 @@
+/**
+ * Draws a line between you and another atom, hurt anyone stood in the line
+ */
+/datum/component/damage_chain
+ dupe_mode = COMPONENT_DUPE_ALLOWED
+ /// How often do we attempt to deal damage?
+ var/tick_interval
+ /// Tracks when we can next deal damage
+ COOLDOWN_DECLARE(tick_cooldown)
+ /// Damage inflicted per tick
+ var/damage_per_tick
+ /// Type of damage to inflict
+ var/damage_type
+ /// Optional callback which checks if we can damage the target
+ var/datum/callback/validate_target
+ /// Optional callback for additional visuals or text display when dealing damage
+ var/datum/callback/chain_damage_feedback
+ /// We will fire the damage feedback callback on every x successful attacks
+ var/feedback_interval
+ /// How many successful attacks have we made?
+ var/successful_attacks = 0
+ /// Time between making any attacks at which we just reset the successful attack counter
+ var/reset_feedback_timer = 0
+ /// Our chain
+ var/datum/beam/chain
+
+/datum/component/damage_chain/Initialize(
+ atom/linked_to,
+ max_distance = 7,
+ beam_icon = 'icons/effects/beam.dmi',
+ beam_state = "medbeam",
+ beam_type = /obj/effect/ebeam,
+ tick_interval = 0.3 SECONDS,
+ damage_per_tick = 1.2,
+ damage_type = BURN,
+ datum/callback/validate_target = null,
+ datum/callback/chain_damage_feedback = null,
+ feedback_interval = 5,
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (!isatom(linked_to))
+ CRASH("Attempted to create [type] linking [parent.type] with non-atom [linked_to]!")
+
+ src.tick_interval = tick_interval
+ src.damage_per_tick = damage_per_tick
+ src.damage_type = damage_type
+ src.validate_target = validate_target
+ src.chain_damage_feedback = chain_damage_feedback
+ src.feedback_interval = feedback_interval
+
+ var/atom/atom_parent = parent
+ chain = atom_parent.Beam(linked_to, icon = beam_icon, icon_state = beam_state, beam_type = beam_type, maxdistance = max_distance)
+ RegisterSignal(chain, COMSIG_QDELETING, PROC_REF(end_beam))
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/damage_chain/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(end_beam)) // We actually don't really use many signals it's all processing
+
+/datum/component/damage_chain/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_LIVING_DEATH)
+
+/datum/component/damage_chain/Destroy(force, silent)
+ if (!QDELETED(chain))
+ UnregisterSignal(chain, COMSIG_QDELETING)
+ QDEL_NULL(chain)
+ chain = null
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/// Destroy ourself
+/datum/component/damage_chain/proc/end_beam()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/damage_chain/process(seconds_per_tick)
+ var/successful_hit = FALSE
+ var/list/target_turfs = list()
+ for(var/obj/effect/ebeam/chainpart in chain.elements)
+ if (isnull(chainpart) || !chainpart.x || !chainpart.y || !chainpart.z)
+ continue
+ var/turf/overlaps = get_turf_pixel(chainpart)
+ target_turfs |= overlaps
+ if(overlaps == get_turf(chain.origin) || overlaps == get_turf(chain.target))
+ continue
+ for(var/turf/nearby_turf in circle_range(overlaps, 1))
+ target_turfs |= nearby_turf
+
+ for(var/turf/hit_turf as anything in target_turfs)
+ for(var/mob/living/victim in hit_turf)
+ if (victim == parent || victim.stat == DEAD)
+ continue
+ if (!isnull(validate_target) && !validate_target.Invoke(victim))
+ continue
+ if (successful_attacks == 0)
+ chain_damage_feedback?.Invoke(victim)
+ victim.apply_damage(damage_per_tick, damage_type, wound_bonus = CANT_WOUND)
+ successful_hit = TRUE
+
+ if (isnull(chain_damage_feedback))
+ return
+ if (successful_hit)
+ successful_attacks++
+ reset_feedback_timer = addtimer(CALLBACK(src, PROC_REF(reset_feedback)), 10 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE|TIMER_DELETE_ME)
+ if (successful_attacks > feedback_interval)
+ reset_feedback()
+
+/// Make it so that the next time we hit something we'll invoke the feedback callback
+/datum/component/damage_chain/proc/reset_feedback()
+ successful_attacks = 0
+ deltimer(reset_feedback_timer)
diff --git a/code/datums/components/direct_explosive_trap.dm b/code/datums/components/direct_explosive_trap.dm
new file mode 100644
index 00000000000..0d204f21a1e
--- /dev/null
+++ b/code/datums/components/direct_explosive_trap.dm
@@ -0,0 +1,85 @@
+/**
+ * Responds to certain signals and 'explodes' on the person using the item.
+ * Differs from `interaction_booby_trap` in that this doesn't actually explode, it just directly calls ex_act on one person.
+ */
+/datum/component/direct_explosive_trap
+ /// An optional mob to inform about explosions
+ var/mob/living/saboteur
+ /// Amount of force to apply
+ var/explosive_force
+ /// Colour for examine notification
+ var/glow_colour
+ /// Optional additional target checks before we go off
+ var/datum/callback/explosive_checks
+ /// Signals which set off the bomb, must pass a mob as the first non-source argument
+ var/list/triggering_signals
+
+/datum/component/direct_explosive_trap/Initialize(
+ mob/living/saboteur,
+ explosive_force = EXPLODE_HEAVY,
+ expire_time = 1 MINUTES,
+ glow_colour = COLOR_RED,
+ datum/callback/explosive_checks,
+ list/triggering_signals = list(COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_BUMPED)
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.saboteur = saboteur
+ src.explosive_force = explosive_force
+ src.glow_colour = glow_colour
+ src.explosive_checks = explosive_checks
+ src.triggering_signals = triggering_signals
+
+ if (expire_time > 0)
+ addtimer(CALLBACK(src, PROC_REF(bomb_expired)), expire_time, TIMER_DELETE_ME)
+
+/datum/component/direct_explosive_trap/RegisterWithParent()
+ if (!(COMSIG_ATOM_EXAMINE in triggering_signals)) // Maybe you're being extra mean with this one
+ RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
+ RegisterSignals(parent, triggering_signals, PROC_REF(explode))
+ if (!isnull(saboteur))
+ RegisterSignal(saboteur, COMSIG_QDELETING, PROC_REF(on_bomber_deleted))
+
+/datum/component/direct_explosive_trap/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ATOM_EXAMINE) + triggering_signals)
+ if (!isnull(saboteur))
+ UnregisterSignal(saboteur, COMSIG_QDELETING)
+
+/datum/component/direct_explosive_trap/Destroy(force, silent)
+ if (isnull(saboteur))
+ return ..()
+ UnregisterSignal(saboteur, COMSIG_QDELETING)
+ saboteur = null
+ return ..()
+
+/// Called if we sit too long without going off
+/datum/component/direct_explosive_trap/proc/bomb_expired()
+ if (!isnull(saboteur))
+ to_chat(saboteur, span_bolddanger("Failure! Your trap didn't catch anyone this time..."))
+ qdel(src)
+
+/// Let people know something is up
+/datum/component/direct_explosive_trap/proc/on_examined(datum/source, mob/user, text)
+ SIGNAL_HANDLER
+ text += span_holoparasite("It glows with a strange light...")
+
+/// Blow up
+/datum/component/direct_explosive_trap/proc/explode(atom/source, mob/living/victim)
+ SIGNAL_HANDLER
+ if (!isliving(victim))
+ return
+ if (!isnull(explosive_checks) && !explosive_checks.Invoke(victim))
+ return
+ to_chat(victim, span_bolddanger("[source] was boobytrapped!"))
+ if (!isnull(saboteur))
+ to_chat(saboteur, span_bolddanger("Success! Your trap on [source] caught [victim.name]!"))
+ playsound(source, 'sound/effects/explosion2.ogg', 200, TRUE)
+ new /obj/effect/temp_visual/explosion(get_turf(source))
+ EX_ACT(victim, explosive_force)
+ qdel(src)
+
+/// Don't hang a reference to the person who placed the bomb
+/datum/component/direct_explosive_trap/proc/on_bomber_deleted()
+ SIGNAL_HANDLER
+ saboteur = null
diff --git a/code/datums/components/healing_touch.dm b/code/datums/components/healing_touch.dm
index 8efbfa627a8..cf6ef88f24d 100644
--- a/code/datums/components/healing_touch.dm
+++ b/code/datums/components/healing_touch.dm
@@ -8,10 +8,15 @@
* This intercepts the attack and starts a do_after if the target is in its allowed type list.
*/
/datum/component/healing_touch
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// How much brute damage to heal
var/heal_brute
/// How much burn damage to heal
var/heal_burn
+ /// How much toxin damage to heal
+ var/heal_tox
+ /// How much oxygen damage to heal
+ var/heal_oxy
/// How much stamina damage to heal
var/heal_stamina
/// Interaction will use this key, and be blocked while this key is in use
@@ -36,10 +41,16 @@
var/show_health
/// Color for the healing effect
var/heal_color
+ /// Optional click modifier required
+ var/required_modifier
+ /// Callback to run after healing a mob
+ var/datum/callback/after_healed
/datum/component/healing_touch/Initialize(
heal_brute = 20,
heal_burn = 20,
+ heal_tox = 0,
+ heal_oxy = 0,
heal_stamina = 0,
heal_time = 2 SECONDS,
interaction_key = DOAFTER_SOURCE_HEAL_TOUCH,
@@ -52,12 +63,16 @@
complete_text = "%SOURCE% finishes healing %TARGET%",
show_health = FALSE,
heal_color = COLOR_HEALING_CYAN,
+ required_modifier = null,
+ datum/callback/after_healed = null,
)
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.heal_brute = heal_brute
src.heal_burn = heal_burn
+ src.heal_tox = heal_tox
+ src.heal_oxy = heal_oxy
src.heal_stamina = heal_stamina
src.heal_time = heal_time
src.interaction_key = interaction_key
@@ -70,10 +85,20 @@
src.complete_text = complete_text
src.show_health = show_health
src.heal_color = heal_color
+ src.required_modifier = required_modifier
+ src.after_healed = after_healed
RegisterSignal(parent, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(try_healing)) // Players
RegisterSignal(parent, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(try_healing)) // NPCs
+// Let's populate this list as we actually use it, this thing has too many args
+/datum/component/healing_touch/InheritComponent(
+ datum/component/new_component,
+ i_am_original,
+ heal_color,
+)
+ src.heal_color = heal_color
+
/datum/component/healing_touch/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_LIVING_UNARMED_ATTACK, COMSIG_HOSTILE_PRE_ATTACKINGTARGET))
return ..()
@@ -83,12 +108,15 @@
return ..()
/// Validate our target, and interrupt the attack chain to start healing it if it is allowed
-/datum/component/healing_touch/proc/try_healing(mob/living/healer, atom/target)
+/datum/component/healing_touch/proc/try_healing(mob/living/healer, atom/target, proximity, modifiers)
SIGNAL_HANDLER
if (!isliving(target))
return
- if (!is_type_in_typecache(target, valid_targets_typecache))
+ if (!isnull(required_modifier) && !LAZYACCESS(modifiers, required_modifier))
+ return
+
+ if (length(valid_targets_typecache) && !is_type_in_typecache(target, valid_targets_typecache))
return // Fall back to attacking it
if (extra_checks && !extra_checks.Invoke(healer, target))
@@ -131,6 +159,10 @@
return FALSE
if (target.getStaminaLoss() > 0 && heal_stamina)
return TRUE
+ if (target.getOxyLoss() > 0 && heal_oxy)
+ return TRUE
+ if (target.getToxLoss() > 0 && heal_tox)
+ return TRUE
if (!iscarbon(target))
return (target.getBruteLoss() > 0 && heal_brute) || (target.getFireLoss() > 0 && heal_burn)
var/mob/living/carbon/carbon_target = target
@@ -154,12 +186,26 @@
if (complete_text)
healer.visible_message(span_notice("[format_string(complete_text, healer, target)]"))
- target.heal_overall_damage(brute = heal_brute, burn = heal_burn, stamina = heal_stamina, required_bodytype = required_bodytype)
+ var/healed = target.heal_overall_damage(
+ brute = heal_brute,
+ burn = heal_burn,
+ stamina = heal_stamina,
+ required_bodytype = required_bodytype,
+ updating_health = FALSE,
+ )
+ healed += target.adjustOxyLoss(-heal_oxy, updating_health = FALSE, required_biotype = valid_biotypes)
+ healed += target.adjustToxLoss(-heal_tox, updating_health = FALSE, required_biotype = valid_biotypes)
+ if (healed <= 0)
+ return
+
+ target.updatehealth()
new /obj/effect/temp_visual/heal(get_turf(target), heal_color)
+ after_healed?.Invoke(target)
- if(show_health && !iscarbon(target))
- var/formatted_string = format_string("%TARGET% now has [target.health]/[target.maxHealth] health.", healer, target)
- to_chat(healer, span_danger(formatted_string))
+ if(!show_health)
+ return
+ var/formatted_string = format_string("%TARGET% now has [health_percentage(target)] health.", healer, target)
+ to_chat(healer, span_danger(formatted_string))
/// Reformats the passed string with the replacetext keys
/datum/component/healing_touch/proc/format_string(string, atom/source, atom/target)
diff --git a/code/datums/components/life_link.dm b/code/datums/components/life_link.dm
new file mode 100644
index 00000000000..34cdd0504c6
--- /dev/null
+++ b/code/datums/components/life_link.dm
@@ -0,0 +1,169 @@
+/**
+ * A mob with this component passes all damage (and healing) it takes to another mob, passed as a parameter
+ * Essentially we use another mob's health bar as our health bar
+ */
+/datum/component/life_link
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Mob we pass all of our damage to
+ var/mob/living/host
+ /// Optional callback invoked when damage gets transferred
+ var/datum/callback/on_passed_damage
+ /// Optional callback invoked when the linked mob dies
+ var/datum/callback/on_linked_death
+
+/datum/component/life_link/Initialize(mob/living/host, datum/callback/on_passed_damage, datum/callback/on_linked_death)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ if (!istype(host))
+ CRASH("Life link created on [parent.type] and attempted to link to invalid type [host?.type].")
+ register_host(host)
+ src.on_passed_damage = on_passed_damage
+ src.on_linked_death = on_linked_death
+
+/datum/component/life_link/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_CARBON_LIMB_DAMAGED, PROC_REF(on_limb_damage))
+ RegisterSignals(parent, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_damage_adjusted))
+ RegisterSignal(parent, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_updated))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_status_tab_updated))
+ if (!isnull(host))
+ var/mob/living/living_parent = parent
+ living_parent.updatehealth()
+
+/datum/component/life_link/UnregisterFromParent()
+ unregister_host()
+ UnregisterSignal(parent, list(COMSIG_CARBON_LIMB_DAMAGED, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+
+/datum/component/life_link/InheritComponent(datum/component/new_comp, i_am_original, mob/living/host, datum/callback/on_passed_damage, datum/callback/on_linked_death)
+ register_host(host)
+
+/// Set someone up as our new host
+/datum/component/life_link/proc/register_host(mob/living/new_host)
+ unregister_host()
+ if (isnull(new_host))
+ return
+ host = new_host
+ RegisterSignal(host, COMSIG_LIVING_DEATH, PROC_REF(on_host_died))
+ RegisterSignal(host, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_updated))
+ RegisterSignal(host, COMSIG_LIVING_REVIVE, PROC_REF(on_host_revived))
+ RegisterSignal(host, COMSIG_QDELETING, PROC_REF(on_host_deleted))
+ var/mob/living/living_parent = parent
+ living_parent.updatehealth()
+
+/// Drop someone from being our host
+/datum/component/life_link/proc/unregister_host()
+ if (isnull(host))
+ return
+ UnregisterSignal(host, list(COMSIG_LIVING_DEATH, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_REVIVE, COMSIG_QDELETING))
+ host = null
+
+/// Called when your damage goes up or down
+/datum/component/life_link/proc/on_damage_adjusted(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (forced)
+ return
+ amount *= our_mob.get_damage_mod(type)
+ switch (type)
+ if(BRUTE)
+ host.adjustBruteLoss(amount, forced = TRUE)
+ if(BURN)
+ host.adjustFireLoss(amount, forced = TRUE)
+ if(TOX)
+ host.adjustToxLoss(amount, forced = TRUE)
+ if(OXY)
+ host.adjustOxyLoss(amount, forced = TRUE)
+ if(CLONE)
+ host.adjustCloneLoss(amount, forced = TRUE)
+
+ on_passed_damage?.Invoke(our_mob, host, amount)
+ return COMPONENT_IGNORE_CHANGE
+
+/// Called when someone hurts one of our limbs, bypassing normal damage adjustment
+/datum/component/life_link/proc/on_limb_damage(mob/living/our_mob, limb, brute, burn)
+ SIGNAL_HANDLER
+ if (brute != 0)
+ host.adjustBruteLoss(brute, updating_health = FALSE)
+ if (burn != 0)
+ host.adjustFireLoss(burn, updating_health = FALSE)
+ if (brute != 0 || burn != 0)
+ host.updatehealth()
+ on_passed_damage?.Invoke(our_mob, host, brute + burn)
+ return COMPONENT_PREVENT_LIMB_DAMAGE
+
+/// Called when either the host or parent's health tries to update, update our displayed health
+/datum/component/life_link/proc/on_health_updated()
+ SIGNAL_HANDLER
+ update_health_hud(parent)
+ update_med_hud_health(parent)
+ update_med_hud_status(parent)
+
+/// Update our parent's health display based on how harmed our host is
+/datum/component/life_link/proc/update_health_hud(mob/living/mob_parent)
+ var/severity = 0
+ var/healthpercent = health_percentage(host)
+ switch(healthpercent)
+ if(100 to INFINITY)
+ severity = 0
+ if(85 to 100)
+ severity = 1
+ if(70 to 85)
+ severity = 2
+ if(55 to 70)
+ severity = 3
+ if(40 to 55)
+ severity = 4
+ if(25 to 40)
+ severity = 5
+ else
+ severity = 6
+ if(severity > 0)
+ mob_parent.overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
+ else
+ mob_parent.clear_fullscreen("brute")
+ if(mob_parent.hud_used?.healths)
+ mob_parent.hud_used.healths.maptext = MAPTEXT("
[round(healthpercent, 0.5)]%
")
+
+/// Update our health on the medical hud
+/datum/component/life_link/proc/update_med_hud_health(mob/living/mob_parent)
+ var/image/holder = mob_parent.hud_list?[HEALTH_HUD]
+ if(isnull(holder))
+ return
+ holder.icon_state = "hud[RoundHealth(host)]"
+ var/icon/size_check = icon(mob_parent.icon, mob_parent.icon_state, mob_parent.dir)
+ holder.pixel_y = size_check.Height() - world.icon_size
+
+/// Update our vital status on the medical hud
+/datum/component/life_link/proc/update_med_hud_status(mob/living/mob_parent)
+ var/image/holder = mob_parent.hud_list?[STATUS_HUD]
+ if(isnull(holder))
+ return
+ var/icon/size_check = icon(mob_parent.icon, mob_parent.icon_state, mob_parent.dir)
+ holder.pixel_y = size_check.Height() - world.icon_size
+ if(host.stat == DEAD || HAS_TRAIT(host, TRAIT_FAKEDEATH))
+ holder.icon_state = "huddead"
+ else
+ holder.icon_state = "hudhealthy"
+
+/// When our status tab updates, draw how much HP our host has in there
+/datum/component/life_link/proc/on_status_tab_updated(mob/living/source, list/items)
+ SIGNAL_HANDLER
+ var/healthpercent = health_percentage(host)
+ items += "Host Health: [round(healthpercent, 0.5)]%"
+
+/// Called when our host dies, we should die too
+/datum/component/life_link/proc/on_host_died(mob/living/source, gibbed)
+ SIGNAL_HANDLER
+ on_linked_death?.Invoke(parent, host, gibbed)
+ var/mob/living/living_parent = parent
+ living_parent.death(gibbed)
+
+/// Called when our host undies, we should undie too
+/datum/component/life_link/proc/on_host_revived(mob/living/source, full_heal_flags)
+ SIGNAL_HANDLER
+ var/mob/living/living_parent = parent
+ living_parent.revive(full_heal_flags)
+
+/// Called when
+/datum/component/life_link/proc/on_host_deleted()
+ SIGNAL_HANDLER
+ qdel(src)
diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm
index d1a8f827caf..c8c969a8c6b 100644
--- a/code/datums/components/riding/riding.dm
+++ b/code/datums/components/riding/riding.dm
@@ -67,8 +67,11 @@
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(vehicle_moved))
RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(vehicle_bump))
RegisterSignal(parent, COMSIG_BUCKLED_CAN_Z_MOVE, PROC_REF(riding_can_z_move))
+ RegisterSignals(parent, GLOB.movement_type_addtrait_signals, PROC_REF(on_movement_type_trait_gain))
+ RegisterSignals(parent, GLOB.movement_type_removetrait_signals, PROC_REF(on_movement_type_trait_loss))
if(!can_force_unbuckle)
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(force_unbuckle))
+
/**
* This proc handles all of the proc calls to things like set_vehicle_dir_layer() that a type of riding datum needs to call on creation
*
@@ -91,6 +94,10 @@
unequip_buckle_inhands(rider)
rider.updating_glide_size = TRUE
UnregisterSignal(rider, COMSIG_LIVING_TRY_PULL)
+ for (var/trait in GLOB.movement_type_trait_to_flag)
+ if (HAS_TRAIT(parent, trait))
+ REMOVE_TRAIT(rider, trait, REF(src))
+ REMOVE_TRAIT(rider, TRAIT_NO_FLOATING_ANIM, REF(src))
if(!movable_parent.has_buckled_mobs())
qdel(src)
@@ -107,6 +114,11 @@
rider.stop_pulling()
RegisterSignal(rider, COMSIG_LIVING_TRY_PULL, PROC_REF(on_rider_try_pull))
+ for (var/trait in GLOB.movement_type_trait_to_flag)
+ if (HAS_TRAIT(parent, trait))
+ ADD_TRAIT(rider, trait, REF(src))
+ ADD_TRAIT(rider, TRAIT_NO_FLOATING_ANIM, REF(src))
+
/// This proc is called when the rider attempts to grab the thing they're riding, preventing them from doing so.
/datum/component/riding/proc/on_rider_try_pull(mob/living/rider_pulling, atom/movable/target, force)
SIGNAL_HANDLER
@@ -297,6 +309,20 @@
SIGNAL_HANDLER
return COMPONENT_RIDDEN_ALLOW_Z_MOVE
+/// Called when our vehicle gains a movement trait, so we can apply it to the riders
+/datum/component/riding/proc/on_movement_type_trait_gain(atom/movable/source, trait)
+ SIGNAL_HANDLER
+ var/atom/movable/movable_parent = parent
+ for (var/mob/rider in movable_parent.buckled_mobs)
+ ADD_TRAIT(rider, trait, REF(src))
+
+/// Called when our vehicle loses a movement trait, so we can remove it from the riders
+/datum/component/riding/proc/on_movement_type_trait_loss(atom/movable/source, trait)
+ SIGNAL_HANDLER
+ var/atom/movable/movable_parent = parent
+ for (var/mob/rider in movable_parent.buckled_mobs)
+ REMOVE_TRAIT(rider, trait, REF(src))
+
/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter)
SIGNAL_HANDLER
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index 032a5ea54df..0ad6c1e4c7f 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -503,7 +503,7 @@
set_vehicle_dir_layer(WEST, ABOVE_MOB_LAYER)
/datum/component/riding/creature/guardian/ride_check(mob/living/user, consequences = TRUE)
- var/mob/living/simple_animal/hostile/guardian/charger = parent
+ var/mob/living/basic/guardian/charger = parent
if(!istype(charger))
return ..()
return charger.summoner == user
diff --git a/code/datums/elements/dextrous.dm b/code/datums/elements/dextrous.dm
index 47b6089f197..681e2a9488d 100644
--- a/code/datums/elements/dextrous.dm
+++ b/code/datums/elements/dextrous.dm
@@ -50,11 +50,10 @@
/// Try picking up items
/datum/element/dextrous/proc/on_hand_clicked(mob/living/hand_haver, atom/target, proximity, modifiers)
SIGNAL_HANDLER
- if(!proximity)
- if(isitem(target))
- var/obj/item/obj_item = target
- if(!obj_item.atom_storage && !(obj_item.item_flags & IN_STORAGE))
- return NONE
+ if (!proximity && target.loc != hand_haver)
+ var/obj/item/obj_item = target
+ if (istype(obj_item) && !obj_item.atom_storage && !(obj_item.item_flags & IN_STORAGE))
+ return NONE
if (!isitem(target) && hand_haver.combat_mode)
return NONE
if (LAZYACCESS(modifiers, RIGHT_CLICK))
diff --git a/code/datums/proximity_monitor/fields/timestop.dm b/code/datums/proximity_monitor/fields/timestop.dm
index 7fad290fc74..86ea41aee01 100644
--- a/code/datums/proximity_monitor/fields/timestop.dm
+++ b/code/datums/proximity_monitor/fields/timestop.dm
@@ -36,7 +36,7 @@
for(var/mob/living/to_check in GLOB.player_list)
if(HAS_TRAIT(to_check, TRAIT_TIME_STOP_IMMUNE))
immune[to_check] = TRUE
- for(var/mob/living/simple_animal/hostile/guardian/stand in GLOB.parasites)
+ for(var/mob/living/basic/guardian/stand in GLOB.parasites)
if(stand.summoner && HAS_TRAIT(stand.summoner, TRAIT_TIME_STOP_IMMUNE)) //It would only make sense that a person's stand would also be immune.
immune[stand] = TRUE
if(start)
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index 8f52165cdbb..925273eb1fa 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -233,10 +233,10 @@
qdel(moblight)
moblight = new moblight_type(owner)
- SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
cache_stacks()
update_overlay()
update_particles()
+ SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
return TRUE
/**
diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm
index 7704e11a1cb..aece8d6741b 100644
--- a/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm
+++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/assistance.dm
@@ -69,7 +69,7 @@
name = "Guardian Deck"
desc = "A deck of guardian tarot cards, capable of binding a personal guardian to your body. There are multiple types of guardian available, but all of them will transfer some amount of damage to you. \
It would be wise to avoid buying these with anything capable of causing you to swap bodies with others."
- item_path = /obj/item/guardiancreator/choose/wizard
+ item_path = /obj/item/guardian_creator/wizard
category = "Assistance"
/datum/spellbook_entry/item/bloodbottle
diff --git a/code/modules/cargo/exports/lavaland.dm b/code/modules/cargo/exports/lavaland.dm
index 31cc1319861..51165be191c 100644
--- a/code/modules/cargo/exports/lavaland.dm
+++ b/code/modules/cargo/exports/lavaland.dm
@@ -31,12 +31,12 @@
cost = CARGO_CRATE_VALUE * 40
unit_name = "lava planet artifact"
export_types = list(
- /obj/item/guardiancreator/miner,
- /obj/item/rod_of_asclepius,
/obj/item/dragons_blood,
- /obj/item/melee/ghost_sword,
+ /obj/item/guardian_creator/miner,
/obj/item/lava_staff,
+ /obj/item/melee/ghost_sword,
/obj/item/prisoncube,
+ /obj/item/rod_of_asclepius,
)
//Megafauna loot, except for ash drakes
diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm
index 9d1d4294669..8725fee2e22 100644
--- a/code/modules/mining/lavaland/necropolis_chests.dm
+++ b/code/modules/mining/lavaland/necropolis_chests.dm
@@ -55,7 +55,7 @@
if(12)
new /obj/item/jacobs_ladder(src)
if(13)
- new /obj/item/guardiancreator/miner(src)
+ new /obj/item/guardian_creator/miner(src)
if(14)
new /obj/item/warp_cube/red(src)
if(15)
diff --git a/code/modules/mining/lavaland/tendril_loot.dm b/code/modules/mining/lavaland/tendril_loot.dm
index 589cfbe3b3c..457bc438dfb 100644
--- a/code/modules/mining/lavaland/tendril_loot.dm
+++ b/code/modules/mining/lavaland/tendril_loot.dm
@@ -197,14 +197,14 @@
continue
regurgitate_guardian(guardian)
-/obj/item/clothing/neck/necklace/memento_mori/proc/consume_guardian(mob/living/simple_animal/hostile/guardian/guardian)
+/obj/item/clothing/neck/necklace/memento_mori/proc/consume_guardian(mob/living/basic/guardian/guardian)
new /obj/effect/temp_visual/guardian/phase/out(get_turf(guardian))
guardian.locked = TRUE
guardian.forceMove(src)
to_chat(guardian, span_userdanger("You have been locked away in your summoner's pendant!"))
guardian.playsound_local(get_turf(guardian), 'sound/magic/summonitems_generic.ogg', 50, TRUE)
-/obj/item/clothing/neck/necklace/memento_mori/proc/regurgitate_guardian(mob/living/simple_animal/hostile/guardian/guardian)
+/obj/item/clothing/neck/necklace/memento_mori/proc/regurgitate_guardian(mob/living/basic/guardian/guardian)
guardian.locked = FALSE
guardian.recall(forced = TRUE)
to_chat(guardian, span_notice("You have been returned back from your summoner's pendant!"))
diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm
index 4ad94270837..585c8b6b97b 100644
--- a/code/modules/mob/living/basic/basic.dm
+++ b/code/modules/mob/living/basic/basic.dm
@@ -208,7 +208,7 @@
face_atom(target)
if (!ignore_cooldown)
changeNext_move(melee_attack_cooldown)
- if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target) & COMPONENT_HOSTILE_NO_ATTACK)
+ if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target, Adjacent(target), modifiers) & COMPONENT_HOSTILE_NO_ATTACK)
return FALSE //but more importantly return before attack_animal called
var/result = target.attack_basic_mob(src, modifiers)
SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result)
diff --git a/code/modules/mob/living/basic/guardian/guardian.dm b/code/modules/mob/living/basic/guardian/guardian.dm
new file mode 100644
index 00000000000..e36960a80ae
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian.dm
@@ -0,0 +1,333 @@
+/**
+ * A mob which acts as a guardian angel to another mob, sharing health but protecting them using special powers.
+ * Usually either obtained in magical form by a wizard, or technological form by a traitor. Sometimes found by miners.
+ */
+/mob/living/basic/guardian
+ name = "Guardian Spirit"
+ real_name = "Guardian Spirit"
+ desc = "A mysterious being that stands by its charge, ever vigilant."
+ icon = 'icons/mob/nonhuman-player/guardian.dmi'
+ icon_state = "magicbase"
+ icon_living = "magicbase"
+ icon_dead = "magicbase"
+ gender = NEUTER
+ basic_mob_flags = DEL_ON_DEATH
+ mob_biotypes = MOB_SPECIAL
+ sentience_type = SENTIENCE_HUMANOID
+ hud_type = /datum/hud/guardian
+ faction = list()
+ speed = 0
+ maxHealth = INFINITY // The spirit itself is invincible and passes damage to its host
+ health = INFINITY
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
+ unsuitable_atmos_damage = 0
+ unsuitable_cold_damage = 0
+ unsuitable_heat_damage = 0
+ speak_emote = list("hisses")
+ bubble_icon = "guardian"
+ response_help_continuous = "passes through"
+ response_help_simple = "pass through"
+ response_disarm_continuous = "flails at"
+ response_disarm_simple = "flail at"
+ response_harm_continuous = "punches"
+ response_harm_simple = "punch"
+ attack_sound = 'sound/weapons/punch1.ogg'
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ combat_mode = TRUE
+ obj_damage = 40
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ melee_attack_cooldown = CLICK_CD_MELEE
+ light_system = MOVABLE_LIGHT
+ light_range = 3
+ light_on = FALSE
+
+ /// The summoner of the guardian, we share health with them and can't move too far away (usually)
+ var/mob/living/summoner
+ /// How far from the summoner the guardian can be.
+ var/range = 10
+
+ /// The guardian's colour, used for their sprite, chat, and some effects.
+ var/guardian_colour
+ /// Coloured overlay we apply
+ var/mutable_appearance/overlay
+
+ /// Which toggle button the HUD uses.
+ var/toggle_button_type = /atom/movable/screen/guardian/toggle_mode/inactive
+ /// Name used by the guardian creator.
+ var/creator_name = "Error"
+ /// Description used by the guardian creator.
+ var/creator_desc = "This shouldn't be here! Report it on GitHub!"
+ /// Icon used by the guardian creator.
+ var/creator_icon = "fuck"
+
+ /// What type of guardian are we?
+ var/guardian_type = null
+ /// How are we themed?
+ var/datum/guardian_fluff/theme
+ /// A string explaining to the guardian what they can do.
+ var/playstyle_string = span_boldholoparasite("You are a Guardian without any type. You shouldn't exist and are an affront to god!")
+
+ /// Are we forced to not be able to manifest/recall?
+ var/locked = FALSE
+ /// Cooldown between manifests/recalls.
+ COOLDOWN_DECLARE(manifest_cooldown)
+ /// Cooldown between the summoner resetting the guardian's client.
+ COOLDOWN_DECLARE(resetting_cooldown)
+
+ /// List of actions we give to our summoner
+ var/static/list/control_actions = list(
+ /datum/action/cooldown/mob_cooldown/guardian_comms,
+ /datum/action/cooldown/mob_cooldown/recall_guardian,
+ /datum/action/cooldown/mob_cooldown/replace_guardian,
+ )
+
+/mob/living/basic/guardian/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ GLOB.parasites += src
+ src.theme = theme
+ theme?.apply(src)
+ var/list/death_loot = string_list(list(/obj/item/stack/sheet/mineral/wood))
+ AddElement(/datum/element/death_drops, death_loot)
+ AddElement(/datum/element/simple_flying)
+ AddComponent(/datum/component/basic_inhands)
+ // life link
+ update_appearance(UPDATE_ICON)
+ manifest_effects()
+
+/mob/living/basic/guardian/Destroy()
+ GLOB.parasites -= src
+ if (is_deployed())
+ recall_effects()
+ cut_summoner(different_person = TRUE)
+ return ..()
+
+/mob/living/basic/guardian/update_overlays()
+ . = ..()
+ . += overlay
+
+/mob/living/basic/guardian/Login() //if we have a mind, set its name to ours when it logs in
+ . = ..()
+ if (!. || isnull(client))
+ return FALSE
+ if (isnull(summoner))
+ to_chat(src, span_boldholoparasite("For some reason, somehow, you have no summoner. Please report this bug immediately."))
+ stack_trace("Guardian created with client but no summoner.")
+ else
+ to_chat(src, span_holoparasite("You are a [theme.name], bound to serve [summoner.real_name]."))
+ to_chat(src, span_holoparasite("You are capable of manifesting or recalling to your master with the buttons on your HUD. You will also find a button to communicate with [summoner.p_them()] privately there."))
+ to_chat(src, span_holoparasite("While personally invincible, you will die if [summoner.real_name] does, and any damage dealt to you will have a portion passed on to [summoner.p_them()] as you feed upon [summoner.p_them()] to sustain yourself."))
+ to_chat(src, playstyle_string)
+ if (!isnull(guardian_colour))
+ return // Already set up so we don't need to do it again
+ locked = TRUE
+ guardian_rename()
+ guardian_recolour()
+ locked = FALSE
+
+/mob/living/basic/guardian/mind_initialize()
+ . = ..()
+ if (isnull(summoner))
+ to_chat(src, span_boldholoparasite("For some reason, somehow, you have no summoner. Please report this bug immediately."))
+ return
+ mind.enslave_mind_to_creator(summoner) // Once our mind is created, we become enslaved to our summoner. cant be done in the first run of set_summoner, because by then we dont have a mind yet.
+
+/// Pick a new colour for our guardian
+/mob/living/basic/guardian/proc/guardian_recolour()
+ if (isnull(client))
+ return
+ var/chosen_guardian_colour = input(src, "What would you like your colour to be?", "Choose Your Colour", "#ffffff") as color|null
+ if (isnull(chosen_guardian_colour)) //redo proc until we get a color
+ to_chat(src, span_warning("Invalid colour, please try again."))
+ return guardian_recolour()
+ set_guardian_colour(chosen_guardian_colour)
+
+/// Apply a new colour to our guardian
+/mob/living/basic/guardian/proc/set_guardian_colour(colour)
+ guardian_colour = colour
+ set_light_color(guardian_colour)
+ overlay?.color = guardian_colour
+ update_appearance(UPDATE_ICON)
+
+/mob/living/basic/guardian/proc/guardian_rename()
+ if (isnull(client))
+ return
+
+ var/new_name = sanitize_name(reject_bad_text(tgui_input_text(src, "What would you like your name to be?", "Choose Your Name", generate_random_name(), MAX_NAME_LEN)))
+ if (!new_name) //redo proc until we get a good name
+ to_chat(src, span_warning("Invalid name, please try again."))
+ return guardian_rename()
+ to_chat(src, span_notice("Your new name [span_name(new_name)] anchors itself in your mind."))
+ fully_replace_character_name(null, new_name)
+
+/// Picks a random name as a suggestion
+/mob/living/basic/guardian/proc/generate_random_name()
+ var/list/surname_options = list("Guardian") // Fallback in case you define a guardian with no theme
+ switch(theme?.fluff_type)
+ if (GUARDIAN_MAGIC)
+ surname_options = GLOB.guardian_fantasy_surnames
+ if (GUARDIAN_TECH)
+ surname_options = GLOB.guardian_tech_surnames
+
+ return "[pick(GLOB.guardian_first_names)] [pick(surname_options)]"
+
+/mob/living/basic/guardian/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ if (!is_deployed())
+ balloon_alert(src, "not tangible!")
+ return FALSE
+ return ..()
+
+/mob/living/basic/guardian/death(gibbed)
+ if (!QDELETED(summoner))
+ to_chat(summoner, span_bolddanger("Your [name] died somehow!"))
+ summoner.dust()
+ return ..()
+
+/mob/living/basic/guardian/ex_act(severity, target)
+ switch(severity)
+ if (EXPLODE_DEVASTATE)
+ investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
+ gib()
+ return TRUE
+ if (EXPLODE_HEAVY)
+ adjustBruteLoss(60)
+ if (EXPLODE_LIGHT)
+ adjustBruteLoss(30)
+
+ return TRUE
+
+/mob/living/basic/guardian/gib()
+ death(TRUE)
+
+/mob/living/basic/guardian/dust(just_ash, drop_items, force)
+ death(TRUE)
+
+/// Link up with a summoner mob.
+/mob/living/basic/guardian/proc/set_summoner(mob/living/to_who, different_person = FALSE)
+ if (QDELETED(src))
+ return // Just in case
+ if (QDELETED(to_who))
+ ghostize(FALSE)
+ qdel(src) // No life of free invulnerability for you.
+ return
+ cut_summoner(different_person)
+ AddComponent(/datum/component/life_link, to_who, CALLBACK(src, PROC_REF(on_harm)), CALLBACK(src, PROC_REF(on_summoner_death)))
+ summoner = to_who
+
+ for (var/action_type in control_actions)
+ if (locate(action_type) in summoner.actions)
+ continue
+ var/datum/action/new_action = new action_type(summoner)
+ new_action.Grant(summoner)
+
+ if (different_person)
+ if (mind)
+ mind.enslave_mind_to_creator(to_who)
+ else //mindless guardian, manually give them factions
+ faction += summoner.faction
+ summoner.faction += "[REF(src)]"
+ remove_all_languages(LANGUAGE_MASTER)
+ copy_languages(to_who, LANGUAGE_MASTER) // make sure holoparasites speak same language as master
+ RegisterSignal(to_who, COMSIG_QDELETING, PROC_REF(on_summoner_deletion))
+ RegisterSignal(to_who, COMSIG_LIVING_ON_WABBAJACKED, PROC_REF(on_summoner_wabbajacked))
+ RegisterSignal(to_who, COMSIG_LIVING_SHAPESHIFTED, PROC_REF(on_summoner_shapeshifted))
+ RegisterSignal(to_who, COMSIG_LIVING_UNSHAPESHIFTED, PROC_REF(on_summoner_unshapeshifted))
+ recall(forced = TRUE)
+ leash_to(src, summoner)
+ if (to_who.stat == DEAD)
+ on_summoner_death(src, to_who)
+ summoner.updatehealth()
+
+/// Remove all references to our summoner
+/mob/living/basic/guardian/proc/cut_summoner(different_person = FALSE)
+ if (isnull(summoner))
+ return
+ if (is_deployed())
+ recall_effects()
+ var/summoner_turf = get_turf(summoner)
+ if (!isnull(summoner_turf))
+ forceMove(summoner_turf)
+ unleash()
+ UnregisterSignal(summoner, list(COMSIG_QDELETING, COMSIG_LIVING_ON_WABBAJACKED, COMSIG_LIVING_SHAPESHIFTED, COMSIG_LIVING_UNSHAPESHIFTED))
+ if (different_person)
+ summoner.faction -= "[REF(src)]"
+ faction -= summoner.faction
+ mind?.remove_all_antag_datums()
+ if (!length(summoner.get_all_linked_holoparasites() - src))
+ for (var/action_type in control_actions)
+ var/datum/action/remove_action = locate(action_type) in summoner.actions
+ if (isnull(remove_action))
+ continue
+ remove_action.Remove(summoner)
+ summoner = null
+
+/// Connects these two mobs by a leash
+/mob/living/basic/guardian/proc/leash_to(atom/movable/leashed, atom/movable/leashed_to)
+ leashed.AddComponent(\
+ /datum/component/leash,\
+ owner = leashed_to,\
+ distance = range,\
+ force_teleport_out_effect = /obj/effect/temp_visual/guardian/phase/out,\
+ force_teleport_in_effect = /obj/effect/temp_visual/guardian/phase,\
+ )
+
+/// Removes the leash from this guardian
+/mob/living/basic/guardian/proc/unleash()
+ qdel(GetComponent(/datum/component/leash))
+
+/// Called when our owner dies. We fucked up, so now neither of us get to exist.
+/mob/living/basic/guardian/proc/on_summoner_death(mob/living/source, mob/living/former_owner)
+ cut_summoner()
+ if (!isnull(former_owner.loc))
+ forceMove(former_owner.loc)
+ to_chat(src, span_danger("Your summoner has died!"))
+ visible_message(span_bolddanger("\The [src] dies along with its user!"))
+ former_owner.visible_message(span_bolddanger("[former_owner]'s body is completely consumed by the strain of sustaining [src]!"))
+ former_owner.dust(drop_items = TRUE)
+
+/// Called when our health changes, inform our owner of why they are getting hurt (if they are)
+/mob/living/basic/guardian/proc/on_harm(mob/living/source, mob/living/summoner, amount)
+ if (QDELETED(src) || QDELETED(summoner) || amount <= 2)
+ return
+ to_chat(summoner, span_bolddanger("[name] is under attack! You take damage!"))
+ summoner.visible_message(span_bolddanger("Blood sprays from [summoner] as [src] takes damage!"))
+ if(summoner.stat == UNCONSCIOUS || summoner.stat == HARD_CRIT)
+ to_chat(summoner, span_bolddanger("Your head pounds, you can't take the strain of sustaining [src] in this condition!"))
+ summoner.adjustOrganLoss(ORGAN_SLOT_BRAIN, amount * 0.5)
+
+/// When our owner is deleted, we go too.
+/mob/living/basic/guardian/proc/on_summoner_deletion(mob/living/source)
+ SIGNAL_HANDLER
+ cut_summoner()
+ to_chat(src, span_danger("Your summoner is gone, you feel yourself fading!"))
+ ghostize(FALSE)
+ qdel(src)
+
+/// Signal proc for [COMSIG_LIVING_ON_WABBAJACKED], when our summoner is wabbajacked we should be alerted.
+/mob/living/basic/guardian/proc/on_summoner_wabbajacked(mob/living/source, mob/living/new_mob)
+ SIGNAL_HANDLER
+ set_summoner(new_mob)
+ to_chat(src, span_holoparasite("Your summoner has changed form!"))
+
+/// Signal proc for [COMSIG_LIVING_SHAPESHIFTED], when our summoner is shapeshifted we should change to the new mob
+/mob/living/basic/guardian/proc/on_summoner_shapeshifted(mob/living/source, mob/living/new_shape)
+ SIGNAL_HANDLER
+ set_summoner(new_shape)
+ to_chat(src, span_holoparasite("Your summoner has shapeshifted into that of a [new_shape]!"))
+
+/// Signal proc for [COMSIG_LIVING_UNSHAPESHIFTED], when our summoner unshapeshifts go back to that mob
+/mob/living/basic/guardian/proc/on_summoner_unshapeshifted(mob/living/source, mob/living/old_summoner)
+ SIGNAL_HANDLER
+ set_summoner(old_summoner)
+ to_chat(src, span_holoparasite("Your summoner has shapeshifted back into their normal form!"))
+
+/mob/living/basic/guardian/wabbajack(what_to_randomize, change_flags = WABBAJACK)
+ visible_message(span_warning("[src] resists the polymorph!")) // Ha, no
+
+/mob/living/basic/guardian/can_suicide()
+ return FALSE // You gotta persuade your boss to end it instead, sorry
+
+/// Returns true if you are out and about
+/mob/living/basic/guardian/proc/is_deployed()
+ return isnull(summoner) || loc != summoner
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian_creator.dm b/code/modules/mob/living/basic/guardian/guardian_creator.dm
similarity index 60%
rename from code/modules/mob/living/simple_animal/guardian/guardian_creator.dm
rename to code/modules/mob/living/basic/guardian/guardian_creator.dm
index ebd5658f07f..7ebc9737b41 100644
--- a/code/modules/mob/living/simple_animal/guardian/guardian_creator.dm
+++ b/code/modules/mob/living/basic/guardian/guardian_creator.dm
@@ -2,14 +2,15 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
/proc/setup_guardian_radial()
. = list()
- for(var/mob/living/simple_animal/hostile/guardian/guardian_path as anything in subtypesof(/mob/living/simple_animal/hostile/guardian))
+ for(var/mob/living/basic/guardian/guardian_path as anything in subtypesof(/mob/living/basic/guardian))
var/datum/radial_menu_choice/option = new()
option.name = initial(guardian_path.creator_name)
option.image = image(icon = 'icons/hud/guardian.dmi', icon_state = initial(guardian_path.creator_icon))
option.info = span_boldnotice(initial(guardian_path.creator_desc))
.[guardian_path] = option
-/obj/item/guardiancreator
+/// An item which grants you your very own soul buddy
+/obj/item/guardian_creator
name = "enchanted deck of tarot cards"
desc = "An enchanted deck of tarot cards, rumored to be a source of unimaginable power."
icon = 'icons/obj/toys/playing_cards.dmi'
@@ -31,36 +32,41 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
/// Message sent if we successfully get a guardian.
var/success_message = span_holoparasite("%GUARDIAN has been summoned!")
/// If true, you are given a random guardian rather than picking from a selection.
- var/random = TRUE
+ var/random = FALSE
/// If true, you can have multiple guardians at the same time.
- var/allowmultiple = FALSE
+ var/allow_multiple = FALSE
/// If true, lings can get guardians from this.
- var/allowling = TRUE
+ var/allow_changeling = TRUE
/// If true, a dextrous guardian can get their own guardian, infinite chain!
- var/allowguardian = FALSE
+ var/allow_guardian = FALSE
/// List of all the guardians this type can spawn.
var/list/possible_guardians = list( //default, has everything but dextrous
- /mob/living/simple_animal/hostile/guardian/assassin,
- /mob/living/simple_animal/hostile/guardian/charger,
- /mob/living/simple_animal/hostile/guardian/explosive,
- /mob/living/simple_animal/hostile/guardian/gaseous,
- /mob/living/simple_animal/hostile/guardian/gravitokinetic,
- /mob/living/simple_animal/hostile/guardian/lightning,
- /mob/living/simple_animal/hostile/guardian/protector,
- /mob/living/simple_animal/hostile/guardian/ranged,
- /mob/living/simple_animal/hostile/guardian/standard,
- /mob/living/simple_animal/hostile/guardian/support,
+ /mob/living/basic/guardian/assassin,
+ /mob/living/basic/guardian/charger,
+ /mob/living/basic/guardian/explosive,
+ /mob/living/basic/guardian/gaseous,
+ /mob/living/basic/guardian/gravitokinetic,
+ /mob/living/basic/guardian/lightning,
+ /mob/living/basic/guardian/protector,
+ /mob/living/basic/guardian/ranged,
+ /mob/living/basic/guardian/standard,
+ /mob/living/basic/guardian/support,
)
-/obj/item/guardiancreator/attack_self(mob/living/user)
- if(isguardian(user) && !allowguardian)
- to_chat(user, span_holoparasite("[mob_name] chains are not allowed."))
+/obj/item/guardian_creator/Initialize(mapload)
+ . = ..()
+ var/datum/guardian_fluff/using_theme = GLOB.guardian_themes[theme]
+ mob_name = using_theme.name
+
+/obj/item/guardian_creator/attack_self(mob/living/user)
+ if(isguardian(user) && !allow_guardian)
+ balloon_alert(user, "can't do that!")
return
var/list/guardians = user.get_all_linked_holoparasites()
- if(length(guardians) && !allowmultiple)
- to_chat(user, span_holoparasite("You already have a [mob_name]!"))
+ if(length(guardians) && !allow_multiple)
+ balloon_alert(user, "already have one!")
return
- if(user.mind && user.mind.has_antag_datum(/datum/antagonist/changeling) && !allowling)
+ if(user.mind && user.mind.has_antag_datum(/datum/antagonist/changeling) && !allow_changeling)
to_chat(user, ling_failure)
return
if(used)
@@ -71,19 +77,22 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
if(possible_guardian in possible_guardians)
continue
radial_options -= possible_guardian
- var/mob/living/simple_animal/hostile/guardian/guardian_path
+ var/mob/living/basic/guardian/guardian_path
if(random)
guardian_path = pick(possible_guardians)
else
guardian_path = show_radial_menu(user, src, radial_options, custom_check = CALLBACK(src, PROC_REF(check_menu), user), radius = 42, require_near = TRUE)
- if(!guardian_path)
+ if(isnull(guardian_path))
return
used = TRUE
to_chat(user, use_message)
- var/guardian_type_name = "a random"
- if(!random)
- guardian_type_name = "the " + lowertext(initial(guardian_path.creator_name))
- var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as [guardian_type_name] [mob_name] of [user.real_name]?", ROLE_PAI, FALSE, 100, POLL_IGNORE_HOLOPARASITE)
+ var/guardian_type_name = random ? "Random" : capitalize(initial(guardian_path.creator_name))
+ var/list/mob/dead/observer/candidates = poll_ghost_candidates(
+ "Do you want to play as [user.real_name]'s [guardian_type_name] [mob_name]?",
+ jobban_type = ROLE_PAI,
+ poll_time = 10 SECONDS,
+ ignore_category = POLL_IGNORE_HOLOPARASITE,
+ )
if(LAZYLEN(candidates))
var/mob/dead/observer/candidate = pick(candidates)
spawn_guardian(user, candidate, guardian_path)
@@ -91,124 +100,111 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
SEND_SIGNAL(src, COMSIG_TRAITOR_ITEM_USED(type))
else
to_chat(user, failure_message)
+ used = FALSE
-/obj/item/guardiancreator/proc/spawn_guardian(mob/living/user, mob/dead/candidate, guardian_path)
+/// Actually create our guy
+/obj/item/guardian_creator/proc/spawn_guardian(mob/living/user, mob/dead/candidate, guardian_path)
if(QDELETED(user) || user.stat == DEAD)
return
var/list/guardians = user.get_all_linked_holoparasites()
- if(length(guardians) && !allowmultiple)
- to_chat(user, span_holoparasite("You already have a [mob_name]!") )
+ if(length(guardians) && !allow_multiple)
+ balloon_alert(user, "already got one!")
used = FALSE
return
- var/mob/living/simple_animal/hostile/guardian/summoned_guardian = new guardian_path(user, theme)
+ var/datum/guardian_fluff/guardian_theme = GLOB.guardian_themes[theme]
+ var/mob/living/basic/guardian/summoned_guardian = new guardian_path(user, guardian_theme)
summoned_guardian.set_summoner(user, different_person = TRUE)
summoned_guardian.key = candidate.key
user.log_message("has summoned [key_name(summoned_guardian)], a [summoned_guardian.creator_name] holoparasite.", LOG_GAME)
summoned_guardian.log_message("was summoned as a [summoned_guardian.creator_name] holoparasite.", LOG_GAME)
- to_chat(user, summoned_guardian.used_fluff_string)
+ to_chat(user, guardian_theme.get_fluff_string(summoned_guardian.guardian_type))
to_chat(user, replacetext(success_message, "%GUARDIAN", mob_name))
summoned_guardian.client?.init_verbs()
return summoned_guardian
-/obj/item/guardiancreator/proc/check_menu(mob/living/user)
+/// Checks to ensure we're still capable of using the radial selector
+/obj/item/guardian_creator/proc/check_menu(mob/living/user)
if(!istype(user))
return FALSE
if(user.incapacitated() || !user.is_holding(src) || used)
return FALSE
return TRUE
-/obj/item/guardiancreator/choose
- random = FALSE
-
-/obj/item/guardiancreator/choose/all/Initialize(mapload)
- . = ..()
- possible_guardians = subtypesof(/mob/living/simple_animal/hostile/guardian)
-
-/obj/item/guardiancreator/choose/wizard
- allowmultiple = TRUE
- possible_guardians = list( //no support, but dextrous
- /mob/living/simple_animal/hostile/guardian/assassin,
- /mob/living/simple_animal/hostile/guardian/charger,
- /mob/living/simple_animal/hostile/guardian/dextrous,
- /mob/living/simple_animal/hostile/guardian/explosive,
- /mob/living/simple_animal/hostile/guardian/gaseous,
- /mob/living/simple_animal/hostile/guardian/gravitokinetic,
- /mob/living/simple_animal/hostile/guardian/lightning,
- /mob/living/simple_animal/hostile/guardian/protector,
- /mob/living/simple_animal/hostile/guardian/ranged,
- /mob/living/simple_animal/hostile/guardian/standard,
+/// Guardian creator available in the wizard spellbook. All but support are available.
+/obj/item/guardian_creator/wizard
+ allow_multiple = TRUE
+ possible_guardians = list(
+ /mob/living/basic/guardian/assassin,
+ /mob/living/basic/guardian/charger,
+ /mob/living/basic/guardian/dextrous,
+ /mob/living/basic/guardian/explosive,
+ /mob/living/basic/guardian/gaseous,
+ /mob/living/basic/guardian/gravitokinetic,
+ /mob/living/basic/guardian/lightning,
+ /mob/living/basic/guardian/protector,
+ /mob/living/basic/guardian/ranged,
+ /mob/living/basic/guardian/standard,
)
-/obj/item/guardiancreator/choose/wizard/spawn_guardian(mob/living/user, mob/dead/candidate)
- . = ..()
- var/mob/guardian = .
- if(!guardian)
- return
+/obj/item/guardian_creator/wizard/spawn_guardian(mob/living/user, mob/dead/candidate)
+ var/mob/guardian = ..()
+ if(isnull(guardian))
+ return null
+ // Add the wizard team datum
var/datum/antagonist/wizard/antag_datum = user.mind.has_antag_datum(/datum/antagonist/wizard)
- if(antag_datum)
- if(!antag_datum.wiz_team)
- antag_datum.create_wiz_team()
- guardian.mind.add_antag_datum(/datum/antagonist/wizard_minion, antag_datum.wiz_team)
-
-/obj/item/guardiancreator/tech
+ if(isnull(antag_datum))
+ return guardian
+ if(!antag_datum.wiz_team)
+ antag_datum.create_wiz_team()
+ guardian.mind.add_antag_datum(/datum/antagonist/wizard_minion, antag_datum.wiz_team)
+ return guardian
+
+/// Guardian creator available in the traitor uplink. All but dextrous are available, you can pick which you want, and changelings cannot use it.
+/obj/item/guardian_creator/tech
name = "holoparasite injector"
desc = "It contains an alien nanoswarm of unknown origin. Though capable of near sorcerous feats via use of hardlight holograms and nanomachines, it requires an organic host as a home base and source of fuel."
icon = 'icons/obj/medical/syringe.dmi'
icon_state = "combat_hypo"
theme = GUARDIAN_THEME_TECH
- mob_name = "Holoparasite"
+ allow_changeling = FALSE
use_message = span_holoparasite("You start to power on the injector...")
used_message = span_holoparasite("The injector has already been used.")
failure_message = span_boldholoparasite("...ERROR. BOOT SEQUENCE ABORTED. AI FAILED TO INTIALIZE. PLEASE CONTACT SUPPORT OR TRY AGAIN LATER.")
ling_failure = span_boldholoparasite("The holoparasites recoil in horror. They want nothing to do with a creature like you.")
success_message = span_holoparasite("%GUARDIAN is now online!")
-/obj/item/guardiancreator/tech/choose
- random = FALSE
-
-/obj/item/guardiancreator/tech/choose/all/Initialize(mapload)
- . = ..()
- possible_guardians = subtypesof(/mob/living/simple_animal/hostile/guardian)
-
-/obj/item/guardiancreator/tech/choose/traitor
- allowling = FALSE
-
-/obj/item/guardiancreator/carp
+/// Guardian creator only spawned by admins, which creates a holographic fish. You can have several of them.
+/obj/item/guardian_creator/carp
name = "holocarp fishsticks"
desc = "Using the power of Carp'sie, you can catch a carp from byond the veil of Carpthulu, and bind it to your fleshy flesh form."
icon = 'icons/obj/food/meat.dmi'
icon_state = "fishfingers"
theme = GUARDIAN_THEME_CARP
- mob_name = "Holocarp"
use_message = span_holoparasite("You put the fishsticks in your mouth...")
used_message = span_holoparasite("Someone's already taken a bite out of these fishsticks! Ew.")
failure_message = span_boldholoparasite("You couldn't catch any carp spirits from the seas of Lake Carp. Maybe there are none, maybe you fucked up.")
ling_failure = span_boldholoparasite("Carp'sie seems to not have taken you as the chosen one. Maybe it's because of your horrifying origin.")
success_message = span_holoparasite("%GUARDIAN has been caught!")
- allowmultiple = TRUE
+ allow_multiple = TRUE
-/obj/item/guardiancreator/carp/choose
- random = FALSE
-
-/obj/item/guardiancreator/miner
+/// Guardian creator available to miners from chests, very limited selection and randomly assigned.
+/obj/item/guardian_creator/miner
name = "dusty shard"
desc = "Seems to be a very old rock, may have originated from a strange meteor."
icon = 'icons/obj/mining_zones/artefacts.dmi'
icon_state = "dustyshard"
theme = GUARDIAN_THEME_MINER
- mob_name = "Power Miner"
use_message = span_holoparasite("You pierce your skin with the shard...")
used_message = span_holoparasite("This shard seems to have lost all its power...")
failure_message = span_boldholoparasite("The shard hasn't reacted at all. Maybe try again later...")
ling_failure = span_boldholoparasite("The power of the shard seems to not react with your horrifying, mutated body.")
success_message = span_holoparasite("%GUARDIAN has appeared!")
- possible_guardians = list( //limited to ones useful on lavaland
- /mob/living/simple_animal/hostile/guardian/charger,
- /mob/living/simple_animal/hostile/guardian/protector,
- /mob/living/simple_animal/hostile/guardian/ranged,
- /mob/living/simple_animal/hostile/guardian/standard,
- /mob/living/simple_animal/hostile/guardian/support,
+ random = TRUE
+ //limited to ones which are plausibly useful on lavaland
+ possible_guardians = list(
+ /mob/living/basic/guardian/charger, // A flying mount which can cross chasms
+ /mob/living/basic/guardian/protector, // Bodyblocks projectiles for you
+ /mob/living/basic/guardian/ranged, // Shoots the bad guys
+ /mob/living/basic/guardian/standard, // Can mine walls
+ /mob/living/basic/guardian/support, // Heals and teleports you
)
-
-/obj/item/guardiancreator/miner/choose
- random = FALSE
diff --git a/code/modules/mob/living/basic/guardian/guardian_fluff.dm b/code/modules/mob/living/basic/guardian/guardian_fluff.dm
new file mode 100644
index 00000000000..4ede238921e
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_fluff.dm
@@ -0,0 +1,124 @@
+/**
+ * Defines a theme used by guardian mobs for visuals and some text output
+ * The default is used for ones created by wizards
+ */
+/datum/guardian_fluff
+ /// What name do we apply before one has been selected?
+ var/name = "Guardian Spirit"
+ /// Mob description to apply
+ var/desc = "A mysterious being that stands by its charge, ever vigilant."
+ /// Are we magical or technological? Mostly just used to pick a surname
+ var/fluff_type = GUARDIAN_MAGIC
+ /// What speech bubble do we use?
+ var/bubble_icon = "guardian"
+ /// What is our base icon state?
+ var/icon_state = "magicbase"
+ /// What is the icon state for our coloured overlay?
+ var/overlay_state = "magic"
+ /// Emote used for speaking
+ var/list/speak_emote = list("hisses")
+ /// Verb shown to viewers when attacking
+ var/attack_verb_continuous = "punches"
+ /// Verb shown to attacker when attacking
+ var/attack_verb_simple = "punch"
+ /// Sound played when we attack
+ var/attack_sound = 'sound/weapons/punch1.ogg'
+ /// Visible effect when we attack
+ var/attack_vis_effect = ATTACK_EFFECT_PUNCH
+ /// An associative list of type of guardian to some kind of descriptive text to show on appearance.
+ var/guardian_fluff = list(
+ GUARDIAN_ASSASSIN = "...And draw the Space Ninja, a lethal and invisible assassin.",
+ GUARDIAN_CHARGER = "...And draw the Hunter, alien master of rapid assault.",
+ GUARDIAN_DEXTROUS = "...And draw the Monkey, ascendant beast who has learned to use tools.",
+ GUARDIAN_EXPLOSIVE = "...And draw the Scientist, herald of explosive death.",
+ GUARDIAN_GASEOUS = "...And draw the Atmospheric Technician, veiled in a purple haze.",
+ GUARDIAN_GRAVITOKINETIC = "...And draw the Singularity, a terrible, irresistible force..",
+ GUARDIAN_LIGHTNING = "...And draw the Supermatter, a shockingly lethal font of power.",
+ GUARDIAN_PROTECTOR = "...And draw the Corgi, a stalwart protector that never leaves the side of its charge.",
+ GUARDIAN_RANGED = "...And draw the Watcher, impaling its prey from afar.",
+ GUARDIAN_STANDARD = "...And draw the Assistant, faceless but never to be underestimated.",
+ GUARDIAN_SUPPORT = "...And draw the Paramedic, arbiter of life and death.",
+ )
+
+/// Applies relevant visual properties to our guardian
+/datum/guardian_fluff/proc/apply(mob/living/basic/guardian/guardian)
+ guardian.name = name
+ guardian.real_name = name
+ guardian.bubble_icon = bubble_icon
+ guardian.icon_living = icon_state
+ guardian.icon_state = icon_state
+
+ guardian.speak_emote = speak_emote
+ guardian.attack_verb_continuous = attack_verb_continuous
+ guardian.attack_verb_simple = attack_verb_simple
+ guardian.attack_sound = attack_sound
+ guardian.attack_vis_effect = attack_vis_effect
+
+ guardian.overlay = mutable_appearance(guardian.icon, overlay_state)
+
+/// Output an appropriate fluff string for our guardian when it is created
+/datum/guardian_fluff/proc/get_fluff_string(guardian_type)
+ return span_holoparasite(guardian_fluff[guardian_type] || "You bring forth a glitching abomination, something which should not be! Please contact a coder about it.")
+
+/// Used by holoparasites in the Traitor uplink
+/datum/guardian_fluff/tech
+ name = "Holoparasite"
+ fluff_type = GUARDIAN_TECH
+ bubble_icon = "holo"
+ icon_state = "techbase"
+ overlay_state = "tech"
+ guardian_fluff = list(
+ GUARDIAN_ASSASSIN = "Boot sequence complete. Stealth modules loaded. Holoparasite swarm online.",
+ GUARDIAN_CHARGER = "Boot sequence complete. Overclocking motive engines. Holoparasite swarm online.",
+ GUARDIAN_DEXTROUS = "Boot sequence complete. Armed combat routines loaded. Holoparasite swarm online.",
+ GUARDIAN_EXPLOSIVE = "Boot sequence complete. Payload generator online. Holoparasite swarm online.",
+ GUARDIAN_GASEOUS = "Boot sequence complete. Atmospheric projectors operational. Holoparasite swarm online.",
+ GUARDIAN_GRAVITOKINETIC = "Boot sequence complete. Gravitic engine spinning up. Holoparasite swarm online.",
+ GUARDIAN_LIGHTNING = "Boot sequence complete. Tesla projectors charged. Holoparasite swarm online.",
+ GUARDIAN_PROTECTOR = "Boot sequence complete. Bodyguard routines loaded. Holoparasite swarm online.",
+ GUARDIAN_RANGED = "Boot sequence complete. Flechette launchers operational. Holoparasite swarm online.",
+ GUARDIAN_STANDARD = "Boot sequence complete. CQC suite engaged. Holoparasite swarm online.",
+ GUARDIAN_SUPPORT = "Boot sequence complete. Medical suite active. Holoparasite swarm online.",
+ )
+
+/// Used by powerminers found in necropolis chests
+/datum/guardian_fluff/miner
+ name = "Power Miner"
+ icon_state = "minerbase"
+ overlay_state = "miner"
+ guardian_fluff = list(
+ GUARDIAN_ASSASSIN = "The shard reveals... Glass, a sharp but fragile ambusher.",
+ GUARDIAN_CHARGER = "The shard reveals... Titanium, a lightweight, agile fighter.",
+ GUARDIAN_DEXTROUS = "The shard reveals... Gold, a malleable hoarder of treasure.",
+ GUARDIAN_EXPLOSIVE = "The shard reveals... Gibtonite, volatile and surprising.",
+ GUARDIAN_GASEOUS = "The shard reveals... Plasma, the bringer of flame.",
+ GUARDIAN_GRAVITOKINETIC = "The shard reveals... Bananium, a manipulator of motive forces.",
+ GUARDIAN_LIGHTNING = "The shard reveals... Iron, a conductive font of lightning.",
+ GUARDIAN_PROTECTOR = "The shard reveals... Uranium, dense and resistant.",
+ GUARDIAN_RANGED = "The shard reveals... Diamond, projecting a million sharp edges.",
+ GUARDIAN_STANDARD = "The shard reveals... Plastitanium, a powerful fighter.",
+ GUARDIAN_SUPPORT = "The shard reveals... Bluespace, master of relocation.",
+ )
+
+/// Used by holocarp spawned by admins
+/datum/guardian_fluff/carp
+ name = "Holocarp"
+ fluff_type = GUARDIAN_TECH
+ desc = "A mysterious fish that swims by its charge, ever fingilant."
+ icon_state = null // Handled entirely by the overlay
+ bubble_icon = "holo"
+ overlay_state = "carp"
+ speak_emote = list("gnashes")
+ guardian_fluff = list(
+ GUARDIAN_ASSASSIN = "CARP CARP CARP! Caught one! It's an assassin carp! Just when you thought it was safe to go back to the water... which is unhelpful, because we're in space.",
+ GUARDIAN_CHARGER = "CARP CARP CARP! Caught one! It's a charger carp which likes running at people. But it doesn't have any legs...",
+ GUARDIAN_DEXTROUS = "CARP CARP CARP! You caught one! It's a dextrous carp ready to slap people with a fish, once it picks one up.",
+ GUARDIAN_EXPLOSIVE = "CARP CARP CARP! Caught one! It's an explosive carp! You two are going to have a blast.",
+ GUARDIAN_GASEOUS = "CARP CARP CARP! You caught one! It's a gaseous carp, but don't worry it actually smells pretty good!",
+ GUARDIAN_GRAVITOKINETIC = "CARP CARP CARP! Caught one! It's a gravitokinetic carp! Now do you understand the gravity of the situation?",
+ GUARDIAN_LIGHTNING = "CARP CARP CARP! Caught one! It's a lightning carp! What a shocking result!",
+ GUARDIAN_PROTECTOR = "CARP CARP CARP! You caught one! Wait, no... it caught you! The fisher has become the fishy...",
+ GUARDIAN_RANGED = "CARP CARP CARP! You caught one! It's a ranged carp! It has been collecting glass shards in preparation for this moment.",
+ GUARDIAN_STANDARD = "CARP CARP CARP! You caught one! This one is a little generic and disappointing... Better punch through some walls to ease the tension.",
+ GUARDIAN_SUPPORT = "CARP CARP CARP! You caught a support carp! Now it's here, now you're over there!",
+ )
diff --git a/code/modules/mob/living/basic/guardian/guardian_helpers.dm b/code/modules/mob/living/basic/guardian/guardian_helpers.dm
new file mode 100644
index 00000000000..df50a43e6a0
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_helpers.dm
@@ -0,0 +1,13 @@
+/// Returns a list of all holoparasites that has this mob as a summoner.
+/mob/living/proc/get_all_linked_holoparasites()
+ RETURN_TYPE(/list)
+ var/list/all_parasites = list()
+ for(var/mob/living/basic/guardian/stand as anything in GLOB.parasites)
+ if (stand.summoner != src)
+ continue
+ all_parasites += stand
+ return all_parasites
+
+/// Returns true if this holoparasite has the same summoner as the passed holoparasite.
+/mob/living/basic/guardian/proc/shares_summoner(mob/living/basic/guardian/other_guardian)
+ return istype(other_guardian) && other_guardian.summoner == summoner
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm b/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm
new file mode 100644
index 00000000000..d62b9bcedea
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/assassin.dm
@@ -0,0 +1,116 @@
+#define CAN_STEALTH_ALERT "can_stealth"
+
+/**
+ * Can enter stealth mode to become invisible and deal bonus damage on their next attack, an ambush predator.
+ */
+/mob/living/basic/guardian/assassin
+ guardian_type = GUARDIAN_ASSASSIN
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ attack_verb_continuous = "slashes"
+ attack_verb_simple = "slash"
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attack_vis_effect = ATTACK_EFFECT_SLASH
+ sharpness = SHARP_POINTY
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
+ playstyle_string = span_holoparasite("As an assassin type you do medium damage and have no damage resistance, but can enter stealth, massively increasing the damage of your next attack and causing it to ignore armor. Stealth is broken when you attack or take damage.")
+ creator_name = "Assassin"
+ creator_desc = "Does medium damage and takes full damage, but can enter stealth, causing its next attack to do massive damage and ignore armor. However, it becomes briefly unable to recall after attacking from stealth."
+ creator_icon = "assassin"
+ toggle_button_type = /atom/movable/screen/guardian/toggle_mode/assassin
+ /// How long to put stealth on cooldown if we are forced out?
+ var/stealth_cooldown_time = 16 SECONDS
+ /// Cooldown for the stealth toggle.
+ COOLDOWN_DECLARE(stealth_cooldown)
+
+/mob/living/basic/guardian/assassin/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ show_can_stealth()
+ RegisterSignal(src, COMSIG_GUARDIAN_ASSASSIN_REVEALED, PROC_REF(on_forced_unstealth))
+
+// Toggle stealth
+/mob/living/basic/guardian/assassin/toggle_modes()
+ var/stealthed = has_status_effect(/datum/status_effect/guardian_stealth)
+ if (stealthed)
+ to_chat(src, span_bolddanger("You exit stealth."))
+ remove_status_effect(/datum/status_effect/guardian_stealth)
+ show_can_stealth()
+ return
+ if (COOLDOWN_FINISHED(src, stealth_cooldown))
+ if (!is_deployed())
+ to_chat(src, span_bolddanger("You have to be manifested to enter stealth!"))
+ return
+ apply_status_effect(/datum/status_effect/guardian_stealth)
+ clear_alert(CAN_STEALTH_ALERT)
+ return
+ to_chat(src, span_bolddanger("You cannot yet enter stealth, wait another [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]!"))
+
+/mob/living/basic/guardian/assassin/get_status_tab_items()
+ . = ..()
+ if(!COOLDOWN_FINISHED(src, stealth_cooldown))
+ . += "Stealth Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]"
+
+/// Called when we are removed from stealth involuntarily
+/mob/living/basic/guardian/assassin/proc/on_forced_unstealth(mob/living/source)
+ SIGNAL_HANDLER
+ visible_message(span_danger("\The [src] suddenly appears!"))
+ COOLDOWN_START(src, manifest_cooldown, 4 SECONDS)
+ COOLDOWN_START(src, stealth_cooldown, stealth_cooldown_time)
+ addtimer(CALLBACK(src, PROC_REF(show_can_stealth)), stealth_cooldown_time)
+
+/// Displays an alert letting us know that we can enter stealth
+/mob/living/basic/guardian/assassin/proc/show_can_stealth()
+ if(!COOLDOWN_FINISHED(src, stealth_cooldown))
+ return
+ throw_alert(CAN_STEALTH_ALERT, /atom/movable/screen/alert/canstealth)
+
+/// Status effect which makes us sneakier and do bonus damage
+/datum/status_effect/guardian_stealth
+ id = "guardian_stealth"
+ alert_type = /atom/movable/screen/alert/status_effect/instealth
+ /// Damage added in stealth mode.
+ var/damage_bonus = 35
+ /// Our wound bonus when in stealth mode. Allows you to actually cause wounds, unlike normal.
+ var/stealth_wound_bonus = -20
+
+/datum/status_effect/guardian_stealth/on_apply()
+ new /obj/effect/temp_visual/guardian/phase/out(get_turf(owner))
+ owner.melee_damage_lower += damage_bonus
+ owner.melee_damage_upper += damage_bonus
+ if (isbasicmob(owner))
+ var/mob/living/basic/basic_owner = owner
+ basic_owner.armour_penetration = 100
+ basic_owner.wound_bonus = stealth_wound_bonus
+ basic_owner.obj_damage = 0
+ to_chat(owner, span_bolddanger("You enter stealth, empowering your next attack."))
+ animate(owner, alpha = 15, time = 0.5 SECONDS)
+
+ RegisterSignals(owner, list(COMSIG_GUARDIAN_RECALLED, COMSIG_HOSTILE_POST_ATTACKINGTARGET), PROC_REF(forced_exit))
+ RegisterSignals(owner, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_health_changed))
+ return TRUE
+
+/datum/status_effect/guardian_stealth/on_remove()
+ owner.melee_damage_lower -= damage_bonus
+ owner.melee_damage_upper -= damage_bonus
+ if (isbasicmob(owner))
+ var/mob/living/basic/basic_owner = owner
+ basic_owner.armour_penetration = initial(basic_owner.armour_penetration)
+ basic_owner.wound_bonus = initial(basic_owner.wound_bonus)
+ basic_owner.obj_damage = initial(basic_owner.obj_damage)
+ animate(owner, alpha = initial(owner.alpha), time = 0.5 SECONDS)
+ UnregisterSignal(owner, list(COMSIG_GUARDIAN_RECALLED, COMSIG_HOSTILE_POST_ATTACKINGTARGET) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+
+/// If we take damage, exit the status effect
+/datum/status_effect/guardian_stealth/proc/on_health_changed(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (amount <= 0)
+ return
+ forced_exit()
+
+/// Forcibly exit the status effect
+/datum/status_effect/guardian_stealth/proc/forced_exit()
+ SIGNAL_HANDLER
+ SEND_SIGNAL(owner, COMSIG_GUARDIAN_ASSASSIN_REVEALED)
+ qdel(src)
+
+#undef CAN_STEALTH_ALERT
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/charger.dm b/code/modules/mob/living/basic/guardian/guardian_types/charger.dm
new file mode 100644
index 00000000000..02f839c6a41
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/charger.dm
@@ -0,0 +1,69 @@
+/**
+ * Very fast, has a charging attack, and most importantly can be ridden like a horse.
+ */
+/mob/living/basic/guardian/charger
+ guardian_type = GUARDIAN_CHARGER
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ speed = -0.5
+ damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
+ playstyle_string = span_holoparasite("As a charger type you do medium damage, have light damage resistance, move very fast, can be ridden, and can charge at a location, damaging any target hit and forcing them to drop any items they are holding.")
+ creator_name = "Charger"
+ creator_desc = "Moves very fast, does medium damage on attack, can be ridden and can charge at targets, damaging the first target hit and forcing them to drop any items they are holding."
+ creator_icon = "charger"
+
+/mob/living/basic/guardian/charger/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ AddElement(/datum/element/ridable, /datum/component/riding/creature/guardian)
+ var/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/charge = new(src)
+ charge.Grant(src)
+ charge.set_click_ability(src)
+
+/// Guardian charger's charging attack, it knocks items out of people's hands
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian
+ name = "Charge!"
+ cooldown_time = 4 SECONDS
+ melee_cooldown_time = 0 SECONDS
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "speed"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ charge_delay = 0
+ recoil_duration = 0
+ charge_damage = 20
+ charge_distance = 10
+ unset_after_click = FALSE
+ destroy_objects = FALSE
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/PreActivate(atom/target)
+ if (!isguardian(owner))
+ return ..()
+ var/mob/living/basic/guardian/guardian_owner = owner
+ if (guardian_owner.is_deployed())
+ return ..()
+ return FALSE
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/do_charge_indicator(atom/charger, atom/charge_target)
+ playsound(charger, 'sound/items/modsuit/loader_launch.ogg', 75, TRUE)
+ var/obj/effect/temp_visual/decoy/decoy_flash = new /obj/effect/temp_visual/decoy(charger.loc, charger)
+ animate(decoy_flash, alpha = 0, color = "#FF0000", transform = matrix() * 2, time = 3)
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/can_hit_target(atom/movable/source, atom/target)
+ var/mob/living/living_target = target
+ if (!istype(living_target))
+ return FALSE
+ var/mob/living/basic/guardian/guardian_owner = owner
+ if (!istype(guardian_owner))
+ return TRUE
+ if (living_target == guardian_owner.summoner || guardian_owner.shares_summoner(target))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/charge/basic_charge/guardian/hit_target(atom/movable/source, mob/living/target, damage_dealt)
+ if(ishuman(target))
+ var/mob/living/carbon/human/hit_human = target
+ if(hit_human.check_shields(src, charge_damage, name, attack_type = LEAP_ATTACK))
+ return
+ . = ..()
+ var/mob/living/hit_mob = target
+ hit_mob.drop_all_held_items()
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
new file mode 100644
index 00000000000..ed54a23771d
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/dextrous.dm
@@ -0,0 +1,100 @@
+/// Dextrous guardians have some of the most powerful abilities of all: hands and pockets
+/mob/living/basic/guardian/dextrous
+ guardian_type = GUARDIAN_DEXTROUS
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
+ playstyle_string = span_holoparasite("As a dextrous type you can hold items, store an item within yourself, and have medium damage resistance, but do low damage on attacks. Recalling and leashing will force you to drop unstored items!")
+ creator_name = "Dextrous"
+ creator_desc = "Does low damage on attack, but is capable of holding items and storing a single item within it. It will drop items held in its hands when it recalls, but it will retain the stored item."
+ creator_icon = "dextrous"
+ hud_type = /datum/hud/dextrous/guardian
+ held_items = list(null, null)
+ /// An internal pocket we can put stuff in
+ var/obj/item/internal_storage
+
+/mob/living/basic/guardian/dextrous/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
+ AddElement(/datum/element/dextrous, hud_type = hud_type)
+ AddComponent(/datum/component/personal_crafting)
+ AddComponent(/datum/component/basic_inhands)
+
+/mob/living/basic/guardian/dextrous/death(gibbed)
+ dropItemToGround(internal_storage)
+ return ..()
+
+/mob/living/basic/guardian/dextrous/examine(mob/user)
+ . = ..()
+ if(isnull(internal_storage) || (internal_storage.item_flags & ABSTRACT))
+ return
+ . += span_info("It is holding [internal_storage.get_examine_string(user)] in its internal storage.")
+
+/mob/living/basic/guardian/dextrous/recall_effects()
+ . = ..()
+ drop_all_held_items()
+
+// Bullshit related to having a fake pocket begins here
+
+/mob/living/basic/guardian/dextrous/doUnEquip(obj/item/equipped_item, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
+ . = ..()
+ if (!.)
+ return FALSE
+ update_held_items()
+ if(equipped_item == internal_storage)
+ internal_storage = null
+ update_inv_internal_storage()
+ return TRUE
+
+/mob/living/basic/guardian/dextrous/can_equip(mob/living/M, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE, indirect_action = FALSE)
+ if(slot != ITEM_SLOT_DEX_STORAGE)
+ return FALSE
+ return isnull(internal_storage)
+
+/mob/living/basic/guardian/dextrous/get_item_by_slot(slot_id)
+ if(slot_id == ITEM_SLOT_DEX_STORAGE)
+ return internal_storage
+ return ..()
+
+/mob/living/basic/guardian/dextrous/get_slot_by_item(obj/item/looking_for)
+ if(internal_storage == looking_for)
+ return ITEM_SLOT_DEX_STORAGE
+ return ..()
+
+/mob/living/basic/guardian/dextrous/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
+ if (slot != ITEM_SLOT_DEX_STORAGE)
+ to_chat(src, span_danger("You are trying to equip this item to an unsupported inventory slot. Report this to a coder!"))
+ return FALSE
+
+ var/index = get_held_index_of_item(equipping)
+ if(index)
+ held_items[index] = null
+ update_held_items()
+
+ if(equipping.pulledby)
+ equipping.pulledby.stop_pulling()
+
+ equipping.screen_loc = null // will get moved if inventory is visible
+ equipping.forceMove(src)
+ SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
+
+ internal_storage = equipping
+ update_inv_internal_storage()
+
+ equipping.on_equipped(src, slot)
+ return TRUE
+
+/mob/living/basic/guardian/dextrous/getBackSlot()
+ return ITEM_SLOT_DEX_STORAGE
+
+/mob/living/basic/guardian/dextrous/getBeltSlot()
+ return ITEM_SLOT_DEX_STORAGE
+
+/mob/living/basic/guardian/dextrous/proc/update_inv_internal_storage()
+ if(isnull(internal_storage) || isnull(client) || !hud_used?.hud_shown)
+ return
+ internal_storage.screen_loc = ui_id
+ client.screen += internal_storage
+
+/mob/living/basic/guardian/dextrous/regenerate_icons()
+ update_inv_internal_storage()
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm b/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm
new file mode 100644
index 00000000000..db59c3209d0
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/explosive.dm
@@ -0,0 +1,77 @@
+/// A durable guardian which can convert objects into hidden explosives.
+/mob/living/basic/guardian/explosive
+ guardian_type = GUARDIAN_EXPLOSIVE
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ damage_coeff = list(BRUTE = 0.6, BURN = 0.6, TOX = 0.6, CLONE = 0.6, STAMINA = 0, OXY = 0.6)
+ range = 13
+ playstyle_string = span_holoparasite("As an explosive type, you have moderate close combat abilities and are capable of converting nearby items and objects into disguised bombs via right-click.")
+ creator_name = "Explosive"
+ creator_desc = "High damage resist and medium power attack. Can turn any object, including objects too large to pick up, into a bomb, dealing explosive damage to the next person to touch it. The object will return to normal after the trap is triggered or after a delay."
+ creator_icon = "explosive"
+ /// Ability which plants bombs
+ var/datum/action/cooldown/mob_cooldown/explosive_booby_trap/bomb
+
+/mob/living/basic/guardian/explosive/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ bomb = new(src)
+ bomb.Grant(src)
+
+/mob/living/basic/guardian/explosive/Destroy()
+ QDEL_NULL(bomb)
+ return ..()
+
+/mob/living/basic/guardian/explosive/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
+ if(LAZYACCESS(modifiers, RIGHT_CLICK) && proximity_flag && isobj(attack_target))
+ bomb.Trigger(target = attack_target)
+ return
+ return ..()
+
+
+/// An ability which can turn an object into a bomb
+/datum/action/cooldown/mob_cooldown/explosive_booby_trap
+ name = "Explosive Trap"
+ desc = "Convert an inanimate object into a deadly and mostly undetectable explosive, triggered on touch."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "smoke"
+ cooldown_time = 20 SECONDS
+ melee_cooldown_time = 0 SECONDS
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ /// After this amount of time passses, bomb deactivates.
+ var/decay_time = 1 MINUTES
+ /// Static list of signals that activate the bomb.
+ var/static/list/boom_signals = list(COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_BUMPED, COMSIG_ATOM_ATTACK_HAND)
+
+/datum/action/cooldown/mob_cooldown/explosive_booby_trap/PreActivate(atom/target)
+ if (!isobj(target))
+ return FALSE
+ if (!owner.Adjacent(target))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/explosive_booby_trap/Activate(atom/target)
+ var/glow_colour = COLOR_RED
+ var/mob/living/basic/guardian/guardian_owner = owner
+ if (istype(guardian_owner))
+ glow_colour = guardian_owner.guardian_colour
+ target.AddComponent(\
+ /datum/component/direct_explosive_trap, \
+ saboteur = owner, \
+ expire_time = decay_time, \
+ glow_colour = glow_colour,\
+ explosive_checks = CALLBACK(src, PROC_REF(validate_target)), \
+ triggering_signals = boom_signals, \
+ )
+ target.balloon_alert(owner, "bomb planted")
+ StartCooldown()
+ return TRUE
+
+/// Validate that we should blow up on this thing, preferably not on one of our allies
+/datum/action/cooldown/mob_cooldown/explosive_booby_trap/proc/validate_target(mob/living/target)
+ if (target == owner)
+ return FALSE
+ var/mob/living/basic/guardian/guardian_owner = owner
+ if (!istype(guardian_owner))
+ return TRUE
+ return target != guardian_owner.summoner && !guardian_owner.shares_summoner(target)
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
new file mode 100644
index 00000000000..6790916be07
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/gaseous.dm
@@ -0,0 +1,159 @@
+/// Not particularly resistant, but versatile due to the selection of gases it can generate.
+/mob/living/basic/guardian/gaseous
+ guardian_type = GUARDIAN_GASEOUS
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 0)
+ range = 7
+ playstyle_string = span_holoparasite("As a gaseous type, you have only light damage resistance, but you can expel gas in an area. In addition, your punches cause sparks, and you make your summoner inflammable.")
+ creator_name = "Gaseous"
+ creator_desc = "Creates sparks on touch and continuously expels a gas of its choice. Automatically extinguishes the user if they catch on fire."
+ creator_icon = "gaseous"
+ toggle_button_type = /atom/movable/screen/guardian/toggle_mode/gases
+ /// Ability we use to select gases
+ var/datum/action/cooldown/mob_cooldown/expel_gas/gas
+ /// Rate of temperature stabilization per second.
+ var/temp_stabilization_rate = 0.1
+
+/mob/living/basic/guardian/gaseous/Initialize(mapload, theme)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_PRE_PRESSURE_PUSH, PROC_REF(pre_pressure_moved))
+ gas = new(src)
+ gas.owner_has_control = FALSE // It's nicely integrated with the Guardian UI, no need to have two buttons
+ gas.Grant(src)
+
+/mob/living/basic/guardian/gaseous/Destroy()
+ QDEL_NULL(gas)
+ return ..()
+
+/mob/living/basic/guardian/gaseous/toggle_modes()
+ gas.Trigger()
+
+/mob/living/basic/guardian/gaseous/set_summoner(mob/living/to_who, different_person)
+ . = ..()
+ if (QDELETED(src))
+ return
+ RegisterSignal(summoner, COMSIG_LIVING_IGNITED, PROC_REF(on_summoner_ignited))
+ RegisterSignal(summoner, COMSIG_LIVING_LIFE, PROC_REF(on_summoner_life))
+
+/mob/living/basic/guardian/gaseous/cut_summoner(different_person)
+ if (!isnull(summoner))
+ UnregisterSignal(summoner, list(COMSIG_LIVING_IGNITED, COMSIG_LIVING_LIFE))
+ return ..()
+
+/// Prevent our summoner from being on fire
+/mob/living/basic/guardian/gaseous/proc/on_summoner_ignited(mob/living/source)
+ SIGNAL_HANDLER
+ source.extinguish_mob()
+ source.set_fire_stacks(0, remove_wet_stacks = FALSE)
+
+/// Maintain our summoner at a stable body temperature
+/mob/living/basic/guardian/gaseous/proc/on_summoner_life(mob/living/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ source.adjust_bodytemperature(get_temp_change_amount((summoner.get_body_temp_normal() - summoner.bodytemperature), temp_stabilization_rate * seconds_per_tick))
+
+/mob/living/basic/guardian/gaseous/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if(!. || !isliving(target))
+ return
+ do_sparks(1, TRUE, target)
+
+/mob/living/basic/guardian/gaseous/recall_effects()
+ . = ..()
+ if(!isnull(summoner))
+ UnregisterSignal(summoner, COMSIG_ATOM_PRE_PRESSURE_PUSH)
+
+/mob/living/basic/guardian/gaseous/manifest_effects()
+ . = ..()
+ if (!isnull(summoner))
+ RegisterSignal(summoner, COMSIG_ATOM_PRE_PRESSURE_PUSH, PROC_REF(pre_pressure_moved))
+
+/// We stand firm in the face of gas
+/mob/living/basic/guardian/gaseous/proc/pre_pressure_moved(datum/source)
+ SIGNAL_HANDLER
+ return COMSIG_ATOM_BLOCKS_PRESSURE
+
+
+/// Expel a range of gases
+/datum/action/cooldown/mob_cooldown/expel_gas
+ name = "Release Gas"
+ desc = "Start or stop expelling a selected gas into the environment."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "smoke"
+ cooldown_time = 0 SECONDS // We're here for the interface not the cooldown
+ melee_cooldown_time = 0 SECONDS
+ click_to_activate = FALSE
+ /// Gas being expelled.
+ var/active_gas = null
+ /// Associative list of types of gases to moles we create every life tick.
+ var/static/list/possible_gases = list(
+ /datum/gas/oxygen = 50,
+ /datum/gas/nitrogen = 750, //overpressurizing is hard!.
+ /datum/gas/water_vapor = 1, //you need incredibly little water vapor for the effects to kick in
+ /datum/gas/nitrous_oxide = 15,
+ /datum/gas/carbon_dioxide = 50,
+ /datum/gas/plasma = 3,
+ /datum/gas/bz = 10,
+ )
+
+/datum/action/cooldown/mob_cooldown/expel_gas/Grant(mob/granted_to)
+ . = ..()
+ if (isnull(owner))
+ return
+ RegisterSignal(owner, COMSIG_GUARDIAN_RECALLED, PROC_REF(stop_gas))
+
+/datum/action/cooldown/mob_cooldown/expel_gas/Remove(mob/removed_from)
+ UnregisterSignal(owner, list(COMSIG_GUARDIAN_RECALLED, COMSIG_LIVING_LIFE))
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/expel_gas/Activate(atom/target)
+ StartCooldown(360 SECONDS)
+ // Regeneated each time just in case someone fucks with our list
+ var/list/gas_selection = list("None")
+ for(var/datum/gas/gas as anything in possible_gases)
+ gas_selection[initial(gas.name)] = gas
+
+ var/picked_gas = tgui_input_list(owner, "Select a gas to emit.", "Gas Producer", gas_selection)
+ StartCooldown()
+ if(picked_gas == "None")
+ stop_gas()
+ return
+
+ var/gas_type = gas_selection[picked_gas]
+ if(isnull(picked_gas) || isnull(gas_type))
+ return
+
+ to_chat(owner, span_bolddanger("You start releasing [picked_gas]."))
+ owner.investigate_log("set their gas type to [picked_gas].", INVESTIGATE_ATMOS)
+ var/had_gas = !isnull(active_gas)
+ active_gas = gas_type
+ if(isnull(owner.particles))
+ owner.particles = new /particles/smoke/steam()
+ owner.particles.position = list(-1, 8, 0)
+ owner.particles.fadein = 5
+ owner.particles.height = 200
+ var/datum/gas/chosen_gas = active_gas // Casting it so that we can access gas vars in initial, it's still a typepath
+ owner.particles.color = initial(chosen_gas.primary_color)
+ if (!had_gas)
+ RegisterSignal(owner, COMSIG_LIVING_LIFE, PROC_REF(on_life))
+
+/// Turns off the gas
+/datum/action/cooldown/mob_cooldown/expel_gas/proc/stop_gas()
+ SIGNAL_HANDLER
+ if (!isnull(active_gas))
+ to_chat(src, span_notice("You stop releasing gas."))
+ active_gas = null
+ QDEL_NULL(owner.particles)
+ UnregisterSignal(owner, COMSIG_LIVING_LIFE)
+
+/// Release gas every life tick while active
+/datum/action/cooldown/mob_cooldown/expel_gas/proc/on_life(datum/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ if (isnull(active_gas))
+ return // We shouldn't even be registered at this point but just in case
+ var/datum/gas_mixture/mix_to_spawn = new()
+ mix_to_spawn.add_gas(active_gas)
+ mix_to_spawn.gases[active_gas][MOLES] = possible_gases[active_gas] * seconds_per_tick
+ mix_to_spawn.temperature = T20C
+ var/turf/open/our_turf = get_turf(owner)
+ our_turf.assume_air(mix_to_spawn)
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/gravitokinetic.dm b/code/modules/mob/living/basic/guardian/guardian_types/gravitokinetic.dm
new file mode 100644
index 00000000000..a0ad9c8c21b
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/gravitokinetic.dm
@@ -0,0 +1,107 @@
+/// Somewhat durable guardian who can increase gravity in an area
+/mob/living/basic/guardian/gravitokinetic
+ guardian_type = GUARDIAN_GRAVITOKINETIC
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
+ playstyle_string = span_holoparasite("As a gravitokinetic type, you can right-click to make the gravity on the ground stronger, and punching applies this effect to a target.")
+ creator_name = "Gravitokinetic"
+ creator_desc = "Attacks will apply crushing gravity to the target. Can target the ground as well to slow targets advancing on you, but you are not immune to your own such effects."
+ creator_icon = "gravitokinetic"
+ /// Targets we have applied our gravity effects on.
+ var/list/gravity_targets = list()
+ /// Distance at which our ability works
+ var/gravity_power_range = 10
+ /// Gravity added on punches.
+ var/punch_gravity = 5
+ /// Gravity added to turfs.
+ var/turf_gravity = 3
+
+/mob/living/basic/guardian/gravitokinetic/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ AddElement(/datum/element/forced_gravity, 1)
+
+ var/static/list/container_connections = list(
+ COMSIG_MOVABLE_MOVED = PROC_REF(on_moved),
+ )
+ AddComponent(/datum/component/connect_containers, src, container_connections)
+ RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+
+/mob/living/basic/guardian/gravitokinetic/set_summoner(mob/living/to_who, different_person)
+ . = ..()
+ if (!QDELETED(src))
+ return
+ to_who.AddElement(/datum/element/forced_gravity, 1)
+
+/mob/living/basic/guardian/gravitokinetic/cut_summoner(different_person)
+ summoner?.RemoveElement(/datum/element/forced_gravity, 1)
+ return ..()
+
+/mob/living/basic/guardian/gravitokinetic/death(gibbed)
+ . = ..()
+ clear_gravity()
+
+/mob/living/basic/guardian/gravitokinetic/recall_effects()
+ . = ..()
+ if (length(gravity_targets))
+ to_chat(src, span_bolddanger("You have released your gravitokinetic powers!"))
+ clear_gravity()
+
+/mob/living/basic/guardian/gravitokinetic/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!. || !isliving(target) || target == src || target == summoner || shares_summoner(target) || gravity_targets[target])
+ return
+ to_chat(src, span_bolddanger("Your punch has applied heavy gravity to [target]!"))
+ add_gravity(target, punch_gravity)
+ to_chat(target, span_userdanger("Everything feels really heavy!"))
+ return TRUE
+
+/mob/living/basic/guardian/gravitokinetic/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
+ if (LAZYACCESS(modifiers, RIGHT_CLICK) && proximity_flag && !gravity_targets[attack_target])
+ slam_turf(attack_target)
+ return
+ return ..()
+
+/// Apply forced gravity to the floor
+/mob/living/basic/guardian/gravitokinetic/proc/slam_turf(turf/open/slammed)
+ if (!isopenturf(slammed) || isgroundlessturf(slammed))
+ return
+ visible_message(span_danger("[src] slams their fist into the [slammed]!"), span_notice("You amplify gravity around the [slammed]."))
+ do_attack_animation(slammed)
+ add_gravity(slammed, turf_gravity)
+
+/// Remove our forced gravity from all targets
+/mob/living/basic/guardian/gravitokinetic/proc/clear_gravity()
+ for(var/gravity_target in gravity_targets)
+ remove_gravity(gravity_target)
+
+/// Make something heavier
+/mob/living/basic/guardian/gravitokinetic/proc/add_gravity(atom/target, new_gravity = 3)
+ if (gravity_targets[target])
+ return
+ target.AddElement(/datum/element/forced_gravity, new_gravity)
+ gravity_targets[target] = new_gravity
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_target_moved))
+ playsound(src, 'sound/effects/gravhit.ogg', 100, TRUE)
+
+/// Stop making something heavier
+/mob/living/basic/guardian/gravitokinetic/proc/remove_gravity(atom/target, too_far = FALSE)
+ if (isnull(gravity_targets[target]))
+ return
+ if (too_far)
+ to_chat(src, span_bolddanger("You are too far away from [target] to amplify gravity's hold on them!"))
+ UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
+ target.RemoveElement(/datum/element/forced_gravity, gravity_targets[target])
+ gravity_targets -= target
+
+/// When we or something we are inside move check if we are now too far away
+/mob/living/basic/guardian/gravitokinetic/proc/on_moved()
+ for(var/gravity_target in gravity_targets)
+ if (get_dist(src, gravity_target) > gravity_power_range)
+ remove_gravity(gravity_target, too_far = TRUE)
+
+/// When something we put gravity on moves check if it's too far away
+/mob/living/basic/guardian/gravitokinetic/proc/on_target_moved(atom/movable/moving_target, old_loc, dir, forced)
+ SIGNAL_HANDLER
+ if (get_dist(src, moving_target) > gravity_power_range)
+ remove_gravity(moving_target, too_far = TRUE)
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/lightning.dm b/code/modules/mob/living/basic/guardian/guardian_types/lightning.dm
new file mode 100644
index 00000000000..e25184372db
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/lightning.dm
@@ -0,0 +1,95 @@
+/// A reasonably durable guardian linked to you by a chain of lightning, zapping people who get between you
+/mob/living/basic/guardian/lightning
+ guardian_type = GUARDIAN_LIGHTNING
+ melee_damage_lower = 7
+ melee_damage_upper = 7
+ attack_verb_continuous = "shocks"
+ attack_verb_simple = "shock"
+ melee_damage_type = BURN
+ attack_sound = 'sound/machines/defib_zap.ogg'
+ damage_coeff = list(BRUTE = 0.7, BURN = 0.7, TOX = 0.7, CLONE = 0.7, STAMINA = 0, OXY = 0.7)
+ range = 7
+ playstyle_string = span_holoparasite("As a lightning type, you will apply lightning chains to targets on attack and have a lightning chain to your summoner. Lightning chains will shock anyone near them.")
+ creator_name = "Lightning"
+ creator_desc = "Attacks apply lightning chains to targets. Has a lightning chain to the user. Lightning chains shock everything near them, doing constant damage."
+ creator_icon = "lightning"
+ /// Link between us and our summoner
+ var/datum/component/summoner_chain
+ /// Associative list of chained enemies to their chains
+ var/list/enemy_chains
+
+/mob/living/basic/guardian/lightning/death(gibbed)
+ . = ..()
+ clear_chains()
+
+/mob/living/basic/guardian/lightning/Destroy()
+ clear_chains()
+ return ..()
+
+/mob/living/basic/guardian/lightning/manifest_effects()
+ . = ..()
+ if (isnull(summoner))
+ return
+ summoner_chain = chain_to(summoner, max_range = INFINITY) // Functionally it's actually our leash range but admins might fuck with it
+
+/mob/living/basic/guardian/lightning/recall_effects()
+ . = ..()
+ clear_chains()
+
+/// Remove all of our chains
+/mob/living/basic/guardian/lightning/proc/clear_chains()
+ QDEL_NULL(summoner_chain)
+ QDEL_LIST_ASSOC_VAL(enemy_chains)
+
+/mob/living/basic/guardian/lightning/melee_attack(atom/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!. || !validate_target(target) || (target in enemy_chains))
+ return
+ if (length(enemy_chains) == 2)
+ var/old_target = enemy_chains[1]
+ var/datum/old_chain = enemy_chains[old_target]
+ qdel(old_chain)
+ var/datum/new_chain = chain_to(target)
+ RegisterSignal(new_chain, COMSIG_QDELETING, PROC_REF(on_chain_deleted))
+ LAZYADDASSOC(enemy_chains, target, new_chain)
+
+/// Create a damaging lightning chain between ourselves and a target
+/mob/living/basic/guardian/lightning/proc/chain_to(atom/target, max_range = 7)
+ var/datum/component/chain = AddComponent(\
+ /datum/component/damage_chain, \
+ linked_to = target, \
+ max_distance = max_range, \
+ beam_state = "lightning[rand(1,12)]", \
+ beam_type = /obj/effect/ebeam/chain, \
+ validate_target = CALLBACK(src, PROC_REF(validate_target)), \
+ chain_damage_feedback = CALLBACK(src, PROC_REF(on_chain_zap)), \
+ )
+ return chain
+
+/// Handle losing our reference when we delete a chain
+/mob/living/basic/guardian/lightning/proc/on_chain_deleted(datum/source)
+ SIGNAL_HANDLER
+ for (var/target in enemy_chains)
+ if (enemy_chains[target] != source)
+ continue
+ enemy_chains -= target
+ return
+
+/// Confirm whether something is valid to zap with lightning
+/mob/living/basic/guardian/lightning/proc/validate_target(atom/target)
+ return isliving(target) && target != src && target != summoner && !shares_summoner(target)
+
+/// Called every few zaps by a chain
+/mob/living/basic/guardian/lightning/proc/on_chain_zap(mob/living/target)
+ target.electrocute_act(shock_damage = 0, source = "lightning chain")
+ target.visible_message(
+ span_danger("[target] was shocked by the lightning chain!"),
+ span_userdanger("You are shocked by the lightning chain!"),
+ span_hear("You hear a heavy electrical crack."),
+ )
+
+/// Beam definition for our lightning chain
+/obj/effect/ebeam/chain
+ name = "lightning chain"
+ layer = LYING_MOB_LAYER
+ plane = GAME_PLANE_FOV_HIDDEN
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/protector.dm b/code/modules/mob/living/basic/guardian/guardian_types/protector.dm
new file mode 100644
index 00000000000..a0aa34ad17f
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/protector.dm
@@ -0,0 +1,122 @@
+/// Very durable, and reverses the usual leash dynamic. Can slow down to become extremely durable.
+/mob/living/basic/guardian/protector
+ guardian_type = GUARDIAN_PROTECTOR
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ range = 5 // You want this to be low so you can drag them around
+ damage_coeff = list(BRUTE = 0.4, BURN = 0.4, TOX = 0.4, CLONE = 0.4, STAMINA = 0, OXY = 0.4)
+ playstyle_string = span_holoparasite("As a protector type you cause your summoner to leash to you instead of you leashing to them and have two modes; Combat Mode, where you do and take medium damage, and Protection Mode, where you do and take almost no damage, but move slightly slower.")
+ creator_name = "Protector"
+ creator_desc = "Causes you to teleport to it when out of range, unlike other parasites. Has two modes; Combat, where it does and takes medium damage, and Protection, where it does and takes almost no damage but moves slightly slower."
+ creator_icon = "protector"
+ toggle_button_type = /atom/movable/screen/guardian/toggle_mode
+ /// Action which toggles our shield
+ var/datum/action/cooldown/mob_cooldown/protector_shield/shield
+
+/mob/living/basic/guardian/protector/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ shield = new(src)
+ shield.owner_has_control = FALSE // Hide it from the user, it's integrated with guardian UI
+ shield.Grant(src)
+
+/mob/living/basic/guardian/protector/Destroy()
+ QDEL_NULL(shield)
+ return ..()
+
+// Invert the order
+/mob/living/basic/guardian/protector/leash_to(atom/movable/leashed, atom/movable/leashed_to)
+ return ..(leashed_to, leashed)
+
+/mob/living/basic/guardian/protector/unleash()
+ qdel(summoner?.GetComponent(/datum/component/leash))
+
+/mob/living/basic/guardian/protector/toggle_modes()
+ shield.Trigger()
+
+/mob/living/basic/guardian/protector/ex_act(severity)
+ if(severity >= EXPLODE_DEVASTATE)
+ adjustBruteLoss(400) //if in protector mode, will do 20 damage and not actually necessarily kill the summoner
+ return TRUE
+ return ..()
+
+/// Toggle a status effect which makes you slow but defensive
+/datum/action/cooldown/mob_cooldown/protector_shield
+ name = "Protection Mode"
+ desc = "Enter a defensive stance which slows you down and reduces your damage, but makes you almost invincible."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "shield-old"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ cooldown_time = 1 SECONDS
+ click_to_activate = FALSE
+
+/datum/action/cooldown/mob_cooldown/protector_shield/Activate(mob/living/target)
+ if (!isliving(target))
+ return FALSE
+ if (target.has_status_effect(/datum/status_effect/protector_shield))
+ target.remove_status_effect(/datum/status_effect/protector_shield)
+ return
+ target.apply_status_effect(/datum/status_effect/protector_shield)
+ StartCooldown()
+ return TRUE
+
+/// Makes the guardian even more durable, but slower
+/datum/status_effect/protector_shield
+ id = "guardian_shield"
+ alert_type = null
+ /// Damage removed in protecting mode.
+ var/damage_penalty = 13
+ /// Colour for our various overlays.
+ var/overlay_colour = COLOR_TEAL
+ /// Overlay for our protection shield.
+ var/mutable_appearance/shield_overlay
+ /// Damage coefficients when shielded
+ var/list/shielded_damage = list(BRUTE = 0.05, BURN = 0.05, TOX = 0.05, CLONE = 0.05, STAMINA = 0, OXY = 0.05)
+
+/datum/status_effect/protector_shield/on_apply()
+ if (isguardian(owner))
+ var/mob/living/basic/guardian/guardian_owner = owner
+ overlay_colour = guardian_owner.guardian_colour
+ shield_overlay = mutable_appearance('icons/effects/effects.dmi', "shield-grey")
+ shield_overlay.color = overlay_colour
+
+ owner.melee_damage_lower -= damage_penalty
+ owner.melee_damage_upper -= damage_penalty
+ owner.add_movespeed_modifier(/datum/movespeed_modifier/status_effect/guardian_shield)
+
+ if (isbasicmob(owner)) // Better hope you are or this status is doing basically nothing useful for you
+ var/mob/living/basic/basic_owner = owner
+ basic_owner.damage_coeff = shielded_damage
+
+ to_chat(owner, span_bolddanger("You enter protection mode."))
+ RegisterSignal(owner, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
+ RegisterSignals(owner, COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES, PROC_REF(on_health_changed))
+ owner.update_appearance(UPDATE_ICON)
+ return TRUE
+
+/datum/status_effect/protector_shield/on_remove()
+ owner.melee_damage_lower += damage_penalty
+ owner.melee_damage_upper += damage_penalty
+ owner.remove_movespeed_modifier(/datum/movespeed_modifier/status_effect/guardian_shield)
+
+ if (isbasicmob(owner))
+ var/mob/living/basic/basic_owner = owner
+ basic_owner.damage_coeff = initial(basic_owner.damage_coeff)
+
+ to_chat(owner, span_bolddanger("You return to your normal mode."))
+ UnregisterSignal(owner, list(COMSIG_ATOM_UPDATE_OVERLAYS) + COMSIG_LIVING_ADJUST_STANDARD_DAMAGE_TYPES)
+ owner.update_appearance(UPDATE_ICON)
+
+/// Show an extra overlay when we're in shield mode
+/datum/status_effect/protector_shield/proc/on_update_overlays(atom/source, list/overlays)
+ SIGNAL_HANDLER
+ overlays += shield_overlay
+
+/// Flash an animation when someone tries to hurt us
+/datum/status_effect/protector_shield/proc/on_health_changed(mob/living/our_mob, type, amount, forced)
+ SIGNAL_HANDLER
+ if (amount <= 0 && !QDELETED(our_mob))
+ return
+ var/image/flash_overlay = new('icons/effects/effects.dmi', owner, "shield-flash", dir = pick(GLOB.cardinals))
+ flash_overlay.color = overlay_colour
+ owner.flick_overlay_view(flash_overlay, 0.5 SECONDS)
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm b/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm
new file mode 100644
index 00000000000..293dbfc2320
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/ranged.dm
@@ -0,0 +1,205 @@
+/// A ranged guardian can fling shards of glass at people very very quickly. It can also enter a long-range scouting mode.
+/mob/living/basic/guardian/ranged
+ guardian_type = GUARDIAN_RANGED
+ combat_mode = FALSE
+ friendly_verb_continuous = "quietly assesses"
+ friendly_verb_simple = "quietly assess"
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ damage_coeff = list(BRUTE = 0.9, BURN = 0.9, TOX = 0.9, CLONE = 0.9, STAMINA = 0, OXY = 0.9)
+ range = 13
+ playstyle_string = span_holoparasite("As a ranged type, you have only light damage resistance, but are capable of spraying shards of crystal at incredibly high speed. You can also deploy surveillance snares to monitor enemy movement. Finally, you can switch to scout mode, in which you can't attack, but can move without limit.")
+ creator_name = "Ranged"
+ creator_desc = "Has two modes. Ranged; which fires a constant stream of weak, armor-ignoring projectiles. Scout; where it cannot attack, but can move through walls and is quite hard to see. Can lay surveillance snares, which alert it when crossed, in either mode."
+ creator_icon = "ranged"
+ see_invisible = SEE_INVISIBLE_LIVING
+ toggle_button_type = /atom/movable/screen/guardian/toggle_mode
+
+/mob/living/basic/guardian/ranged/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ AddComponent(\
+ /datum/component/ranged_attacks,\
+ projectile_type = /obj/projectile/guardian,\
+ projectile_sound = 'sound/effects/hit_on_shattered_glass.ogg',\
+ cooldown_time = 0.1 SECONDS, \
+ )
+ AddComponent(/datum/component/ranged_mob_full_auto, autofire_shot_delay = 0.1 SECONDS)
+ var/datum/action/cooldown/mob_cooldown/guardian_alarm_snare/snare = new (src)
+ snare.Grant(src)
+
+/mob/living/basic/guardian/ranged/toggle_modes()
+ if(is_deployed() && !isnull(summoner))
+ balloon_alert(src, "must not be manifested!")
+ return
+ if (has_status_effect(/datum/status_effect/guardian_scout_mode))
+ remove_status_effect(/datum/status_effect/guardian_scout_mode)
+ return
+ apply_status_effect(/datum/status_effect/guardian_scout_mode)
+
+/mob/living/basic/guardian/ranged/toggle_light()
+ var/msg
+ switch(lighting_cutoff)
+ if (LIGHTING_CUTOFF_VISIBLE)
+ lighting_cutoff_red = 10
+ lighting_cutoff_green = 10
+ lighting_cutoff_blue = 15
+ msg = "You activate your night vision."
+ if (LIGHTING_CUTOFF_MEDIUM)
+ lighting_cutoff_red = 25
+ lighting_cutoff_green = 25
+ lighting_cutoff_blue = 35
+ msg = "You increase your night vision."
+ if (LIGHTING_CUTOFF_HIGH)
+ lighting_cutoff_red = 35
+ lighting_cutoff_green = 35
+ lighting_cutoff_blue = 50
+ msg = "You maximize your night vision."
+ else
+ lighting_cutoff_red = 0
+ lighting_cutoff_green = 0
+ lighting_cutoff_blue = 0
+ msg = "You deactivate your night vision."
+ sync_lighting_plane_cutoff()
+ to_chat(src, span_notice(msg))
+
+/// Become an incorporeal scout
+/datum/status_effect/guardian_scout_mode
+ id = "guardian_scout"
+ alert_type = null
+
+/datum/status_effect/guardian_scout_mode/on_apply()
+ animate(owner, alpha = 45, time = 0.5 SECONDS)
+ RegisterSignal(owner, COMSIG_GUARDIAN_MANIFESTED, PROC_REF(on_manifest))
+ RegisterSignal(owner, COMSIG_GUARDIAN_RECALLED, PROC_REF(on_recall))
+ RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(on_click))
+
+ var/mob/living/basic/guardian/guardian_mob = owner
+ guardian_mob.unleash()
+ to_chat(owner, span_bolddanger("You enter scouting mode."))
+ return TRUE
+
+/datum/status_effect/guardian_scout_mode/on_remove()
+ animate(owner, alpha = initial(owner.alpha), time = 0.5 SECONDS)
+ UnregisterSignal(owner, list(COMSIG_GUARDIAN_MANIFESTED, COMSIG_GUARDIAN_RECALLED, COMSIG_MOB_CLICKON))
+ to_chat(owner, span_bolddanger("You return to your normal mode."))
+ var/mob/living/basic/guardian/guardian_mob = owner
+ guardian_mob.leash_to(owner, guardian_mob.summoner)
+
+/// Restore incorporeal move when we become corporeal, yes I know that suonds silly
+/datum/status_effect/guardian_scout_mode/proc/on_manifest()
+ SIGNAL_HANDLER
+ owner.incorporeal_move = INCORPOREAL_MOVE_BASIC
+
+/// Stop having incorporeal move when we recall so that we can't move
+/datum/status_effect/guardian_scout_mode/proc/on_recall()
+ SIGNAL_HANDLER
+ owner.incorporeal_move = FALSE
+
+/// While this is active we can't click anything
+/datum/status_effect/guardian_scout_mode/proc/on_click()
+ SIGNAL_HANDLER
+ return COMSIG_MOB_CANCEL_CLICKON
+
+
+/// Place an invisible trap which alerts the guardian when it is crossed
+/datum/action/cooldown/mob_cooldown/guardian_alarm_snare
+ name = "Surveillance Snare"
+ desc = "Place an invisible snare which will alert you when it is crossed."
+ button_icon = 'icons/mob/actions/actions_ecult.dmi'
+ button_icon_state = "eye"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ cooldown_time = 2 SECONDS
+ melee_cooldown_time = 0
+ click_to_activate = FALSE
+ /// How many snares can we have?
+ var/maximum_snares = 5
+ /// What snares have we already placed?
+ var/list/placed_snares = list()
+
+/datum/action/cooldown/mob_cooldown/guardian_alarm_snare/Activate(atom/target)
+ StartCooldown(360 SECONDS)
+
+ if (length(placed_snares) >= maximum_snares)
+ var/picked_snare = tgui_input_list(owner, "Choose a snare to replace.", "Remove Snare", sort_names(placed_snares))
+ if(isnull(picked_snare))
+ return FALSE
+ qdel(picked_snare)
+ if (length(placed_snares) >= maximum_snares)
+ StartCooldown(0)
+ return FALSE
+
+ owner.balloon_alert(owner, "snare deployed") // We need feedback because they are invisible
+ var/turf/snare_loc = get_turf(owner)
+ var/obj/effect/abstract/surveillance_snare/new_snare = new(snare_loc, owner)
+ new_snare.assign_owner(owner)
+ RegisterSignal(new_snare, COMSIG_QDELETING, PROC_REF(on_snare_deleted))
+ placed_snares += new_snare
+
+ StartCooldown()
+ return TRUE
+
+/// When a snare is deleted remove it from tracking
+/datum/action/cooldown/mob_cooldown/guardian_alarm_snare/proc/on_snare_deleted(atom/snare)
+ SIGNAL_HANDLER
+ placed_snares -= snare
+
+
+/// An invisible marker placed by a ranged guardian, alerts the owner when crossed
+/obj/effect/abstract/surveillance_snare
+ name = "surveillance snare"
+ desc = "This thing is invisible, how are you examining it?"
+ invisibility = INVISIBILITY_ABSTRACT
+ /// Who do we notify when someone steps on us?
+ var/mob/living/owner
+
+/obj/effect/abstract/surveillance_snare/Initialize(mapload, spawning_guardian)
+ . = ..()
+ name = "[get_area(src)] snare ([rand(1, 1000)])"
+ var/static/list/loc_connections = list(COMSIG_ATOM_ENTERED = PROC_REF(on_entered))
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/// Set up crossed notification
+/obj/effect/abstract/surveillance_snare/proc/assign_owner(mob/living/new_owner)
+ if (isnull(new_owner))
+ qdel(src)
+ return
+ owner = new_owner
+ RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(owner_destroyed))
+
+/// When crossed notify our owner
+/obj/effect/abstract/surveillance_snare/proc/on_entered(atom/source, crossed_object)
+ SIGNAL_HANDLER
+ if (isnull(owner))
+ qdel(src)
+ return
+ if (!isliving(crossed_object) || crossed_object == owner)
+ return
+ var/mob/living/basic/guardian/guardian_owner = owner
+ if (isguardian(owner) && crossed_object == guardian_owner.summoner || guardian_owner.shares_summoner(crossed_object))
+ return
+
+ var/send_message = span_bolddanger("[crossed_object] has crossed [name].")
+ if (!isguardian(owner) || isnull(guardian_owner.summoner))
+ to_chat(owner, send_message)
+ return
+
+ to_chat(guardian_owner.summoner, send_message)
+ var/list/guardians = guardian_owner.summoner.get_all_linked_holoparasites()
+ for(var/guardian in guardians)
+ to_chat(guardian, send_message)
+
+/// If the person who placed us doesn't exist we might as well die
+/obj/effect/abstract/surveillance_snare/proc/owner_destroyed()
+ SIGNAL_HANDLER
+ owner = null
+ qdel(src)
+
+
+/// The glass shards we throw as a guardian. They have low damage because you can fire them very very quickly.
+/obj/projectile/guardian
+ name = "crystal spray"
+ icon_state = "guardian"
+ damage = 5
+ damage_type = BRUTE
+ armour_penetration = 100
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/standard.dm b/code/modules/mob/living/basic/guardian/guardian_types/standard.dm
new file mode 100644
index 00000000000..2ca006b385c
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/standard.dm
@@ -0,0 +1,63 @@
+/// Plain, but durable and strong. Can destroy walls.
+/mob/living/basic/guardian/standard
+ guardian_type = GUARDIAN_STANDARD
+ damage_coeff = list(BRUTE = 0.5, BURN = 0.5, TOX = 0.5, CLONE = 0.5, STAMINA = 0, OXY = 0.5)
+ melee_damage_lower = 20
+ melee_damage_upper = 20
+ melee_attack_cooldown = 0.6 SECONDS
+ wound_bonus = -5 //you can wound!
+ obj_damage = 80
+ environment_smash = ENVIRONMENT_SMASH_WALLS
+ playstyle_string = span_holoparasite("As a standard type you have no special abilities, but have a high damage resistance and a powerful attack capable of smashing through walls.")
+ creator_name = "Standard"
+ creator_desc = "Devastating close combat attacks and high damage resistance. Can smash through weak walls."
+ creator_icon = "standard"
+ /// The text we shout when attacking.
+ var/battlecry = "AT"
+
+/mob/living/basic/guardian/standard/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE, tear_time = 1.5 SECONDS)
+ var/datum/action/select_guardian_battlecry/cry = new(src)
+ cry.Grant(src)
+
+/mob/living/basic/guardian/standard/do_attack_animation(atom/attacked_atom, visual_effect_icon, used_item, no_effect)
+ . = ..()
+ if (!isliving(attacked_atom) || !isclosedturf(attacked_atom))
+ return
+ var/msg = ""
+ for(var/i in 1 to 9)
+ msg += battlecry
+ say("[msg]!!", ignore_spam = TRUE)
+ for(var/sounds in 1 to 4)
+ addtimer(CALLBACK(src, PROC_REF(do_attack_sound), attacked_atom.loc), sounds DECISECONDS, TIMER_DELETE_ME)
+
+/// Echo our punching sounds
+/mob/living/basic/guardian/standard/proc/do_attack_sound(atom/playing_from)
+ playsound(playing_from, attack_sound, 50, TRUE, TRUE)
+
+/// Action to change our battlecry
+/datum/action/select_guardian_battlecry
+ name = "Select Battlecry"
+ desc = "Update the really cool thing you shout whenever you attack."
+ button_icon = 'icons/obj/clothing/gloves.dmi'
+ button_icon_state = "boxing"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ /// How long can it be? Shouldn't be too long because we repeat this a shitload of times
+ var/max_length = 6
+
+/datum/action/select_guardian_battlecry/IsAvailable(feedback)
+ if (!istype(owner, /mob/living/basic/guardian/standard))
+ return FALSE
+ return ..()
+
+/datum/action/select_guardian_battlecry/Trigger(trigger_flags)
+ . = ..()
+ if (!.)
+ return
+ var/mob/living/basic/guardian/standard/stand = owner
+ var/input = tgui_input_text(owner, "What do you want your battlecry to be?", "Battle Cry", max_length = max_length)
+ if(!input)
+ return
+ stand.battlecry = input
diff --git a/code/modules/mob/living/basic/guardian/guardian_types/support.dm b/code/modules/mob/living/basic/guardian/guardian_types/support.dm
new file mode 100644
index 00000000000..e22762bcada
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_types/support.dm
@@ -0,0 +1,164 @@
+/// Quick-moving mob which can teleport things to a beacon and heal its allies
+/mob/living/basic/guardian/support
+ guardian_type = GUARDIAN_SUPPORT
+ speed = 0
+ damage_coeff = list(BRUTE = 0.7, BURN = 0.7, TOX = 0.7, CLONE = 0.7, STAMINA = 0, OXY = 0.7)
+ melee_damage_lower = 15
+ melee_damage_upper = 15
+ playstyle_string = span_holoparasite("As a support type, you may right-click to heal targets. In addition, alt-clicking on an adjacent object or mob will warp them to your bluespace beacon after a short delay.")
+ creator_name = "Support"
+ creator_desc = "Does medium damage, but can heal its targets and create beacons to teleport people and things to."
+ creator_icon = "support"
+ /// Amount of each damage type to heal per hit
+ var/healing_amount = 5
+
+/mob/living/basic/guardian/support/Initialize(mapload, datum/guardian_fluff/theme)
+ . = ..()
+ AddComponent(\
+ /datum/component/healing_touch,\
+ heal_brute = healing_amount,\
+ heal_burn = healing_amount,\
+ heal_tox = healing_amount,\
+ heal_oxy = healing_amount,\
+ heal_time = 0,\
+ action_text = "",\
+ complete_text = "",\
+ required_modifier = RIGHT_CLICK,\
+ after_healed = CALLBACK(src, PROC_REF(after_healed)),\
+ )
+
+ var/datum/atom_hud/medsensor = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED]
+ medsensor.show_to(src)
+
+ var/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/teleport = new(src)
+ teleport.Grant(src)
+
+/mob/living/basic/guardian/support/set_guardian_colour(colour)
+ . = ..()
+ AddComponent(/datum/component/healing_touch, heal_color = guardian_colour)
+
+/// Called after we heal someone, show some visuals
+/mob/living/basic/guardian/support/proc/after_healed(mob/living/healed)
+ do_attack_animation(healed, ATTACK_EFFECT_PUNCH)
+ healed.visible_message(
+ message = span_notice("[src] heals [healed]!"),
+ self_message = span_userdanger("[src] heals you!"),
+ vision_distance = COMBAT_MESSAGE_RANGE,
+ ignored_mobs = src,
+ )
+ to_chat(src, span_notice("You heal [healed]!"))
+ playsound(healed, attack_sound, 50, TRUE, TRUE, frequency = -1) // play punch sound in REVERSE
+
+
+/// Place a beacon and then listen for clicks to teleport people to it
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon
+ name = "Place Bluespace Beacon"
+ desc = "Mark the ground under your feet as a teleportation point. Alt-click things to teleport them to your beacon."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "the_freezer"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ cooldown_time = 5 MINUTES
+ melee_cooldown_time = 0
+ cooldown_rounding = 1
+ click_to_activate = FALSE
+ /// Our teleportation beacon.
+ var/obj/structure/guardian_beacon/beacon
+ /// Time it takes to teleport something.
+ var/teleport_time = 6 SECONDS
+
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(owner, COMSIG_MOB_ALTCLICKON, PROC_REF(try_teleporting))
+
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/Remove(mob/removed_from)
+ UnregisterSignal(owner, COMSIG_MOB_ALTCLICKON)
+ return ..()
+
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/Activate(atom/movable/target)
+ var/turf/beacon_loc = owner.loc
+ if(!isfloorturf(beacon_loc))
+ owner.balloon_alert(owner, "no room!")
+ return FALSE
+
+ if (!isnull(beacon))
+ beacon.visible_message("[beacon] vanishes!")
+ new /obj/effect/temp_visual/guardian/phase/out(beacon.loc)
+ qdel(beacon)
+
+ beacon = new(beacon_loc, src)
+ if (isguardian(owner))
+ var/mob/living/basic/guardian/guardian_owner = owner
+ beacon.add_atom_colour(guardian_owner.guardian_colour, FIXED_COLOUR_PRIORITY)
+ RegisterSignal(beacon, COMSIG_QDELETING, PROC_REF(on_beacon_deleted))
+ to_chat(src, span_bolddanger("Beacon placed! You may now warp targets and objects to it, including your user, via Alt+Click."))
+ StartCooldown()
+ return TRUE
+
+/// Don't hold a reference to a deleted beacon
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/on_beacon_deleted()
+ SIGNAL_HANDLER
+ beacon = null
+
+/// Try and teleport something to our beacon
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/try_teleporting(mob/living/source, atom/target)
+ SIGNAL_HANDLER
+ if (!can_teleport(source, target))
+ return
+ INVOKE_ASYNC(src, PROC_REF(perform_teleport), source, target)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Validate whether we can teleport this object
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/can_teleport(mob/living/source, atom/movable/target)
+ if (isnull(beacon))
+ source.balloon_alert(source, "no beacon!")
+ return FALSE
+ if (isguardian(source))
+ var/mob/living/basic/guardian/guardian_mob = source
+ if (!guardian_mob.is_deployed())
+ source.balloon_alert(source, "manifest yourself!")
+ return FALSE
+ if (!source.Adjacent(target))
+ target.balloon_alert(source, "too far!")
+ return FALSE
+ if (target.anchored)
+ target.balloon_alert(source, "it won't budge!")
+ return FALSE
+ if(beacon.z != target.z)
+ target.balloon_alert(source, "too far from beacon!")
+ return FALSE
+ return TRUE
+
+/// Start teleporting
+/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/perform_teleport(mob/living/source, atom/target)
+ source.do_attack_animation(target)
+ playsound(target, 'sound/weapons/punch1.ogg', 50, TRUE, TRUE, frequency = -1)
+ source.balloon_alert(source, "teleporting...")
+ target.visible_message(
+ span_danger("[target] starts to glow faintly!"), \
+ span_userdanger("You start to faintly glow, and you feel strangely weightless!"))
+ if(!do_after(source, teleport_time, target))
+ return
+ new /obj/effect/temp_visual/guardian/phase/out(target.loc)
+ if(isliving(target))
+ var/mob/living/living_target = target
+ living_target.flash_act()
+ target.visible_message(
+ span_danger("[target] disappears in a flash of light!"), \
+ span_userdanger("Your vision is obscured by a flash of light!"), \
+ )
+ do_teleport(target, beacon, precision = 0, channel = TELEPORT_CHANNEL_BLUESPACE)
+ new /obj/effect/temp_visual/guardian/phase(get_turf(target))
+
+
+/// Structure which acts as the landing point for a support guardian's teleportation effects
+/obj/structure/guardian_beacon
+ name = "guardian beacon"
+ icon = 'icons/turf/floors.dmi'
+ desc = "A glowing zone which acts as a beacon for teleportation."
+ icon_state = "light_on-8"
+ light_range = MINIMUM_USEFUL_LIGHT_RANGE
+ density = FALSE
+ anchored = TRUE
+ plane = FLOOR_PLANE
+ layer = ABOVE_OPEN_TURF_LAYER
diff --git a/code/modules/mob/living/basic/guardian/guardian_verbs.dm b/code/modules/mob/living/basic/guardian/guardian_verbs.dm
new file mode 100644
index 00000000000..02d1fd1ed3a
--- /dev/null
+++ b/code/modules/mob/living/basic/guardian/guardian_verbs.dm
@@ -0,0 +1,187 @@
+/// Pop out into the realm of the living.
+/mob/living/basic/guardian/proc/manifest(forced)
+ if (is_deployed() || isnull(summoner) || isnull(summoner.loc) || istype(summoner.loc, /obj/effect) || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
+ return FALSE
+ forceMove(summoner.loc)
+ new /obj/effect/temp_visual/guardian/phase(loc)
+ COOLDOWN_START(src, manifest_cooldown, 1 SECONDS)
+ reset_perspective()
+ manifest_effects()
+ return TRUE
+
+/// Go and hide inside your boss.
+/mob/living/basic/guardian/proc/recall(forced)
+ if (!is_deployed() || isnull(summoner) || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
+ return FALSE
+ new /obj/effect/temp_visual/guardian/phase/out(loc)
+ recall_effects()
+ forceMove(summoner)
+ COOLDOWN_START(src, manifest_cooldown, 1 SECONDS)
+ return TRUE
+
+/// Do something when we appear.
+/mob/living/basic/guardian/proc/manifest_effects()
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_GUARDIAN_MANIFESTED)
+
+/// Do something when we vanish.
+/mob/living/basic/guardian/proc/recall_effects()
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_GUARDIAN_RECALLED)
+
+/// Swap to a different mode... if we have one
+/mob/living/basic/guardian/proc/toggle_modes()
+ to_chat(src, span_bolddanger("You don't have another mode!"))
+
+
+/// Turn an internal light on or off.
+/mob/living/basic/guardian/proc/toggle_light()
+ if (!light_on)
+ to_chat(src, span_notice("You activate your light."))
+ set_light_on(TRUE)
+ else
+ to_chat(src, span_notice("You deactivate your light."))
+ set_light_on(FALSE)
+
+
+/// Prints what type of guardian we are and what we can do.
+/mob/living/basic/guardian/verb/check_type()
+ set name = "Check Guardian Type"
+ set category = "Guardian"
+ set desc = "Check what type you are."
+ to_chat(src, playstyle_string)
+
+
+/// Speak with our boss at a distance
+/mob/living/basic/guardian/proc/communicate()
+ if (isnull(summoner))
+ return
+ var/sender_key = key
+ var/input = tgui_input_text(src, "Enter a message to tell your summoner", "Guardian")
+ if (sender_key != key || !input) //guardian got reset, or did not enter anything
+ return
+
+ var/preliminary_message = span_boldholoparasite("[input]") //apply basic color/bolding
+ var/my_message = "[span_bolditalic(src.name)]: [preliminary_message]" //add source, color source with the guardian's color
+
+ to_chat(summoner, "[my_message]")
+ var/list/guardians = summoner.get_all_linked_holoparasites()
+ for(var/guardian in guardians)
+ to_chat(guardian, "[my_message]")
+ for(var/dead_mob in GLOB.dead_mob_list)
+ var/link = FOLLOW_LINK(dead_mob, src)
+ to_chat(dead_mob, "[link] [my_message]")
+
+ src.log_talk(input, LOG_SAY, tag="guardian")
+
+
+/// Speak with your guardian(s) at a distance.
+/datum/action/cooldown/mob_cooldown/guardian_comms
+ name = "Guardian Communication"
+ desc = "Communicate telepathically with your guardian."
+ button_icon = 'icons/hud/guardian.dmi'
+ button_icon_state = "communicate"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ click_to_activate = FALSE
+ cooldown_time = 0 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+
+/datum/action/cooldown/mob_cooldown/guardian_comms/Activate(atom/target)
+ StartCooldown(360 SECONDS)
+ var/input = tgui_input_text(owner, "Enter a message to tell your guardian", "Message")
+ StartCooldown()
+ if (!input)
+ return FALSE
+
+ var/preliminary_message = span_boldholoparasite("[input]") //apply basic color/bolding
+ var/my_message = span_boldholoparasite("[owner]: [preliminary_message]") //add source, color source with default grey...
+
+ to_chat(owner, "[my_message]")
+ var/mob/living/living_owner = owner
+ var/list/guardians = living_owner.get_all_linked_holoparasites()
+ for(var/mob/living/basic/guardian/guardian as anything in guardians)
+ to_chat(guardian, "[span_bolditalic(owner.real_name)]: [preliminary_message]" )
+ for(var/dead_mob in GLOB.dead_mob_list)
+ var/link = FOLLOW_LINK(dead_mob, owner)
+ to_chat(dead_mob, "[link] [my_message]")
+ owner.log_talk(input, LOG_SAY, tag="guardian")
+
+ return TRUE
+
+
+/// Tell your slacking or distracted guardian to come home.
+/datum/action/cooldown/mob_cooldown/recall_guardian
+ name = "Recall Guardian"
+ desc = "Forcibly recall your guardian."
+ button_icon = 'icons/hud/guardian.dmi'
+ button_icon_state = "recall"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ click_to_activate = FALSE
+ cooldown_time = 0 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+
+/datum/action/cooldown/mob_cooldown/recall_guardian/Activate(atom/target)
+ var/mob/living/living_owner = owner
+ var/list/guardians = living_owner.get_all_linked_holoparasites()
+ for(var/mob/living/basic/guardian/guardian in guardians)
+ guardian.recall()
+ StartCooldown()
+ return TRUE
+
+/// Replace an annoying griefer you were paired up to with a different but probably no less annoying player.
+/datum/action/cooldown/mob_cooldown/replace_guardian
+ name = "Reset Guardian Consciousness"
+ desc = "Replaces the mind of your guardian with that of a different ghost."
+ button_icon = 'icons/mob/simple/mob.dmi'
+ button_icon_state = "ghost"
+ background_icon = 'icons/hud/guardian.dmi'
+ background_icon_state = "base"
+ click_to_activate = FALSE
+ cooldown_time = 5 SECONDS
+ melee_cooldown_time = 0
+ shared_cooldown = NONE
+
+/datum/action/cooldown/mob_cooldown/replace_guardian/Activate(atom/target)
+ StartCooldown(5 MINUTES)
+
+ var/mob/living/living_owner = owner
+ var/list/guardians = living_owner.get_all_linked_holoparasites()
+ for(var/mob/living/basic/guardian/resetting_guardian as anything in guardians)
+ if (!COOLDOWN_FINISHED(resetting_guardian, resetting_cooldown))
+ guardians -= resetting_guardian //clear out guardians that are already reset
+
+ if (!length(guardians))
+ to_chat(owner, span_holoparasite("You cannot reset [length(guardians) > 1 ? "any of your guardians":"your guardian"] yet."))
+ StartCooldown()
+ return FALSE
+
+ var/mob/living/basic/guardian/chosen_guardian = tgui_input_list(owner, "Pick the guardian you wish to reset", "Guardian Reset", sort_names(guardians))
+ if (isnull(chosen_guardian))
+ to_chat(owner, span_holoparasite("You decide not to reset [length(guardians) > 1 ? "any of your guardians":"your guardian"]."))
+ StartCooldown()
+ return FALSE
+
+ to_chat(owner, span_holoparasite("You attempt to reset [span_bold(chosen_guardian.real_name)]'s personality..."))
+ var/list/mob/dead/observer/ghost_candidates = poll_ghost_candidates("Do you want to play as [owner.real_name]'s [chosen_guardian.theme.name]?", ROLE_PAI, FALSE, 100)
+ if (!LAZYLEN(ghost_candidates))
+ to_chat(owner, span_holoparasite("Your attempt to reset the personality of \
+ [span_bold(chosen_guardian.real_name)] appears to have failed... \
+ Looks like you're stuck with it for now."))
+ StartCooldown()
+ return FALSE
+
+ var/mob/dead/observer/candidate = pick(ghost_candidates)
+ to_chat(chosen_guardian, span_holoparasite("Your user reset you, and your body was taken over by a ghost. Looks like they weren't happy with your performance."))
+ to_chat(owner, span_boldholoparasite("The personality of [chosen_guardian.theme.name] has been successfully reset."))
+ message_admins("[key_name_admin(candidate)] has taken control of ([ADMIN_LOOKUPFLW(chosen_guardian)])")
+ chosen_guardian.ghostize(FALSE)
+ chosen_guardian.key = candidate.key
+ COOLDOWN_START(chosen_guardian, resetting_cooldown, 5 MINUTES)
+ chosen_guardian.guardian_rename() //give it a new color and name, to show it's a new person
+ chosen_guardian.guardian_recolour()
+ StartCooldown()
+ return TRUE
diff --git a/code/modules/mob/living/basic/health_adjustment.dm b/code/modules/mob/living/basic/health_adjustment.dm
index 9644a1a6299..43352c689c4 100644
--- a/code/modules/mob/living/basic/health_adjustment.dm
+++ b/code/modules/mob/living/basic/health_adjustment.dm
@@ -17,6 +17,12 @@
updatehealth()
return . - bruteloss
+/mob/living/basic/get_damage_mod(damage_type)
+ var/modifier = ..()
+ if (damage_type in damage_coeff)
+ return modifier * damage_coeff[damage_type]
+ return modifier
+
/mob/living/basic/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
if(!can_adjust_brute_loss(amount, forced, required_bodytype))
return 0
diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
index a048fe77ab1..ab8f1f3dbfc 100644
--- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
+++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity.dm
@@ -69,7 +69,7 @@
/datum/action/cooldown/mob_cooldown/charge/basic_charge/lobster/hit_target(atom/movable/source, atom/target, damage_dealt)
. = ..()
- if(!isliving(target) || !isbasicmob(source))
+ if(!isbasicmob(source))
return
var/mob/living/basic/basic_source = source
var/mob/living/living_target = target
diff --git a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
index 3dbb4a743cc..98daeb6d8c2 100644
--- a/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
+++ b/code/modules/mob/living/basic/space_fauna/wumborian_fugu/fugu_gland.dm
@@ -15,7 +15,7 @@
if(fugu_blacklist)
return
fugu_blacklist = typecacheof(list(
- /mob/living/simple_animal/hostile/guardian,
+ /mob/living/basic/guardian,
))
/obj/item/fugu_gland/afterattack(atom/target, mob/user, proximity_flag)
diff --git a/code/modules/mob/living/basic/tree.dm b/code/modules/mob/living/basic/tree.dm
index 01e9579d24e..3f3894f190b 100644
--- a/code/modules/mob/living/basic/tree.dm
+++ b/code/modules/mob/living/basic/tree.dm
@@ -56,7 +56,7 @@
. = ..()
AddComponent(/datum/component/seethrough_mob)
AddElement(/datum/element/swabable, CELL_LINE_TABLE_PINE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
- var/static/list/death_loot = list(/obj/item/stack/sheet/mineral/wood)
+ var/list/death_loot = string_list(list(/obj/item/stack/sheet/mineral/wood))
AddElement(/datum/element/death_drops, death_loot)
AddComponent(/datum/component/aggro_emote, emote_list = string_list(list("growls")), emote_chance = 20)
diff --git a/code/modules/mob/living/carbon/damage_procs.dm b/code/modules/mob/living/carbon/damage_procs.dm
index 6ecc62b0498..e8a9f5bd467 100644
--- a/code/modules/mob/living/carbon/damage_procs.dm
+++ b/code/modules/mob/living/carbon/damage_procs.dm
@@ -28,6 +28,12 @@
return .
+/mob/living/carbon/human/get_damage_mod(damage_type)
+ if (!dna?.species?.damage_modifier)
+ return ..()
+ var/species_mod = (100 - dna.species.damage_modifier) / 100
+ return ..() * species_mod
+
/mob/living/carbon/human/apply_damage(
damage = 0,
damagetype = BRUTE,
diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm
index 41eb7bb06f4..35b1a2c064f 100644
--- a/code/modules/mob/living/damage_procs.dm
+++ b/code/modules/mob/living/damage_procs.dm
@@ -262,6 +262,17 @@
return TRUE
+/// Returns a multiplier to apply to a specific kind of damage
+/mob/living/proc/get_damage_mod(damage_type)
+ switch(damage_type)
+ if (OXY)
+ return HAS_TRAIT(src, TRAIT_NOBREATH) ? 0 : 1
+ if (TOX)
+ if (HAS_TRAIT(src, TRAIT_TOXINLOVER))
+ return -1
+ return HAS_TRAIT(src, TRAIT_TOXIMMUNE) ? 0 : 1
+ return 1
+
/mob/living/proc/getBruteLoss()
return bruteloss
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index fe81afcad25..440465aa379 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -2636,14 +2636,10 @@ GLOBAL_LIST_EMPTY(fire_appearances)
return
var/del_mob = FALSE
var/mob/old_mob
- var/ai_control = FALSE
- var/list/possible_players = list("None", "Poll Ghosts") + sort_list(GLOB.clients)
+ var/list/possible_players = list("Poll Ghosts") + sort_list(GLOB.clients)
var/client/guardian_client = tgui_input_list(admin, "Pick the player to put in control.", "Guardian Controller", possible_players)
- if(!guardian_client)
+ if(isnull(guardian_client))
return
- else if(guardian_client == "None")
- guardian_client = null
- ai_control = (tgui_alert(admin, "Do you want to give the spirit AI control?", "Guardian Controller", list("Yes", "No")) == "Yes")
else if(guardian_client == "Poll Ghosts")
var/list/candidates = poll_ghost_candidates("Do you want to play as an admin created Guardian Spirit of [real_name]?", ROLE_PAI, FALSE, 100, POLL_IGNORE_HOLOPARASITE)
if(LAZYLEN(candidates))
@@ -2656,7 +2652,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
old_mob = guardian_client.mob
if(isobserver(old_mob) || tgui_alert(admin, "Do you want to delete [guardian_client]'s old mob?", "Guardian Controller", list("Yes"," No")) == "Yes")
del_mob = TRUE
- var/picked_type = tgui_input_list(admin, "Pick the guardian type.", "Guardian Controller", subtypesof(/mob/living/simple_animal/hostile/guardian))
+ var/picked_type = tgui_input_list(admin, "Pick the guardian type.", "Guardian Controller", subtypesof(/mob/living/basic/guardian))
var/picked_theme = tgui_input_list(admin, "Pick the guardian theme.", "Guardian Controller", list(GUARDIAN_THEME_TECH, GUARDIAN_THEME_MAGIC, GUARDIAN_THEME_CARP, GUARDIAN_THEME_MINER, "Random"))
if(picked_theme == "Random")
picked_theme = null //holopara code handles not having a theme by giving a random one
@@ -2664,20 +2660,16 @@ GLOBAL_LIST_EMPTY(fire_appearances)
var/picked_color = input(admin, "Set the guardian's color, cancel to let player set it.", "Guardian Controller", "#ffffff") as color|null
if(tgui_alert(admin, "Confirm creation.", "Guardian Controller", list("Yes", "No")) != "Yes")
return
- var/mob/living/simple_animal/hostile/guardian/summoned_guardian = new picked_type(src, picked_theme)
+ var/mob/living/basic/guardian/summoned_guardian = new picked_type(src, picked_theme)
summoned_guardian.set_summoner(src, different_person = TRUE)
if(picked_name)
summoned_guardian.fully_replace_character_name(null, picked_name)
if(picked_color)
- summoned_guardian.set_guardian_color(picked_color)
+ summoned_guardian.set_guardian_colour(picked_color)
summoned_guardian.key = guardian_client?.key
guardian_client?.init_verbs()
if(del_mob)
qdel(old_mob)
- if(ai_control)
- summoned_guardian.can_have_ai = TRUE
- summoned_guardian.toggle_ai(AI_ON)
- summoned_guardian.manifest()
message_admins(span_adminnotice("[key_name_admin(admin)] gave a guardian spirit controlled by [guardian_client || "AI"] to [src]."))
log_admin("[key_name(admin)] gave a guardian spirit controlled by [guardian_client] to [src].")
SSblackbox.record_feedback("tally", "admin_verb", 1, "Give Guardian Spirit")
diff --git a/code/modules/mob/living/simple_animal/damage_procs.dm b/code/modules/mob/living/simple_animal/damage_procs.dm
index 9640dbb9de4..d92478907a1 100644
--- a/code/modules/mob/living/simple_animal/damage_procs.dm
+++ b/code/modules/mob/living/simple_animal/damage_procs.dm
@@ -18,6 +18,12 @@
if(AIStatus == AI_IDLE)
toggle_ai(AI_ON)
+/mob/living/simple_animal/get_damage_mod(damage_type)
+ var/modifier = ..()
+ if (damage_type in damage_coeff)
+ return modifier * damage_coeff[damage_type]
+ return modifier
+
/mob/living/simple_animal/adjustBruteLoss(amount, updating_health = TRUE, forced = FALSE, required_bodytype)
if(!can_adjust_brute_loss(amount, forced, required_bodytype))
return 0
diff --git a/code/modules/mob/living/simple_animal/guardian/guardian.dm b/code/modules/mob/living/simple_animal/guardian/guardian.dm
deleted file mode 100644
index 21286787755..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/guardian.dm
+++ /dev/null
@@ -1,616 +0,0 @@
-GLOBAL_LIST_EMPTY(parasites) //all currently existing/living guardians
-
-/mob/living/simple_animal/hostile/guardian
- name = "Guardian Spirit"
- real_name = "Guardian Spirit"
- desc = "A mysterious being that stands by its charge, ever vigilant."
- speak_emote = list("hisses")
- gender = NEUTER
- mob_biotypes = MOB_SPECIAL
- sentience_type = SENTIENCE_HUMANOID
- bubble_icon = "guardian"
- response_help_continuous = "passes through"
- response_help_simple = "pass through"
- response_disarm_continuous = "flails at"
- response_disarm_simple = "flail at"
- response_harm_continuous = "punches"
- response_harm_simple = "punch"
- icon = 'icons/mob/nonhuman-player/guardian.dmi'
- icon_state = "magicbase"
- icon_living = "magicbase"
- icon_dead = "magicbase"
- speed = 0
- combat_mode = TRUE
- stop_automated_movement = 1
- attack_sound = 'sound/weapons/punch1.ogg'
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- minbodytemp = 0
- maxbodytemp = INFINITY
- attack_verb_continuous = "punches"
- attack_verb_simple = "punch"
- maxHealth = INFINITY //The spirit itself is invincible
- health = INFINITY
- mob_biotypes = MOB_BEAST
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1) //how much damage from each damage type we transfer to the owner
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- obj_damage = 40
- melee_damage_lower = 15
- melee_damage_upper = 15
- del_on_death = TRUE
- loot = list(/obj/effect/temp_visual/guardian/phase/out)
- AIStatus = AI_OFF
- can_have_ai = FALSE
- light_system = MOVABLE_LIGHT
- light_range = 3
- light_on = FALSE
- hud_type = /datum/hud/guardian
- faction = list()
-
- /// The guardian's color, used for their sprite, chat, and some effects made by it.
- var/guardian_color
- /// List of overlays we use.
- var/list/guardian_overlays[GUARDIAN_TOTAL_LAYERS]
-
- /// The summoner of the guardian, the one it's intended to guard!
- var/mob/living/summoner
- /// How far from the summoner the guardian can be.
- var/range = 10
-
- /// Which toggle button the HUD uses.
- var/toggle_button_type = /atom/movable/screen/guardian/toggle_mode/inactive
- /// Name used by the guardian creator.
- var/creator_name = "Error"
- /// Description used by the guardian creator.
- var/creator_desc = "This shouldn't be here! Report it on GitHub!"
- /// Icon used by the guardian creator.
- var/creator_icon = "fuck"
-
- /// A string explaining to the guardian what they can do.
- var/playstyle_string = span_boldholoparasite("You are a Guardian without any type. You shouldn't exist!")
- /// The fluff string we actually use.
- var/used_fluff_string
- /// Fluff string from tarot cards.
- var/magic_fluff_string = span_holoparasite("You draw the Coder, symbolizing bugs and errors. This shouldn't happen! Submit a bug report!")
- /// Fluff string from holoparasite injectors.
- var/tech_fluff_string = span_holoparasite("BOOT SEQUENCE COMPLETE. ERROR MODULE LOADED. THIS SHOULDN'T HAPPEN. Submit a bug report!")
- /// Fluff string from holocarp fishsticks.
- var/carp_fluff_string = span_holoparasite("CARP CARP CARP SOME SORT OF HORRIFIC BUG BLAME THE CODERS CARP CARP CARP")
- /// Fluff string from the dusty shard.
- var/miner_fluff_string = span_holoparasite("You encounter... Mythril, it shouldn't exist... Submit a bug report!")
-
- /// Are we forced to not be able to manifest/recall?
- var/locked = FALSE
- /// Cooldown between manifests/recalls.
- COOLDOWN_DECLARE(manifest_cooldown)
- /// Cooldown between the summoner resetting the guardian's client.
- COOLDOWN_DECLARE(resetting_cooldown)
-
-/mob/living/simple_animal/hostile/guardian/Initialize(mapload, theme)
- . = ..()
- GLOB.parasites += src
- update_theme(theme)
- AddElement(/datum/element/simple_flying)
- AddComponent(/datum/component/basic_inhands)
- manifest_effects()
-
-/mob/living/simple_animal/hostile/guardian/Destroy() //if deleted by admins or something random, cut from the summoner
- if(is_deployed())
- recall_effects()
- if(!QDELETED(summoner))
- cut_summoner(different_person = TRUE)
- return ..()
-
-/// Setter for our summoner mob.
-/mob/living/simple_animal/hostile/guardian/proc/set_summoner(mob/living/to_who, different_person = FALSE)
- if(QDELETED(to_who))
- qdel(src) //no gettin off scot-free pal.........
- return
- if(summoner)
- cut_summoner(different_person)
- summoner = to_who
- update_health_hud()
- med_hud_set_health()
- med_hud_set_status()
- add_verb(to_who, list(
- /mob/living/proc/guardian_comm,
- /mob/living/proc/guardian_recall,
- /mob/living/proc/guardian_reset,
- ))
- if(different_person)
- if(mind)
- mind.enslave_mind_to_creator(to_who)
- else //mindless guardian, manually give them factions
- faction += summoner.faction
- summoner.faction += "[REF(src)]"
- remove_all_languages(LANGUAGE_MASTER)
- copy_languages(to_who, LANGUAGE_MASTER) // make sure holoparasites speak same language as master
- RegisterSignal(to_who, COMSIG_MOVABLE_MOVED, PROC_REF(check_distance))
- RegisterSignal(to_who, COMSIG_QDELETING, PROC_REF(on_summoner_deletion))
- RegisterSignal(to_who, COMSIG_LIVING_DEATH, PROC_REF(on_summoner_death))
- RegisterSignal(to_who, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_summoner_health_update))
- RegisterSignal(to_who, COMSIG_LIVING_ON_WABBAJACKED, PROC_REF(on_summoner_wabbajacked))
- RegisterSignal(to_who, COMSIG_LIVING_SHAPESHIFTED, PROC_REF(on_summoner_shapeshifted))
- RegisterSignal(to_who, COMSIG_LIVING_UNSHAPESHIFTED, PROC_REF(on_summoner_unshapeshifted))
- recall(forced = TRUE)
- if(to_who.stat == DEAD)
- on_summoner_death(to_who)
-
-/mob/living/simple_animal/hostile/guardian/proc/cut_summoner(different_person = FALSE)
- if(is_deployed())
- recall_effects()
- var/summoner_turf = get_turf(src)
- if (!isnull(summoner_turf))
- forceMove(summoner_turf)
- UnregisterSignal(summoner, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING, COMSIG_LIVING_DEATH, COMSIG_LIVING_HEALTH_UPDATE, COMSIG_LIVING_ON_WABBAJACKED, COMSIG_LIVING_SHAPESHIFTED, COMSIG_LIVING_UNSHAPESHIFTED))
- if(different_person)
- summoner.faction -= "[REF(src)]"
- faction -= summoner.faction
- mind?.remove_all_antag_datums()
- if(!length(summoner.get_all_linked_holoparasites() - src))
- remove_verb(summoner, list(
- /mob/living/proc/guardian_comm,
- /mob/living/proc/guardian_recall,
- /mob/living/proc/guardian_reset,
- ))
- summoner = null
-
-/// Signal proc for [COMSIG_LIVING_ON_WABBAJACKED], when our summoner is wabbajacked we should be alerted.
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_wabbajacked(mob/living/source, mob/living/new_mob)
- SIGNAL_HANDLER
-
- set_summoner(new_mob)
- to_chat(src, span_holoparasite("Your summoner has changed form!"))
-
-/// Signal proc for [COMSIG_LIVING_SHAPESHIFTED], when our summoner is shapeshifted we should change to the new mob
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_shapeshifted(mob/living/source, mob/living/new_shape)
- SIGNAL_HANDLER
-
- set_summoner(new_shape)
- to_chat(src, span_holoparasite("Your summoner has shapeshifted into that of a [new_shape]!"))
-
-/// Signal proc for [COMSIG_LIVING_UNSHAPESHIFTED], when our summoner unshapeshifts go back to that mob
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_unshapeshifted(mob/living/source, mob/living/old_summoner)
- SIGNAL_HANDLER
-
- set_summoner(old_summoner)
- to_chat(src, span_holoparasite("Your summoner has shapeshifted back into their normal form!"))
-
-// Ha, no
-/mob/living/simple_animal/hostile/guardian/wabbajack(what_to_randomize, change_flags = WABBAJACK)
- visible_message(span_warning("[src] resists the polymorph!"))
-
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_health_update(mob/living/source)
- SIGNAL_HANDLER
-
- update_health_hud()
- med_hud_set_health()
- med_hud_set_status()
-
-/mob/living/simple_animal/hostile/guardian/med_hud_set_health()
- var/image/holder = hud_list?[HEALTH_HUD]
- if(isnull(holder))
- return
- holder.icon_state = "hud[RoundHealth(summoner || src)]"
- var/icon/size_check = icon(icon, icon_state, dir)
- holder.pixel_y = size_check.Height() - world.icon_size
-
-/mob/living/simple_animal/hostile/guardian/med_hud_set_status()
- var/image/holder = hud_list?[STATUS_HUD]
- if(isnull(holder))
- return
- var/icon/size_check = icon(icon, icon_state, dir)
- holder.pixel_y = size_check.Height() - world.icon_size
- var/mob/living/checking_mob = summoner || src
- if(checking_mob.stat == DEAD || HAS_TRAIT(checking_mob, TRAIT_FAKEDEATH))
- holder.icon_state = "huddead"
- else
- holder.icon_state = "hudhealthy"
-
-/mob/living/simple_animal/hostile/guardian/Destroy()
- GLOB.parasites -= src
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/proc/update_theme(theme) //update the guardian's theme
- if(!theme)
- theme = pick(GUARDIAN_THEME_MAGIC, GUARDIAN_THEME_TECH, GUARDIAN_THEME_CARP, GUARDIAN_THEME_MINER)
- switch(theme)//should make it easier to create new stand designs in the future if anyone likes that
- if(GUARDIAN_THEME_MAGIC)
- name = "Guardian Spirit"
- real_name = "Guardian Spirit"
- bubble_icon = "guardian"
- icon_state = "magicbase"
- icon_living = "magicbase"
- icon_dead = "magicbase"
- used_fluff_string = magic_fluff_string
- if(GUARDIAN_THEME_TECH)
- name = "Holoparasite"
- real_name = "Holoparasite"
- bubble_icon = "holo"
- icon_state = "techbase"
- icon_living = "techbase"
- icon_dead = "techbase"
- used_fluff_string = tech_fluff_string
- if(GUARDIAN_THEME_MINER)
- name = "Power Miner"
- real_name = "Power Miner"
- bubble_icon = "guardian"
- icon_state = "minerbase"
- icon_living = "minerbase"
- icon_dead = "minerbase"
- used_fluff_string = miner_fluff_string
- if(GUARDIAN_THEME_CARP)
- name = "Holocarp"
- real_name = "Holocarp"
- bubble_icon = "holo"
- icon_state = null //entirely handled by overlays
- icon_living = null
- icon_dead = null
- speak_emote = string_list(list("gnashes"))
- desc = "A mysterious fish that stands by its charge, ever vigilant."
- attack_verb_continuous = "bites"
- attack_verb_simple = "bite"
- attack_sound = 'sound/weapons/bite.ogg'
- attack_vis_effect = ATTACK_EFFECT_BITE
- used_fluff_string = carp_fluff_string
- guardian_overlays[GUARDIAN_COLOR_LAYER] = mutable_appearance(icon, theme)
- apply_overlay(GUARDIAN_COLOR_LAYER)
-
-/mob/living/simple_animal/hostile/guardian/Login() //if we have a mind, set its name to ours when it logs in
- . = ..()
- if(!. || !client)
- return FALSE
- if(!summoner)
- to_chat(src, span_boldholoparasite("For some reason, somehow, you have no summoner. Please report this bug immediately."))
- else
- to_chat(src, span_holoparasite("You are a [real_name], bound to serve [summoner.real_name]."))
- to_chat(src, span_holoparasite("You are capable of manifesting or recalling to your master with the buttons on your HUD. You will also find a button to communicate with [summoner.p_them()] privately there."))
- to_chat(src, span_holoparasite("While personally invincible, you will die if [summoner.real_name] does, and any damage dealt to you will have a portion passed on to [summoner.p_them()] as you feed upon [summoner.p_them()] to sustain yourself."))
- to_chat(src, playstyle_string)
- if(!guardian_color)
- locked = TRUE
- guardian_rename()
- guardian_recolor()
- locked = FALSE
-
-/mob/living/simple_animal/hostile/guardian/mind_initialize()
- . = ..()
- if(!summoner)
- to_chat(src, span_boldholoparasite("For some reason, somehow, you have no summoner. Please report this bug immediately."))
- return
- mind.enslave_mind_to_creator(summoner) //once our mind is created, we become enslaved to our summoner. cant be done in the first run of set_summoner, because by then we dont have a mind yet.
-
-/mob/living/simple_animal/hostile/guardian/proc/guardian_recolor()
- if(!client)
- return
- var/chosen_guardian_color = input(src, "What would you like your color to be?","Choose Your Color","#ffffff") as color|null
- if(!chosen_guardian_color) //redo proc until we get a color
- to_chat(src, span_warning("Not a valid color, please try again."))
- guardian_recolor()
- return
- set_guardian_color(chosen_guardian_color)
-
-/mob/living/simple_animal/hostile/guardian/proc/set_guardian_color(colour)
- guardian_color = colour
- set_light_color(guardian_color)
- var/mutable_appearance/guardian_color_overlay = guardian_overlays[GUARDIAN_COLOR_LAYER]
- remove_overlay(GUARDIAN_COLOR_LAYER)
- guardian_color_overlay.color = guardian_color
- guardian_overlays[GUARDIAN_COLOR_LAYER] = guardian_color_overlay
- apply_overlay(GUARDIAN_COLOR_LAYER)
-
-/mob/living/simple_animal/hostile/guardian/proc/guardian_rename()
- if(!client)
- return
- var/new_name = sanitize_name(reject_bad_text(tgui_input_text(src, "What would you like your name to be?", "Choose Your Name", real_name, MAX_NAME_LEN)))
- if(!new_name) //redo proc until we get a good name
- to_chat(src, span_warning("Not a valid name, please try again."))
- guardian_rename()
- return
- to_chat(src, span_notice("Your new name [span_name("[new_name]")] anchors itself in your mind."))
- fully_replace_character_name(null, new_name)
-
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_death(mob/living/source)
- SIGNAL_HANDLER
-
- cut_summoner()
- if (!isnull(source.loc))
- forceMove(source.loc)
- to_chat(src, span_danger("Your summoner has died!"))
- visible_message(span_bolddanger("\The [src] dies along with its user!"))
- source.visible_message(span_bolddanger("[source]'s body is completely consumed by the strain of sustaining [src]!"))
- source.dust(drop_items = TRUE)
- death(TRUE)
-
-/mob/living/simple_animal/hostile/guardian/proc/on_summoner_deletion(mob/living/source)
- SIGNAL_HANDLER
-
- cut_summoner()
- to_chat(src, span_danger("Your summoner is gone!"))
- qdel(src)
-
-/mob/living/simple_animal/hostile/guardian/get_status_tab_items()
- . += ..()
- if(summoner)
- var/healthpercent = health_percentage(summoner)
- . += "Summoner Health: [round(healthpercent, 0.5)]%"
- if(!COOLDOWN_FINISHED(src, manifest_cooldown))
- . += "Manifest/Recall Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, manifest_cooldown))]"
-
-/mob/living/simple_animal/hostile/guardian/Move() //Returns to summoner if they move out of range
- . = ..()
- check_distance()
-
-/mob/living/simple_animal/hostile/guardian/proc/check_distance()
- SIGNAL_HANDLER
-
- if(!summoner)
- return
- if(get_dist(get_turf(summoner), get_turf(src)) <= range)
- return
- to_chat(src, span_holoparasite("You moved out of range, and were pulled back! You can only move [range] meters from [summoner.real_name]!"))
- visible_message(span_danger("\The [src] jumps back to its user."))
- new /obj/effect/temp_visual/guardian/phase/out(loc)
- if(istype(summoner.loc, /obj/effect) || isnull(summoner.loc))
- recall(forced = TRUE)
- return
- forceMove(summoner.loc)
- new /obj/effect/temp_visual/guardian/phase(loc)
-
-/mob/living/simple_animal/hostile/guardian/can_suicide()
- return FALSE
-
-/mob/living/simple_animal/hostile/guardian/proc/is_deployed()
- return loc != summoner || !summoner
-
-/mob/living/simple_animal/hostile/guardian/AttackingTarget(atom/attacked_target)
- if(!is_deployed())
- to_chat(src, span_bolddanger("You must be manifested to attack!"))
- return FALSE
- else
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/death(gibbed)
- if(!QDELETED(summoner))
- to_chat(summoner, span_bolddanger("Your [name] died somehow!"))
- summoner.dust()
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/update_health_hud()
- var/severity = 0
- var/healthpercent = health_percentage(summoner || src)
- switch(healthpercent)
- if(100 to INFINITY)
- severity = 0
- if(85 to 100)
- severity = 1
- if(70 to 85)
- severity = 2
- if(55 to 70)
- severity = 3
- if(40 to 55)
- severity = 4
- if(25 to 40)
- severity = 5
- else
- severity = 6
- if(severity > 0)
- overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
- else
- clear_fullscreen("brute")
- if(hud_used?.healths)
- hud_used.healths.maptext = MAPTEXT("[round(healthpercent, 0.5)]%
")
-
-/mob/living/simple_animal/hostile/guardian/adjustHealth(amount, updating_health = TRUE, forced = FALSE) //The spirit is invincible, but passes on damage to the summoner
- . = amount
- if(!summoner)
- return ..()
- if(!is_deployed())
- return FALSE
- summoner.adjustBruteLoss(amount)
- if(amount < 0 || QDELETED(summoner))
- return
- to_chat(summoner, span_bolddanger("Your [name] is under attack! You take damage!"))
- summoner.visible_message(span_bolddanger("Blood sprays from [summoner] as [src] takes damage!"))
- if(summoner.stat == UNCONSCIOUS || summoner.stat == HARD_CRIT)
- to_chat(summoner, span_bolddanger("Your head pounds, you can't take the strain of sustaining [src] in this condition!"))
- summoner.adjustOrganLoss(ORGAN_SLOT_BRAIN, amount * 0.5)
-
-/mob/living/simple_animal/hostile/guardian/ex_act(severity, target)
- switch(severity)
- if(EXPLODE_DEVASTATE)
- investigate_log("has been gibbed by an explosion.", INVESTIGATE_DEATHS)
- gib()
- return TRUE
- if(EXPLODE_HEAVY)
- adjustBruteLoss(60)
- if(EXPLODE_LIGHT)
- adjustBruteLoss(30)
-
- return TRUE
-
-/mob/living/simple_animal/hostile/guardian/gib()
- death(TRUE)
-
-/mob/living/simple_animal/hostile/guardian/dust(just_ash, drop_items, force)
- death(TRUE)
-
-//HAND HANDLING
-
-/mob/living/simple_animal/hostile/guardian/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
- if(!slot)
- return FALSE
- if(!istype(equipping))
- return FALSE
-
- . = TRUE
- var/index = get_held_index_of_item(equipping)
- if(index)
- held_items[index] = null
- update_held_items()
-
- if(equipping.pulledby)
- equipping.pulledby.stop_pulling()
-
- equipping.screen_loc = null // will get moved if inventory is visible
- equipping.forceMove(src)
- SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
- equipping.on_equipped(src, slot)
-
-/mob/living/simple_animal/hostile/guardian/proc/apply_overlay(cache_index)
- if((. = guardian_overlays[cache_index]))
- add_overlay(.)
-
-/mob/living/simple_animal/hostile/guardian/proc/remove_overlay(cache_index)
- var/overlay = guardian_overlays[cache_index]
- if(overlay)
- cut_overlay(overlay)
- guardian_overlays[cache_index] = null
-
-/mob/living/simple_animal/hostile/guardian/regenerate_icons()
- update_held_items()
-
-//MANIFEST, RECALL, TOGGLE MODE/LIGHT, SHOW TYPE
-
-/mob/living/simple_animal/hostile/guardian/proc/manifest(forced)
- if(is_deployed() || isnull(summoner.loc) || istype(summoner.loc, /obj/effect) || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
- return FALSE
- forceMove(summoner.loc)
- new /obj/effect/temp_visual/guardian/phase(loc)
- COOLDOWN_START(src, manifest_cooldown, 1 SECONDS)
- reset_perspective()
- manifest_effects()
- return TRUE
-
-/mob/living/simple_animal/hostile/guardian/proc/recall(forced)
- if(!is_deployed() || !summoner || (!COOLDOWN_FINISHED(src, manifest_cooldown) && !forced) || locked)
- return FALSE
- new /obj/effect/temp_visual/guardian/phase/out(loc)
- forceMove(summoner)
- COOLDOWN_START(src, manifest_cooldown, 1 SECONDS)
- recall_effects()
- return TRUE
-
-/mob/living/simple_animal/hostile/guardian/proc/manifest_effects()
- return
-
-/mob/living/simple_animal/hostile/guardian/proc/recall_effects()
- return
-
-/mob/living/simple_animal/hostile/guardian/proc/toggle_modes()
- to_chat(src, span_bolddanger("You don't have another mode!"))
-
-/mob/living/simple_animal/hostile/guardian/proc/toggle_light()
- if(!light_on)
- to_chat(src, span_notice("You activate your light."))
- set_light_on(TRUE)
- else
- to_chat(src, span_notice("You deactivate your light."))
- set_light_on(FALSE)
-
-
-/mob/living/simple_animal/hostile/guardian/verb/check_type()
- set name = "Check Guardian Type"
- set category = "Guardian"
- set desc = "Check what type you are."
- to_chat(src, playstyle_string)
-
-//COMMUNICATION
-
-/mob/living/simple_animal/hostile/guardian/proc/communicate()
- if(!summoner)
- return
- var/sender_key = key
- var/input = tgui_input_text(src, "Enter a message to tell your summoner", "Guardian")
- if(sender_key != key || !input) //guardian got reset, or did not enter anything
- return
-
- var/preliminary_message = span_boldholoparasite("[input]") //apply basic color/bolding
- var/my_message = "[src]: [preliminary_message]" //add source, color source with the guardian's color
-
- to_chat(summoner, "[my_message]")
- var/list/guardians = summoner.get_all_linked_holoparasites()
- for(var/guardian in guardians)
- to_chat(guardian, "[my_message]")
- for(var/dead_mob in GLOB.dead_mob_list)
- var/link = FOLLOW_LINK(dead_mob, src)
- to_chat(dead_mob, "[link] [my_message]")
-
- src.log_talk(input, LOG_SAY, tag="guardian")
-
-/mob/living/proc/guardian_comm()
- set name = "Communicate"
- set category = "Guardian"
- set desc = "Communicate telepathically with your guardian."
- var/input = tgui_input_text(src, "Enter a message to tell your guardian", "Message")
- if(!input)
- return
-
- var/preliminary_message = span_boldholoparasite("[input]") //apply basic color/bolding
- var/my_message = span_boldholoparasite("[src]: [preliminary_message]") //add source, color source with default grey...
-
- to_chat(src, "[my_message]")
- var/list/guardians = get_all_linked_holoparasites()
- for(var/mob/living/simple_animal/hostile/guardian/guardian as anything in guardians)
- to_chat(guardian, "[src]: [preliminary_message]" )
- for(var/dead_mob in GLOB.dead_mob_list)
- var/link = FOLLOW_LINK(dead_mob, src)
- to_chat(dead_mob, "[link] [my_message]")
-
- src.log_talk(input, LOG_SAY, tag="guardian")
-
-//FORCE RECALL/RESET
-
-/mob/living/proc/guardian_recall()
- set name = "Recall Guardian"
- set category = "Guardian"
- set desc = "Forcibly recall your guardian."
- var/list/guardians = get_all_linked_holoparasites()
- for(var/mob/living/simple_animal/hostile/guardian/guardian in guardians)
- guardian.recall()
-
-/mob/living/proc/guardian_reset()
- set name = "Reset Guardian Player (5 Minute Cooldown)"
- set category = "Guardian"
- set desc = "Re-rolls which ghost will control your Guardian. Can be used once per 5 minutes."
-
- var/list/guardians = get_all_linked_holoparasites()
- for(var/mob/living/simple_animal/hostile/guardian/resetting_guardian as anything in guardians)
- if(!COOLDOWN_FINISHED(resetting_guardian, resetting_cooldown))
- guardians -= resetting_guardian //clear out guardians that are already reset
-
- var/mob/living/simple_animal/hostile/guardian/chosen_guardian = tgui_input_list(src, "Pick the guardian you wish to reset", "Guardian Reset", sort_names(guardians))
- if(isnull(chosen_guardian))
- to_chat(src, span_holoparasite("You decide not to reset [length(guardians) > 1 ? "any of your guardians":"your guardian"]."))
- return
-
- to_chat(src, span_holoparasite("You attempt to reset [chosen_guardian.real_name]'s personality..."))
- var/list/mob/dead/observer/ghost_candidates = poll_ghost_candidates("Do you want to play as [src.real_name]'s Guardian Spirit?", ROLE_PAI, FALSE, 100)
- if(!LAZYLEN(ghost_candidates))
- to_chat(src, span_holoparasite("There were no ghosts willing to take control of [chosen_guardian.real_name]. Looks like you're stuck with it for now."))
- return
-
- var/mob/dead/observer/candidate = pick(ghost_candidates)
- to_chat(chosen_guardian, span_holoparasite("Your user reset you, and your body was taken over by a ghost. Looks like they weren't happy with your performance."))
- to_chat(src, span_boldholoparasite("Your [chosen_guardian.real_name] has been successfully reset."))
- message_admins("[key_name_admin(candidate)] has taken control of ([ADMIN_LOOKUPFLW(chosen_guardian)])")
- chosen_guardian.ghostize(FALSE)
- chosen_guardian.key = candidate.key
- COOLDOWN_START(chosen_guardian, resetting_cooldown, 5 MINUTES)
- chosen_guardian.guardian_rename() //give it a new color and name, to show it's a new person
- chosen_guardian.guardian_recolor()
-
-////////parasite tracking/finding procs
-
-/// Returns a list of all holoparasites that has this mob as a summoner.
-/mob/living/proc/get_all_linked_holoparasites()
- RETURN_TYPE(/list)
- var/list/all_parasites = list()
- for(var/mob/living/simple_animal/hostile/guardian/stand as anything in GLOB.parasites)
- if(stand.summoner != src)
- continue
- all_parasites += stand
- return all_parasites
-
-/// Returns true if this holoparasite has the same summoner as the passed holoparasite.
-/mob/living/simple_animal/hostile/guardian/proc/hasmatchingsummoner(mob/living/simple_animal/hostile/guardian/other_guardian)
- return istype(other_guardian) && other_guardian.summoner == summoner
diff --git a/code/modules/mob/living/simple_animal/guardian/types/assassin.dm b/code/modules/mob/living/simple_animal/guardian/types/assassin.dm
deleted file mode 100644
index 340e92a5b42..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/assassin.dm
+++ /dev/null
@@ -1,107 +0,0 @@
-//Assassin
-/mob/living/simple_animal/hostile/guardian/assassin
- melee_damage_lower = 15
- melee_damage_upper = 15
- attack_verb_continuous = "slashes"
- attack_verb_simple = "slash"
- attack_sound = 'sound/weapons/bladeslice.ogg'
- attack_vis_effect = ATTACK_EFFECT_SLASH
- sharpness = SHARP_POINTY
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
- playstyle_string = span_holoparasite("As an assassin type you do medium damage and have no damage resistance, but can enter stealth, massively increasing the damage of your next attack and causing it to ignore armor. Stealth is broken when you attack or take damage.")
- magic_fluff_string = span_holoparasite("..And draw the Space Ninja, a lethal, invisible assassin.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Assassin modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one! It's an assassin carp! Just when you thought it was safe to go back to the water... which is unhelpful, because we're in space.")
- miner_fluff_string = span_holoparasite("You encounter... Glass, a sharp, fragile attacker.")
- creator_name = "Assassin"
- creator_desc = "Does medium damage and takes full damage, but can enter stealth, causing its next attack to do massive damage and ignore armor. However, it becomes briefly unable to recall after attacking from stealth."
- creator_icon = "assassin"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode/assassin
- /// Is it in stealth mode?
- var/toggle = FALSE
- /// Time between going in stealth.
- var/stealth_cooldown_time = 16 SECONDS
- /// Damage added in stealth mode.
- var/damage_bonus = 35
- /// Our wound bonus when in stealth mode.
- var/stealth_wound_bonus = -20 //from -100, you can now wound!
- /// Screen alert given when we are able to stealth.
- var/atom/movable/screen/alert/canstealthalert
- /// Screen alert given when we are in stealth.
- var/atom/movable/screen/alert/instealthalert
- /// Cooldown for the stealth toggle.
- COOLDOWN_DECLARE(stealth_cooldown)
-
-/mob/living/simple_animal/hostile/guardian/assassin/get_status_tab_items()
- . = ..()
- if(!COOLDOWN_FINISHED(src, stealth_cooldown))
- . += "Stealth Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]"
-
-/mob/living/simple_animal/hostile/guardian/assassin/AttackingTarget(atom/attacked_target)
- . = ..()
- if(.)
- if(toggle && (isliving(target) || istype(target, /obj/structure/window) || istype(target, /obj/structure/grille)))
- toggle_modes(forced = TRUE)
-
-/mob/living/simple_animal/hostile/guardian/assassin/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- . = ..()
- if(. > 0 && toggle)
- toggle_modes(forced = TRUE)
-
-/mob/living/simple_animal/hostile/guardian/assassin/recall_effects()
- if(toggle)
- toggle_modes(forced = TRUE)
-
-/mob/living/simple_animal/hostile/guardian/assassin/toggle_modes(forced = FALSE)
- if(toggle)
- melee_damage_lower -= damage_bonus
- melee_damage_upper -= damage_bonus
- armour_penetration = initial(armour_penetration)
- wound_bonus = initial(wound_bonus)
- obj_damage = initial(obj_damage)
- environment_smash = initial(environment_smash)
- alpha = initial(alpha)
- if(!forced)
- to_chat(src, span_bolddanger("You exit stealth."))
- else
- visible_message(span_danger("\The [src] suddenly appears!"))
- COOLDOWN_START(src, stealth_cooldown, stealth_cooldown_time) //we were forced out of stealth and go on cooldown
- addtimer(CALLBACK(src, PROC_REF(updatestealthalert)), stealth_cooldown_time)
- COOLDOWN_START(src, manifest_cooldown, 4 SECONDS) //can't recall for 4 seconds
- updatestealthalert()
- toggle = FALSE
- else if(COOLDOWN_FINISHED(src, stealth_cooldown))
- if(!is_deployed())
- to_chat(src, span_bolddanger("You have to be manifested to enter stealth!"))
- return
- melee_damage_lower += damage_bonus
- melee_damage_upper += damage_bonus
- armour_penetration = 100
- wound_bonus = stealth_wound_bonus
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- new /obj/effect/temp_visual/guardian/phase/out(get_turf(src))
- alpha = 15
- if(!forced)
- to_chat(src, span_bolddanger("You enter stealth, empowering your next attack."))
- updatestealthalert()
- toggle = TRUE
- else if(!forced)
- to_chat(src, span_bolddanger("You cannot yet enter stealth, wait another [DisplayTimeText(COOLDOWN_TIMELEFT(src, stealth_cooldown))]!"))
-
-/mob/living/simple_animal/hostile/guardian/assassin/proc/updatestealthalert()
- if(!COOLDOWN_FINISHED(src, stealth_cooldown))
- clear_alert("instealth")
- instealthalert = null
- clear_alert("canstealth")
- canstealthalert = null
- return
- if(toggle && !instealthalert)
- instealthalert = throw_alert("instealth", /atom/movable/screen/alert/instealth)
- clear_alert("canstealth")
- canstealthalert = null
- else if(!toggle && !canstealthalert)
- canstealthalert = throw_alert("canstealth", /atom/movable/screen/alert/canstealth)
- clear_alert("instealth")
- instealthalert = null
-
diff --git a/code/modules/mob/living/simple_animal/guardian/types/charger.dm b/code/modules/mob/living/simple_animal/guardian/types/charger.dm
deleted file mode 100644
index e1e0de66b00..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/charger.dm
+++ /dev/null
@@ -1,74 +0,0 @@
-//Charger
-/mob/living/simple_animal/hostile/guardian/charger
- melee_damage_lower = 15
- melee_damage_upper = 15
- ranged = TRUE //technically
- ranged_message = "charges"
- ranged_cooldown_time = 4 SECONDS
- speed = -0.5
- damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
- playstyle_string = span_holoparasite("As a charger type you do medium damage, have light damage resistance, move very fast, can be ridden, and can charge at a location, damaging any target hit and forcing them to drop any items they are holding.")
- magic_fluff_string = span_holoparasite("..And draw the Hunter, an alien master of rapid assault.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Charge modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one! It's a charger carp, that likes running at people. But it doesn't have any legs...")
- miner_fluff_string = span_holoparasite("You encounter... Titanium, a lightweight, agile fighter.")
- creator_name = "Charger"
- creator_desc = "Moves very fast, does medium damage on attack, can be ridden and can charge at targets, damaging the first target hit and forcing them to drop any items they are holding."
- creator_icon = "charger"
- /// Is it currently charging at something?
- var/charging = FALSE
- /// How much damage it does while charging.
- var/charge_damage = 20
-
-/mob/living/simple_animal/hostile/guardian/charger/Initialize(mapload, theme)
- . = ..()
- AddElement(/datum/element/ridable, /datum/component/riding/creature/guardian)
-
-/mob/living/simple_animal/hostile/guardian/charger/get_status_tab_items()
- . = ..()
- if(!COOLDOWN_FINISHED(src, ranged_cooldown))
- . += "Charge Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, ranged_cooldown))]"
-
-/mob/living/simple_animal/hostile/guardian/charger/OpenFire(atom/target)
- if(charging)
- return
- visible_message(span_danger("[src] [ranged_message] at [target]!"))
- COOLDOWN_START(src, ranged_cooldown, ranged_cooldown_time)
- clear_alert(ALERT_CHARGE)
- addtimer(CALLBACK(src, PROC_REF(throw_alert), ALERT_CHARGE, /atom/movable/screen/alert/cancharge), ranged_cooldown_time)
- Shoot(target)
-
-/mob/living/simple_animal/hostile/guardian/charger/Shoot(atom/targeted_atom)
- charging = TRUE
- playsound(src, 'sound/items/modsuit/loader_launch.ogg', 75, TRUE)
- throw_at(targeted_atom, range, speed = 1.5, thrower = src, spin = FALSE, diagonals_first = TRUE, callback = CALLBACK(src, PROC_REF(charging_end)))
-
-/mob/living/simple_animal/hostile/guardian/charger/proc/charging_end()
- charging = FALSE
-
-/mob/living/simple_animal/hostile/guardian/charger/Move()
- if(charging)
- new /obj/effect/temp_visual/decoy/fading(loc, src)
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/charger/check_distance()
- if(!charging)
- ..()
-
-/mob/living/simple_animal/hostile/guardian/charger/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
- if(!charging)
- return ..()
- if(!isliving(hit_atom) || hit_atom == summoner || hasmatchingsummoner(hit_atom))
- return
- var/mob/living/hit_mob = hit_atom
- if(ishuman(hit_mob))
- var/mob/living/carbon/human/hit_human = hit_mob
- if(hit_human.check_shields(src, charge_damage, name, attack_type = THROWN_PROJECTILE_ATTACK))
- return
- hit_mob.drop_all_held_items()
- hit_mob.visible_message(span_danger("[src] slams into [hit_mob]!"), span_userdanger("[src] slams into you!"))
- hit_mob.apply_damage(charge_damage, BRUTE)
- playsound(hit_mob, 'sound/effects/meteorimpact.ogg', 100, TRUE)
- shake_camera(hit_mob, 4, 3)
- shake_camera(src, 2, 3)
-
diff --git a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
deleted file mode 100644
index d2fbfc33c87..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
+++ /dev/null
@@ -1,90 +0,0 @@
-//Dextrous
-/mob/living/simple_animal/hostile/guardian/dextrous
- melee_damage_lower = 10
- melee_damage_upper = 10
- damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
- playstyle_string = span_holoparasite("As a dextrous type you can hold items, store an item within yourself, and have medium damage resistance, but do low damage on attacks. Recalling and leashing will force you to drop unstored items!")
- magic_fluff_string = span_holoparasite("..And draw the Drone, a dextrous master of construction and repair.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Dextrous combat modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! You caught one! It can hold stuff in its fins, sort of.")
- miner_fluff_string = span_holoparasite("You encounter... Gold, a malleable constructor.")
- creator_name = "Dextrous"
- creator_desc = "Does low damage on attack, but is capable of holding items and storing a single item within it. It will drop items held in its hands when it recalls, but it will retain the stored item."
- creator_icon = "dextrous"
- dextrous = TRUE
- hud_type = /datum/hud/dextrous/guardian
- held_items = list(null, null)
- var/obj/item/internal_storage //what we're storing within ourself
-
-/mob/living/simple_animal/hostile/guardian/dextrous/death(gibbed)
- . = ..()
- if(internal_storage)
- dropItemToGround(internal_storage)
-
-/mob/living/simple_animal/hostile/guardian/dextrous/examine(mob/user)
- . = ..()
- if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
- . += span_info("It is holding [internal_storage.get_examine_string(user)] in its internal storage.")
-
-/mob/living/simple_animal/hostile/guardian/dextrous/recall_effects()
- drop_all_held_items()
-
-/mob/living/simple_animal/hostile/guardian/dextrous/check_distance()
- if(!summoner || get_dist(get_turf(summoner), get_turf(src)) <= range)
- return
- drop_all_held_items()
- ..() //lose items, then return
-
-//SLOT HANDLING BULLSHIT FOR INTERNAL STORAGE
-/mob/living/simple_animal/hostile/guardian/dextrous/doUnEquip(obj/item/equipped_item, force, newloc, no_move, invdrop = TRUE, silent = FALSE)
- if(..())
- update_held_items()
- if(equipped_item == internal_storage)
- internal_storage = null
- update_inv_internal_storage()
- return TRUE
- return FALSE
-
-/mob/living/simple_animal/hostile/guardian/dextrous/can_equip(obj/item/equipped_item, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE, ignore_equipped = FALSE)
- switch(slot)
- if(ITEM_SLOT_DEX_STORAGE)
- if(internal_storage)
- return FALSE
- return TRUE
- ..()
-
-/mob/living/simple_animal/hostile/guardian/dextrous/get_item_by_slot(slot_id)
- if(slot_id == ITEM_SLOT_DEX_STORAGE)
- return internal_storage
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/dextrous/get_slot_by_item(obj/item/looking_for)
- if(internal_storage == looking_for)
- return ITEM_SLOT_DEX_STORAGE
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/dextrous/equip_to_slot(obj/item/equipping, slot, initial = FALSE, redraw_mob = FALSE, indirect_action = FALSE)
- if(!..())
- return
-
- switch(slot)
- if(ITEM_SLOT_DEX_STORAGE)
- internal_storage = equipping
- update_inv_internal_storage()
- else
- to_chat(src, span_danger("You are trying to equip this item to an unsupported inventory slot. Report this to a coder!"))
-
-/mob/living/simple_animal/hostile/guardian/dextrous/getBackSlot()
- return ITEM_SLOT_DEX_STORAGE
-
-/mob/living/simple_animal/hostile/guardian/dextrous/getBeltSlot()
- return ITEM_SLOT_DEX_STORAGE
-
-/mob/living/simple_animal/hostile/guardian/dextrous/proc/update_inv_internal_storage()
- if(internal_storage && client && hud_used?.hud_shown)
- internal_storage.screen_loc = ui_id
- client.screen += internal_storage
-
-/mob/living/simple_animal/hostile/guardian/dextrous/regenerate_icons()
- ..()
- update_inv_internal_storage()
diff --git a/code/modules/mob/living/simple_animal/guardian/types/explosive.dm b/code/modules/mob/living/simple_animal/guardian/types/explosive.dm
deleted file mode 100644
index 6fbc61ee458..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/explosive.dm
+++ /dev/null
@@ -1,72 +0,0 @@
-#define UNREGISTER_BOMB_SIGNALS(A) \
- do { \
- UnregisterSignal(A, boom_signals); \
- UnregisterSignal(A, COMSIG_ATOM_EXAMINE); \
- } while (0)
-
-//Explosive
-/mob/living/simple_animal/hostile/guardian/explosive
- melee_damage_lower = 15
- melee_damage_upper = 15
- damage_coeff = list(BRUTE = 0.6, BURN = 0.6, TOX = 0.6, CLONE = 0.6, STAMINA = 0, OXY = 0.6)
- range = 13
- playstyle_string = span_holoparasite("As an explosive type, you have moderate close combat abilities and are capable of converting nearby items and objects into disguised bombs via right-click.")
- magic_fluff_string = span_holoparasite("..And draw the Scientist, master of explosive death.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Explosive modules active. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one! It's an explosive carp! Boom goes the fishy.")
- miner_fluff_string = span_holoparasite("You encounter... Gibtonite, an explosive fighter.")
- creator_name = "Explosive"
- creator_desc = "High damage resist and medium power attack. Can turn any object, including objects too large to pick up, into a bomb, dealing explosive damage to the next person to touch it. The object will return to normal after the trap is triggered or after a delay."
- creator_icon = "explosive"
- /// Static list of signals that activate the boom.
- var/static/list/boom_signals = list(COMSIG_ATOM_ATTACKBY, COMSIG_ATOM_BUMPED, COMSIG_ATOM_ATTACK_HAND)
- /// After this amount of time passses, boom deactivates.
- var/decay_time = 1 MINUTES
- /// Time between bombs.
- var/bomb_cooldown_time = 20 SECONDS
- /// The cooldown timer between bombs.
- COOLDOWN_DECLARE(bomb_cooldown)
-
-/mob/living/simple_animal/hostile/guardian/explosive/get_status_tab_items()
- . = ..()
- if(!COOLDOWN_FINISHED(src, bomb_cooldown))
- . += "Bomb Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, bomb_cooldown))]"
-
-/mob/living/simple_animal/hostile/guardian/explosive/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
- if(LAZYACCESS(modifiers, RIGHT_CLICK) && proximity_flag && isobj(attack_target))
- plant_bomb(attack_target)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/explosive/proc/plant_bomb(obj/planting_on)
- if(!COOLDOWN_FINISHED(src, bomb_cooldown))
- to_chat(src, span_bolddanger("Your powers are on cooldown! You must wait [DisplayTimeText(COOLDOWN_TIMELEFT(src, bomb_cooldown))] between bombs."))
- return
- to_chat(src, span_bolddanger("Success! Bomb armed!"))
- COOLDOWN_START(src, bomb_cooldown, bomb_cooldown_time)
- RegisterSignal(planting_on, COMSIG_ATOM_EXAMINE, PROC_REF(display_examine))
- RegisterSignals(planting_on, boom_signals, PROC_REF(kaboom))
- addtimer(CALLBACK(src, PROC_REF(disable), planting_on), decay_time, TIMER_UNIQUE|TIMER_OVERRIDE)
-
-/mob/living/simple_animal/hostile/guardian/explosive/proc/kaboom(atom/source, mob/living/explodee)
- SIGNAL_HANDLER
- if(!istype(explodee))
- return
- if(explodee == src || explodee == summoner || hasmatchingsummoner(explodee))
- return
- to_chat(explodee, span_bolddanger("[source] was boobytrapped!"))
- to_chat(src, span_bolddanger("Success! Your trap caught [explodee]"))
- playsound(source, 'sound/effects/explosion2.ogg', 200, TRUE)
- new /obj/effect/temp_visual/explosion(get_turf(source))
- EX_ACT(explodee, EXPLODE_HEAVY)
- UNREGISTER_BOMB_SIGNALS(source)
-
-/mob/living/simple_animal/hostile/guardian/explosive/proc/disable(obj/rigged_obj)
- to_chat(src, span_bolddanger("Failure! Your trap didn't catch anyone this time."))
- UNREGISTER_BOMB_SIGNALS(rigged_obj)
-
-/mob/living/simple_animal/hostile/guardian/explosive/proc/display_examine(datum/source, mob/user, text)
- SIGNAL_HANDLER
- text += span_holoparasite("It glows with a strange light!")
-
-#undef UNREGISTER_BOMB_SIGNALS
diff --git a/code/modules/mob/living/simple_animal/guardian/types/gaseous.dm b/code/modules/mob/living/simple_animal/guardian/types/gaseous.dm
deleted file mode 100644
index 7808f8a6b48..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/gaseous.dm
+++ /dev/null
@@ -1,103 +0,0 @@
-//Gaseous
-/mob/living/simple_animal/hostile/guardian/gaseous
- melee_damage_lower = 10
- melee_damage_upper = 10
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 0)
- range = 7
- playstyle_string = span_holoparasite("As a gaseous type, you have only light damage resistance, but you can expel gas in an area. In addition, your punches cause sparks, and you make your summoner inflammable.")
- magic_fluff_string = span_holoparasite("..And draw the Atmospheric Technician, flooding the area with gas!")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Atmospheric modules activated. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! You caught one! OH GOD, EVERYTHING'S ON FIRE. Except you and the fish.")
- miner_fluff_string = span_holoparasite("You encounter... Plasma, the bringer of fire.")
- creator_name = "Gaseous"
- creator_desc = "Creates sparks on touch and continuously expels a gas of its choice. Automatically extinguishes the user if they catch on fire."
- creator_icon = "gaseous"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode/gases
- /// Gas being expelled.
- var/expelled_gas = null
- /// Rate of temperature stabilization per second.
- var/temp_stabilization_rate = 0.1
- /// Possible gases to expel, with how much moles they create.
- var/static/list/possible_gases = list(
- /datum/gas/oxygen = 50,
- /datum/gas/nitrogen = 750, //overpressurizing is hard!.
- /datum/gas/water_vapor = 1, //you need incredibly little water vapor for the effects to kick in
- /datum/gas/nitrous_oxide = 15,
- /datum/gas/carbon_dioxide = 50,
- /datum/gas/plasma = 3,
- /datum/gas/bz = 10,
- )
- /// Gas colors, used for the particles.
- var/static/list/gas_colors = list(
- /datum/gas/oxygen = "#63BFDD", //color of frozen oxygen
- /datum/gas/nitrogen = "#777777", //grey (grey)
- /datum/gas/water_vapor = "#96ADCF", //water is slightly blue
- /datum/gas/nitrous_oxide = "#FEFEFE", //white like the sprite
- /datum/gas/carbon_dioxide = "#222222", //black like coal
- /datum/gas/plasma = "#B233CC", //color of the plasma sprite
- /datum/gas/bz = "#FAFF00", //color of the bz metabolites reagent
- )
-
-/mob/living/simple_animal/hostile/guardian/gaseous/Initialize(mapload, theme)
- . = ..()
- RegisterSignal(src, COMSIG_ATOM_PRE_PRESSURE_PUSH, PROC_REF(stop_pressure))
-
-/mob/living/simple_animal/hostile/guardian/gaseous/AttackingTarget(atom/attacked_target)
- . = ..()
- if(!isliving(target))
- return
- do_sparks(1, TRUE, target)
-
-/mob/living/simple_animal/hostile/guardian/gaseous/recall(forced)
- expelled_gas = null
- QDEL_NULL(particles) //need to delete before putting in another object
- . = ..()
- if(. && summoner)
- UnregisterSignal(summoner, COMSIG_ATOM_PRE_PRESSURE_PUSH)
-
-/mob/living/simple_animal/hostile/guardian/gaseous/manifest(forced)
- . = ..()
- if(. && summoner)
- RegisterSignal(summoner, COMSIG_ATOM_PRE_PRESSURE_PUSH, PROC_REF(stop_pressure))
-
-/mob/living/simple_animal/hostile/guardian/gaseous/Life(seconds_per_tick, times_fired)
- . = ..()
- if(summoner)
- summoner.extinguish_mob()
- summoner.set_fire_stacks(0, remove_wet_stacks = FALSE)
- summoner.adjust_bodytemperature(get_temp_change_amount((summoner.get_body_temp_normal() - summoner.bodytemperature), temp_stabilization_rate * seconds_per_tick))
- if(!expelled_gas)
- return
- var/datum/gas_mixture/mix_to_spawn = new()
- mix_to_spawn.add_gas(expelled_gas)
- mix_to_spawn.gases[expelled_gas][MOLES] = possible_gases[expelled_gas] * seconds_per_tick
- mix_to_spawn.temperature = T20C
- var/turf/open/our_turf = get_turf(src)
- our_turf.assume_air(mix_to_spawn)
-
-/mob/living/simple_animal/hostile/guardian/gaseous/toggle_modes()
- var/list/gases = list("None")
- for(var/datum/gas/gas as anything in possible_gases)
- gases[initial(gas.name)] = gas
- var/picked_gas = tgui_input_list(src, "Select a gas to expel.", "Gas Producer", gases)
- if(picked_gas == "None")
- expelled_gas = null
- QDEL_NULL(particles)
- to_chat(src, span_notice("You stopped expelling gas."))
- return
- var/gas_type = gases[picked_gas]
- if(!picked_gas || !gas_type)
- return
- to_chat(src, span_bolddanger("You are now expelling [picked_gas]."))
- investigate_log("set their gas type to [picked_gas].", INVESTIGATE_ATMOS)
- expelled_gas = gas_type
- if(!particles)
- particles = new /particles/smoke/steam()
- particles.position = list(-1, 8, 0)
- particles.fadein = 5
- particles.height = 200
- particles.color = gas_colors[gas_type]
-
-/mob/living/simple_animal/hostile/guardian/gaseous/proc/stop_pressure(datum/source)
- SIGNAL_HANDLER
- return COMSIG_ATOM_BLOCKS_PRESSURE
diff --git a/code/modules/mob/living/simple_animal/guardian/types/gravitokinetic.dm b/code/modules/mob/living/simple_animal/guardian/types/gravitokinetic.dm
deleted file mode 100644
index fe6a0ec5856..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/gravitokinetic.dm
+++ /dev/null
@@ -1,91 +0,0 @@
-//gravitokinetic
-/mob/living/simple_animal/hostile/guardian/gravitokinetic
- melee_damage_lower = 15
- melee_damage_upper = 15
- damage_coeff = list(BRUTE = 0.75, BURN = 0.75, TOX = 0.75, CLONE = 0.75, STAMINA = 0, OXY = 0.75)
- playstyle_string = span_holoparasite("As a gravitokinetic type, you can right-click to make the gravity on the ground stronger, and punching applies this effect to a target.")
- magic_fluff_string = span_holoparasite("..And draw the Singularity, an anomalous force of terror.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Gravitokinetic modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one! It's a gravitokinetic carp! Now do you understand the gravity of the situation?")
- miner_fluff_string = span_holoparasite("You encounter... Bananium, a master of gravity business.")
- creator_name = "Gravitokinetic"
- creator_desc = "Attacks will apply crushing gravity to the target. Can target the ground as well to slow targets advancing on you, but this will affect the user."
- creator_icon = "gravitokinetic"
- /// Targets we have applied our effects on.
- var/list/gravity_targets = list()
- /// Distance in which our ability works
- var/gravity_power_range = 10
- /// Gravity added on punches.
- var/punch_gravity = 5
- /// Gravity added to turfs.
- var/turf_gravity = 3
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/Initialize(mapload, theme)
- . = ..()
- AddElement(/datum/element/forced_gravity, 1)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/set_summoner(mob/to_who, different_person)
- . = ..()
- to_who.AddElement(/datum/element/forced_gravity, 1)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/cut_summoner(different_person)
- summoner.RemoveElement(/datum/element/forced_gravity, 1)
- return ..()
-
-///Removes gravity from affected mobs upon guardian death to prevent permanent effects
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/death()
- . = ..()
- for(var/gravity_target in gravity_targets)
- remove_gravity(gravity_target)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/AttackingTarget(atom/attacked_target)
- . = ..()
- if(isliving(target) && !hasmatchingsummoner(attacked_target) && target != src && target != summoner && !gravity_targets[target])
- to_chat(src, span_bolddanger("Your punch has applied heavy gravity to [target]!"))
- add_gravity(target, punch_gravity)
- to_chat(target, span_userdanger("Everything feels really heavy!"))
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
- if(LAZYACCESS(modifiers, RIGHT_CLICK) && proximity_flag && !gravity_targets[target])
- slam_turf(attack_target)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/proc/slam_turf(turf/open/slammed)
- if(!isopenturf(slammed) || isgroundlessturf(slammed))
- to_chat(src, span_warning("You cannot add gravity to this!"))
- return
- visible_message(span_danger("[src] slams their fist into the [slammed]!"), span_notice("You modify the gravity of the [slammed]."))
- do_attack_animation(slammed)
- add_gravity(slammed, turf_gravity)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/recall_effects()
- to_chat(src, span_bolddanger("You have released your gravitokinetic powers!"))
- for(var/gravity_target in gravity_targets)
- remove_gravity(gravity_target)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- for(var/gravity_target in gravity_targets)
- if(get_dist(src, gravity_target) > gravity_power_range)
- remove_gravity(gravity_target)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/proc/add_gravity(atom/target, new_gravity = 3)
- if(gravity_targets[target])
- return
- target.AddElement(/datum/element/forced_gravity, new_gravity)
- gravity_targets[target] = new_gravity
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(distance_check))
- playsound(src, 'sound/effects/gravhit.ogg', 100, TRUE)
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/proc/remove_gravity(atom/target)
- if(isnull(gravity_targets[target]))
- return
- UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
- target.RemoveElement(/datum/element/forced_gravity, gravity_targets[target])
- gravity_targets -= target
-
-/mob/living/simple_animal/hostile/guardian/gravitokinetic/proc/distance_check(atom/movable/moving_target, old_loc, dir, forced)
- SIGNAL_HANDLER
- if(get_dist(src, moving_target) > gravity_power_range)
- remove_gravity(moving_target)
diff --git a/code/modules/mob/living/simple_animal/guardian/types/lightning.dm b/code/modules/mob/living/simple_animal/guardian/types/lightning.dm
deleted file mode 100644
index 9739445f7c9..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/lightning.dm
+++ /dev/null
@@ -1,111 +0,0 @@
-/obj/effect/ebeam/chain
- name = "lightning chain"
- layer = LYING_MOB_LAYER
- plane = GAME_PLANE_FOV_HIDDEN
-
-//Lightning
-/mob/living/simple_animal/hostile/guardian/lightning
- melee_damage_lower = 7
- melee_damage_upper = 7
- attack_verb_continuous = "shocks"
- attack_verb_simple = "shock"
- melee_damage_type = BURN
- attack_sound = 'sound/machines/defib_zap.ogg'
- damage_coeff = list(BRUTE = 0.7, BURN = 0.7, TOX = 0.7, CLONE = 0.7, STAMINA = 0, OXY = 0.7)
- range = 7
- playstyle_string = span_holoparasite("As a lightning type, you will apply lightning chains to targets on attack and have a lightning chain to your summoner. Lightning chains will shock anyone near them.")
- magic_fluff_string = span_holoparasite("..And draw the Tesla, a shocking, lethal source of power.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Lightning modules active. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one! It's a lightning carp! Everyone else goes zap zap.")
- miner_fluff_string = span_holoparasite("You encounter... Iron, a conductive master of lightning.")
- creator_name = "Lightning"
- creator_desc = "Attacks apply lightning chains to targets. Has a lightning chain to the user. Lightning chains shock everything near them, doing constant damage."
- creator_icon = "lightning"
- /// Beam datum of our lightning chain to the summoner.
- var/datum/beam/summonerchain
- /// List of all lightning chains attached to enemies.
- var/list/enemychains = list()
- /// Amount of shocks we've given through the chain to the summoner.
- var/successfulshocks = 0
- /// Cooldown between shocks.
- COOLDOWN_DECLARE(shock_cooldown)
-
-/mob/living/simple_animal/hostile/guardian/lightning/AttackingTarget(atom/attacked_target)
- . = ..()
- if(!. || !isliving(target) || target == summoner || hasmatchingsummoner(target))
- return
- cleardeletedchains()
- for(var/datum/beam/chain as anything in enemychains)
- if(chain.target == target)
- return //oh this guy already HAS a chain, let's not chain again
- if(length(enemychains) > 2)
- var/datum/beam/enemy_chain = pick(enemychains)
- qdel(enemy_chain)
- enemychains -= enemy_chain
- enemychains += Beam(target, "lightning[rand(1,12)]", maxdistance=7, beam_type=/obj/effect/ebeam/chain)
-
-/mob/living/simple_animal/hostile/guardian/lightning/manifest_effects()
- START_PROCESSING(SSfastprocess, src)
- if(summoner)
- summonerchain = Beam(summoner, "lightning[rand(1,12)]", beam_type=/obj/effect/ebeam/chain)
-
-/mob/living/simple_animal/hostile/guardian/lightning/recall_effects()
- STOP_PROCESSING(SSfastprocess, src)
- removechains()
-
-/mob/living/simple_animal/hostile/guardian/lightning/process(seconds_per_tick)
- if(!COOLDOWN_FINISHED(src, shock_cooldown))
- return
- if(successfulshocks > 5)
- successfulshocks = 0
- if(shockallchains())
- successfulshocks++
- COOLDOWN_START(src, shock_cooldown, 0.3 SECONDS)
-
-/mob/living/simple_animal/hostile/guardian/lightning/proc/cleardeletedchains()
- if(summonerchain && QDELETED(summonerchain))
- summonerchain = null
- for(var/datum/chain as anything in enemychains)
- if(QDELETED(chain))
- enemychains -= chain
-
-/mob/living/simple_animal/hostile/guardian/lightning/proc/shockallchains()
- . = 0
- cleardeletedchains()
- if(summonerchain)
- . += chainshock(summonerchain)
- for(var/chain in enemychains)
- . += chainshock(chain)
-
-/mob/living/simple_animal/hostile/guardian/lightning/proc/removechains()
- QDEL_NULL(summonerchain)
- for(var/chain in enemychains)
- qdel(chain)
- enemychains = list()
-
-/mob/living/simple_animal/hostile/guardian/lightning/proc/chainshock(datum/beam/B) //fuck you, fuck this
- . = 0
- var/list/turfs = list()
- for(var/E in B.elements)
- var/obj/effect/ebeam/chainpart = E
- if(chainpart && chainpart.x && chainpart.y && chainpart.z)
- var/turf/T = get_turf_pixel(chainpart)
- turfs |= T
- if(T != get_turf(B.origin) && T != get_turf(B.target))
- for(var/turf/TU in circle_range(T, 1))
- turfs |= TU
- for(var/turf in turfs)
- var/turf/T = turf
- for(var/mob/living/L in T)
- if(L.stat != DEAD && L != src && L != summoner)
- if(hasmatchingsummoner(L)) //if the summoner matches don't hurt them
- continue
- if(successfulshocks > 4)
- L.electrocute_act(0)
- L.visible_message(
- span_danger("[L] was shocked by the lightning chain!"), \
- span_userdanger("You are shocked by the lightning chain!"), \
- span_hear("You hear a heavy electrical crack.") \
- )
- L.adjustFireLoss(1.2) //adds up very rapidly
- . = 1
diff --git a/code/modules/mob/living/simple_animal/guardian/types/protector.dm b/code/modules/mob/living/simple_animal/guardian/types/protector.dm
deleted file mode 100644
index 4809c3dc1c6..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/protector.dm
+++ /dev/null
@@ -1,78 +0,0 @@
-//Protector
-/mob/living/simple_animal/hostile/guardian/protector
- melee_damage_lower = 15
- melee_damage_upper = 15
- range = 15 //worse for it due to how it leashes
- damage_coeff = list(BRUTE = 0.4, BURN = 0.4, TOX = 0.4, CLONE = 0.4, STAMINA = 0, OXY = 0.4)
- playstyle_string = span_holoparasite("As a protector type you cause your summoner to leash to you instead of you leashing to them and have two modes; Combat Mode, where you do and take medium damage, and Protection Mode, where you do and take almost no damage, but move slightly slower.")
- magic_fluff_string = span_holoparasite("..And draw the Guardian, a stalwart protector that never leaves the side of its charge.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Protector modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! You caught one! Wait, no... it caught you! The fisher has become the fishy.")
- miner_fluff_string = span_holoparasite("You encounter... Uranium, a very resistant guardian.")
- creator_name = "Protector"
- creator_desc = "Causes you to teleport to it when out of range, unlike other parasites. Has two modes; Combat, where it does and takes medium damage, and Protection, where it does and takes almost no damage but moves slightly slower."
- creator_icon = "protector"
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode
- /// Damage removed in protecting mode.
- var/damage_penalty = 13
- /// Is it in protecting mode?
- var/toggle = FALSE
- /// Overlay of our protection shield.
- var/mutable_appearance/shield_overlay
-
-/mob/living/simple_animal/hostile/guardian/protector/ex_act(severity)
- if(severity >= EXPLODE_DEVASTATE)
- adjustBruteLoss(400) //if in protector mode, will do 20 damage and not actually necessarily kill the summoner
- else
- . = ..()
- if(QDELETED(src))
- return FALSE
- if(toggle)
- visible_message(span_danger("The explosion glances off [src]'s energy shielding!"))
-
- return TRUE
-
-/mob/living/simple_animal/hostile/guardian/protector/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- . = ..()
- if(. > 0 && toggle)
- var/image/flash_overlay = new('icons/effects/effects.dmi', src, "shield-flash", dir = pick(GLOB.cardinals))
- flash_overlay.color = guardian_color
- flick_overlay_view(flash_overlay, 0.5 SECONDS)
-
-/mob/living/simple_animal/hostile/guardian/protector/toggle_modes()
- if(COOLDOWN_FINISHED(src, manifest_cooldown))
- return
- COOLDOWN_START(src, manifest_cooldown, 1 SECONDS)
- if(toggle)
- cut_overlay(shield_overlay)
- melee_damage_lower += damage_penalty
- melee_damage_upper += damage_penalty
- speed = initial(speed)
- damage_coeff = list(BRUTE = 0.4, BURN = 0.4, TOX = 0.4, CLONE = 0.4, STAMINA = 0, OXY = 0.4)
- to_chat(src, span_bolddanger("You switch to combat mode."))
- toggle = FALSE
- else
- if(!shield_overlay)
- shield_overlay = mutable_appearance('icons/effects/effects.dmi', "shield-grey")
- shield_overlay.color = guardian_color
- add_overlay(shield_overlay)
- melee_damage_lower -= damage_penalty
- melee_damage_upper -= damage_penalty
- speed = 1
- damage_coeff = list(BRUTE = 0.05, BURN = 0.05, TOX = 0.05, CLONE = 0.05, STAMINA = 0, OXY = 0.05) //damage? what's damage?
- to_chat(src, span_bolddanger("You switch to protection mode."))
- toggle = TRUE
-
-/mob/living/simple_animal/hostile/guardian/protector/check_distance() //snap to what? snap to the guardian!
- if(!summoner || get_dist(summoner, src) <= range)
- return
- if(istype(summoner.loc, /obj/effect))
- to_chat(src, span_holoparasite("You moved out of range, and were pulled back! You can only move [range] meters from [summoner.real_name]!"))
- visible_message(span_danger("\The [src] jumps back to its user."))
- recall(forced = TRUE)
- return
- to_chat(summoner, span_holoparasite("You moved out of range, and were pulled back! You can only move [range] meters from [real_name]!"))
- summoner.visible_message(span_danger("\The [summoner] jumps back to [summoner.p_their()] protector."))
- new /obj/effect/temp_visual/guardian/phase/out(get_turf(summoner))
- summoner.forceMove(get_turf(src))
- new /obj/effect/temp_visual/guardian/phase(get_turf(summoner))
diff --git a/code/modules/mob/living/simple_animal/guardian/types/ranged.dm b/code/modules/mob/living/simple_animal/guardian/types/ranged.dm
deleted file mode 100644
index 0cb7303e26a..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/ranged.dm
+++ /dev/null
@@ -1,182 +0,0 @@
-//Ranged
-/obj/projectile/guardian
- name = "crystal spray"
- icon_state = "guardian"
- damage = 5
- damage_type = BRUTE
- armour_penetration = 100
-
-/mob/living/simple_animal/hostile/guardian/ranged
- combat_mode = FALSE
- friendly_verb_continuous = "quietly assesses"
- friendly_verb_simple = "quietly assess"
- melee_damage_lower = 10
- melee_damage_upper = 10
- damage_coeff = list(BRUTE = 0.9, BURN = 0.9, TOX = 0.9, CLONE = 0.9, STAMINA = 0, OXY = 0.9)
- projectiletype = /obj/projectile/guardian
- ranged_cooldown_time = 1 //fast!
- projectilesound = 'sound/effects/hit_on_shattered_glass.ogg'
- ranged = 1
- range = 13
- playstyle_string = span_holoparasite("As a ranged type, you have only light damage resistance, but are capable of spraying shards of crystal at incredibly high speed. You can also deploy surveillance snares to monitor enemy movement. Finally, you can switch to scout mode, in which you can't attack, but can move without limit.")
- magic_fluff_string = span_holoparasite("..And draw the Sentinel, an alien master of ranged combat.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Ranged combat modules active. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! Caught one, it's a ranged carp. This fishy can watch people pee in the ocean.")
- miner_fluff_string = span_holoparasite("You encounter... Diamond, a powerful projectile thrower.")
- creator_name = "Ranged"
- creator_desc = "Has two modes. Ranged; which fires a constant stream of weak, armor-ignoring projectiles. Scout; where it cannot attack, but can move through walls and is quite hard to see. Can lay surveillance snares, which alert it when crossed, in either mode."
- creator_icon = "ranged"
- see_invisible = SEE_INVISIBLE_LIVING
- toggle_button_type = /atom/movable/screen/guardian/toggle_mode
- /// List of all deployed snares.
- var/list/snares = list()
- /// Is it in scouting mode?
- var/toggle = FALSE
- /// Maximum snares deployed at once.
- var/max_snares = 6
- /// Lower damage before scouting.
- var/previous_lower_damage = 0
- /// Upper damage before scouting.
- var/previous_upper_damage = 0
-
-/mob/living/simple_animal/hostile/guardian/ranged/toggle_modes()
- if(is_deployed() && summoner)
- to_chat(src, span_bolddanger("You have to be recalled to toggle modes!"))
- return
- if(toggle)
- ranged = initial(ranged)
- melee_damage_lower = previous_lower_damage
- melee_damage_upper = previous_upper_damage
- previous_lower_damage = 0
- previous_upper_damage = 0
- obj_damage = initial(obj_damage)
- environment_smash = initial(environment_smash)
- alpha = 255
- range = initial(range)
- to_chat(src, span_bolddanger("You switch to combat mode."))
- toggle = FALSE
- else
- ranged = 0
- previous_lower_damage = melee_damage_lower
- melee_damage_lower = 0
- previous_upper_damage = melee_damage_upper
- melee_damage_upper = 0
- obj_damage = 0
- environment_smash = ENVIRONMENT_SMASH_NONE
- alpha = 45
- range = 255
- to_chat(src, span_bolddanger("You switch to scout mode."))
- toggle = TRUE
-
-
-/mob/living/simple_animal/hostile/guardian/ranged/Shoot(atom/targeted_atom)
- . = ..()
- if(!istype(., /obj/projectile))
- return
- var/obj/projectile/shot_projectile = .
- shot_projectile.color = guardian_color
-
-/mob/living/simple_animal/hostile/guardian/ranged/toggle_light()
- var/msg
- switch(lighting_cutoff)
- if (LIGHTING_CUTOFF_VISIBLE)
- lighting_cutoff_red = 10
- lighting_cutoff_green = 10
- lighting_cutoff_blue = 15
- msg = "You activate your night vision."
- if (LIGHTING_CUTOFF_MEDIUM)
- lighting_cutoff_red = 25
- lighting_cutoff_green = 25
- lighting_cutoff_blue = 35
- msg = "You increase your night vision."
- if (LIGHTING_CUTOFF_HIGH)
- lighting_cutoff_red = 35
- lighting_cutoff_green = 35
- lighting_cutoff_blue = 50
- msg = "You maximize your night vision."
- else
- lighting_cutoff_red = 0
- lighting_cutoff_green = 0
- lighting_cutoff_blue = 0
- msg = "You deactivate your night vision."
- sync_lighting_plane_cutoff()
- to_chat(src, span_notice(msg))
-
-
-/mob/living/simple_animal/hostile/guardian/ranged/verb/Snare()
- set name = "Set Surveillance Snare"
- set category = "Guardian"
- set desc = "Set an invisible snare that will alert you when living creatures walk over it. Max of 5"
- if(length(snares) < max_snares)
- var/turf/snare_loc = get_turf(src)
- var/obj/effect/snare/new_snare = new /obj/effect/snare(snare_loc, src)
- new_snare.name = "[get_area(snare_loc)] snare ([rand(1, 1000)])"
- snares += new_snare
- to_chat(src, span_bolddanger("Surveillance snare deployed!"))
- else
- to_chat(src, span_bolddanger("You have too many snares deployed. Remove some first."))
-
-/mob/living/simple_animal/hostile/guardian/ranged/verb/DisarmSnare()
- set name = "Remove Surveillance Snare"
- set category = "Guardian"
- set desc = "Disarm unwanted surveillance snares."
- var/picked_snare = tgui_input_list(src, "Pick which snare to remove.", "Remove Snare", sort_names(snares))
- if(isnull(picked_snare))
- return
- qdel(picked_snare)
- to_chat(src, span_bolddanger("Snare disarmed."))
-
-/obj/effect/snare
- name = "snare"
- desc = "You shouldn't be seeing this!"
- invisibility = INVISIBILITY_ABSTRACT
- var/datum/weakref/guardian_ref
-
-/obj/effect/snare/Initialize(mapload, spawning_guardian)
- . = ..()
- guardian_ref = WEAKREF(spawning_guardian)
- var/static/list/loc_connections = list(
- COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
- )
- AddElement(/datum/element/connect_loc, loc_connections)
-
-/obj/effect/snare/Destroy(force)
- var/mob/living/simple_animal/hostile/guardian/ranged/spawning_guardian = guardian_ref?.resolve()
- if(spawning_guardian)
- spawning_guardian.snares -= src
- return ..()
-
-/obj/effect/snare/proc/on_entered(datum/source, crossed_object)
- SIGNAL_HANDLER
- var/mob/living/simple_animal/hostile/guardian/ranged/spawning_guardian = guardian_ref?.resolve()
- if(!spawning_guardian)
- qdel(src)
- return
- if(!isliving(crossed_object) || crossed_object == spawning_guardian || spawning_guardian.hasmatchingsummoner(crossed_object))
- return
- send_message(spawning_guardian.summoner || spawning_guardian, crossed_object)
-
-/obj/effect/snare/proc/send_message(mob/living/recipient, crossed_object)
- to_chat(recipient, span_bolddanger("[crossed_object] has crossed [name]."))
- var/list/guardians = recipient.get_all_linked_holoparasites()
- for(var/guardian in guardians)
- send_message(guardian, crossed_object)
-
-/obj/effect/snare/singularity_act()
- return
-
-/obj/effect/snare/singularity_pull()
- return
-
-/mob/living/simple_animal/hostile/guardian/ranged/manifest_effects()
- if(toggle)
- incorporeal_move = INCORPOREAL_MOVE_BASIC
-
-/mob/living/simple_animal/hostile/guardian/ranged/recall_effects()
- // To stop scout mode from moving when recalled
- incorporeal_move = FALSE
-
-/mob/living/simple_animal/hostile/guardian/ranged/AttackingTarget(atom/attacked_target)
- if(toggle)
- return
- return ..()
diff --git a/code/modules/mob/living/simple_animal/guardian/types/standard.dm b/code/modules/mob/living/simple_animal/guardian/types/standard.dm
deleted file mode 100644
index 89f671d3ffa..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/standard.dm
+++ /dev/null
@@ -1,41 +0,0 @@
-//Standard
-/mob/living/simple_animal/hostile/guardian/standard
- damage_coeff = list(BRUTE = 0.5, BURN = 0.5, TOX = 0.5, CLONE = 0.5, STAMINA = 0, OXY = 0.5)
- melee_damage_lower = 20
- melee_damage_upper = 20
- wound_bonus = -5 //you can wound!
- obj_damage = 80
- next_move_modifier = 0.8 //attacks 20% faster
- environment_smash = ENVIRONMENT_SMASH_WALLS
- playstyle_string = span_holoparasite("As a standard type you have no special abilities, but have a high damage resistance and a powerful attack capable of smashing through walls.")
- magic_fluff_string = span_holoparasite("..And draw the Assistant, faceless and generic, but never to be underestimated.")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Standard combat modules loaded. Holoparasite swarm online.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! You caught one! It's really boring and standard. Better punch some walls to ease the tension.")
- miner_fluff_string = span_holoparasite("You encounter... Adamantine, a powerful attacker.")
- creator_name = "Standard"
- creator_desc = "Devastating close combat attacks and high damage resistance. Can smash through weak walls."
- creator_icon = "standard"
- /// The text we shout when attacking.
- var/battlecry = "AT"
-
-/mob/living/simple_animal/hostile/guardian/standard/verb/Battlecry()
- set name = "Set Battlecry"
- set category = "Guardian"
- set desc = "Choose what you shout as you punch people."
- var/input = tgui_input_text(src, "What do you want your battlecry to be?", "Battle Cry", max_length = 6)
- if(input)
- battlecry = input
-
-/mob/living/simple_animal/hostile/guardian/standard/AttackingTarget(atom/attacked_target)
- . = ..()
- if(!isliving(target) || attacked_target == src)
- return
- var/msg = ""
- for(var/i in 1 to 9)
- msg += battlecry
- say("[msg]!!", ignore_spam = TRUE)
- for(var/j in 1 to 4)
- addtimer(CALLBACK(src, PROC_REF(do_attack_sound), target.loc), j)
-
-/mob/living/simple_animal/hostile/guardian/standard/proc/do_attack_sound(atom/playing_from)
- playsound(playing_from, attack_sound, 50, TRUE, TRUE)
diff --git a/code/modules/mob/living/simple_animal/guardian/types/support.dm b/code/modules/mob/living/simple_animal/guardian/types/support.dm
deleted file mode 100644
index 9afdf231ce7..00000000000
--- a/code/modules/mob/living/simple_animal/guardian/types/support.dm
+++ /dev/null
@@ -1,141 +0,0 @@
-//Support
-/mob/living/simple_animal/hostile/guardian/support
- speed = 0
- damage_coeff = list(BRUTE = 0.7, BURN = 0.7, TOX = 0.7, CLONE = 0.7, STAMINA = 0, OXY = 0.7)
- melee_damage_lower = 15
- melee_damage_upper = 15
- playstyle_string = span_holoparasite("As a support type, you may right-click to heal targets. In addition, alt-clicking on an adjacent object or mob will warp them to your bluespace beacon after a short delay.")
- magic_fluff_string = span_holoparasite("..And draw the Chief Medical Officer, a potent force of life... and death.")
- carp_fluff_string = span_holoparasite("CARP CARP CARP! You caught a support carp. It's a kleptocarp!")
- tech_fluff_string = span_holoparasite("Boot sequence complete. Support modules active. Holoparasite swarm online.")
- miner_fluff_string = span_holoparasite("You encounter... Bluespace, the master of support.")
- creator_name = "Support"
- creator_desc = "Does medium damage, but can heal its targets and create beacons to teleport people and things to."
- creator_icon = "support"
- /// Is it in healing mode?
- var/toggle = FALSE
- /// How much we heal per hit.
- var/healing_amount = 5
- /// Our teleportation beacon.
- var/obj/structure/receiving_pad/beacon
- /// Time it takes to teleport.
- var/teleporting_time = 6 SECONDS
- /// Time between creating beacons.
- var/beacon_cooldown_time = 5 MINUTES
- /// Cooldown between creating beacons.
- COOLDOWN_DECLARE(beacon_cooldown)
-
-/mob/living/simple_animal/hostile/guardian/support/Initialize(mapload)
- . = ..()
- var/datum/atom_hud/medsensor = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED]
- medsensor.show_to(src)
-
-/mob/living/simple_animal/hostile/guardian/support/get_status_tab_items()
- . = ..()
- if(!COOLDOWN_FINISHED(src, beacon_cooldown))
- . += "Beacon Cooldown Remaining: [DisplayTimeText(COOLDOWN_TIMELEFT(src, beacon_cooldown))]"
-
-/mob/living/simple_animal/hostile/guardian/support/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
- if(LAZYACCESS(modifiers, RIGHT_CLICK) && proximity_flag && isliving(attack_target))
- heal_target(attack_target)
- return
- return ..()
-
-/mob/living/simple_animal/hostile/guardian/support/proc/heal_target(mob/living/target)
- do_attack_animation(target, ATTACK_EFFECT_PUNCH)
- target.visible_message(span_notice("[src] heals [target]!"),\
- span_userdanger("[src] heals you!"), null, COMBAT_MESSAGE_RANGE, src)
- to_chat(src, span_notice("You heal [target]!"))
- playsound(target, attack_sound, 50, TRUE, TRUE, frequency = -1) //play punch in REVERSE
- var/need_mob_update
- need_mob_update = target.adjustBruteLoss(-healing_amount, updating_health = FALSE)
- need_mob_update += target.adjustFireLoss(-healing_amount, updating_health = FALSE)
- need_mob_update += target.adjustOxyLoss(-healing_amount, updating_health = FALSE)
- need_mob_update += target.adjustToxLoss(-healing_amount, updating_health = FALSE, forced = TRUE)
- if(need_mob_update)
- target.updatehealth()
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(target))
- heal_effect.color = guardian_color
-
-/mob/living/simple_animal/hostile/guardian/support/verb/Beacon()
- set name = "Place Bluespace Beacon"
- set category = "Guardian"
- set desc = "Mark a floor as your beacon point, allowing you to warp targets to it. Your beacon will not work at extreme distances."
-
- if(!COOLDOWN_FINISHED(src, beacon_cooldown))
- to_chat(src, span_bolddanger("Your power is on cooldown. You must wait five minutes between placing beacons."))
- return
-
- var/turf/beacon_loc = get_turf(src.loc)
- if(!isfloorturf(beacon_loc))
- return
-
- if(beacon)
- beacon.disappear()
- beacon = null
-
- beacon = new(beacon_loc, src)
-
- to_chat(src, span_bolddanger("Beacon placed! You may now warp targets and objects to it, including your user, via Alt+Click."))
-
- COOLDOWN_START(src, beacon_cooldown, beacon_cooldown_time)
-
-/obj/structure/receiving_pad
- name = "bluespace receiving pad"
- icon = 'icons/turf/floors.dmi'
- desc = "A receiving zone for bluespace teleportations."
- icon_state = "light_on-8"
- light_range = MINIMUM_USEFUL_LIGHT_RANGE
- density = FALSE
- anchored = TRUE
- plane = FLOOR_PLANE
- layer = ABOVE_OPEN_TURF_LAYER
-
-/obj/structure/receiving_pad/New(loc, mob/living/simple_animal/hostile/guardian/spawning_guardian)
- . = ..()
- add_atom_colour(spawning_guardian?.guardian_color, FIXED_COLOUR_PRIORITY)
-
-/obj/structure/receiving_pad/proc/disappear()
- visible_message(span_notice("[src] vanishes!"))
- qdel(src)
-
-/mob/living/simple_animal/hostile/guardian/support/AltClickOn(atom/movable/target)
- teleport_to_beacon(target)
-
-/mob/living/simple_animal/hostile/guardian/support/proc/teleport_to_beacon(atom/movable/teleport_target)
- if(!istype(teleport_target))
- return
- if(!beacon)
- to_chat(src, span_bolddanger("You need a beacon placed to warp things!"))
- return
- if(!is_deployed())
- to_chat(src, span_bolddanger("You must be manifested to warp a target!"))
- return
- if(!Adjacent(teleport_target))
- to_chat(src, span_bolddanger("You must be adjacent to your target!"))
- return
- if(teleport_target.anchored)
- to_chat(src, span_bolddanger("Your target is anchored!"))
- return
- var/turf/target_turf = get_turf(teleport_target)
- if(beacon.z != target_turf.z)
- to_chat(src, span_bolddanger("The beacon is too far away to warp to!"))
- return
- to_chat(src, span_bolddanger("You begin to warp [teleport_target]."))
- teleport_target.visible_message(span_danger("[teleport_target] starts to glow faintly!"), \
- span_userdanger("You start to faintly glow, and you feel strangely weightless!"))
- do_attack_animation(teleport_target)
- playsound(teleport_target, attack_sound, 50, TRUE, TRUE, frequency = -1) //play punch in REVERSE
- if(!do_after(src, teleporting_time, teleport_target)) //now start the channel
- to_chat(src, span_bolddanger("You need to hold still!"))
- return
- new /obj/effect/temp_visual/guardian/phase/out(target_turf)
- if(isliving(teleport_target))
- var/mob/living/living_target = teleport_target
- living_target.flash_act()
- teleport_target.visible_message(
- span_danger("[teleport_target] disappears in a flash of light!"), \
- span_userdanger("Your vision is obscured by a flash of light!"), \
- )
- do_teleport(teleport_target, beacon, 0, channel = TELEPORT_CHANNEL_BLUESPACE)
- new /obj/effect/temp_visual/guardian/phase(get_turf(teleport_target))
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 4c382a59b2f..ed304e276b0 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -133,9 +133,6 @@
///Played when someone punches the creature.
var/attacked_sound = SFX_PUNCH
- ///If the creature has, and can use, hands.
- var/dextrous = FALSE
-
///The Status of our AI, can be set to AI_ON (On, usual processing), AI_IDLE (Will not process, but will return to AI_ON if an enemy comes near), AI_OFF (Off, Not processing ever), AI_Z_OFF (Temporarily off due to nonpresence of players).
var/AIStatus = AI_ON
///once we have become sentient, we can never go back.
@@ -176,10 +173,6 @@
if(!loc)
stack_trace("Simple animal being instantiated in nullspace")
update_simplemob_varspeed()
- if(dextrous)
- AddElement(/datum/element/dextrous, hud_type = hud_type)
- AddComponent(/datum/component/personal_crafting)
- add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
ADD_TRAIT(src, TRAIT_NOFIRE_SPREAD, ROUNDSTART_TRAIT)
if(length(weather_immunities))
add_traits(weather_immunities, ROUNDSTART_TRAIT)
diff --git a/code/modules/movespeed/modifiers/status_effects.dm b/code/modules/movespeed/modifiers/status_effects.dm
index 65245880ef4..4768f66a544 100644
--- a/code/modules/movespeed/modifiers/status_effects.dm
+++ b/code/modules/movespeed/modifiers/status_effects.dm
@@ -53,3 +53,6 @@
/datum/movespeed_modifier/status_effect/midas_blight/gold
multiplicative_slowdown = 2
+
+/datum/movespeed_modifier/status_effect/guardian_shield
+ multiplicative_slowdown = 1
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index ffeefaad688..548c3a52b57 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -1,38 +1,38 @@
GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
/mob/living,
- /obj/structure/blob,
- /obj/effect/rune,
- /obj/item/disk/nuclear,
- /obj/machinery/nuclearbomb,
- /obj/item/beacon,
- /obj/narsie,
- /obj/tear_in_reality,
- /obj/machinery/teleport/station,
- /obj/machinery/teleport/hub,
- /obj/machinery/quantumpad,
- /obj/effect/mob_spawn,
+ /obj/docking_port,
/obj/effect/hierophant,
- /obj/structure/receiving_pad,
- /obj/item/warp_cube,
- /obj/machinery/rnd/production, //print tracking beacons, send shuttle
- /obj/machinery/autolathe, //same
- /obj/projectile/beam/wormhole,
+ /obj/effect/mob_spawn,
/obj/effect/portal,
- /obj/item/shared_storage,
- /obj/structure/extraction_point,
- /obj/machinery/syndicatebomb,
+ /obj/effect/rune,
+ /obj/item/beacon,
+ /obj/item/disk/nuclear,
+ /obj/item/gps,
/obj/item/hilbertshotel,
- /obj/item/swapper,
- /obj/docking_port,
- /obj/machinery/launchpad,
- /obj/machinery/exodrone_launcher,
- /obj/machinery/disposal,
- /obj/structure/disposalpipe,
/obj/item/mail,
+ /obj/item/shared_storage,
+ /obj/item/swapper,
+ /obj/item/warp_cube,
+ /obj/machinery/autolathe, // In case you manage to get it to print a beacon while in transit
/obj/machinery/camera,
- /obj/item/gps,
- /obj/structure/checkoutmachine,
+ /obj/machinery/disposal,
+ /obj/machinery/exodrone_launcher,
/obj/machinery/fax,
+ /obj/machinery/launchpad,
+ /obj/machinery/nuclearbomb,
+ /obj/machinery/quantumpad,
+ /obj/machinery/rnd/production,
+ /obj/machinery/syndicatebomb,
+ /obj/machinery/teleport/hub,
+ /obj/machinery/teleport/station,
+ /obj/narsie,
+ /obj/projectile/beam/wormhole,
+ /obj/structure/blob,
+ /obj/structure/checkoutmachine,
+ /obj/structure/disposalpipe,
+ /obj/structure/extraction_point,
+ /obj/structure/guardian_beacon,
+ /obj/tear_in_reality,
)))
/// How many goody orders we can fit in a lockbox before we upgrade to a crate
diff --git a/code/modules/spells/spell_types/pointed/mind_transfer.dm b/code/modules/spells/spell_types/pointed/mind_transfer.dm
index 72441362b65..fa401c3b432 100644
--- a/code/modules/spells/spell_types/pointed/mind_transfer.dm
+++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm
@@ -69,7 +69,7 @@
return FALSE
if(isguardian(cast_on))
- var/mob/living/simple_animal/hostile/guardian/stand = cast_on
+ var/mob/living/basic/guardian/stand = cast_on
if(stand.summoner && stand.summoner == owner)
to_chat(owner, span_warning("Swapping minds with your own guardian would just put you back into your own head!"))
return FALSE
@@ -96,7 +96,7 @@
var/mob/living/to_swap = cast_on
if(isguardian(cast_on))
- var/mob/living/simple_animal/hostile/guardian/stand = cast_on
+ var/mob/living/basic/guardian/stand = cast_on
if(stand.summoner)
to_swap = stand.summoner
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 3b3c110432d..a00d32aaade 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -59,18 +59,6 @@
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/asteroid/wolf,
/mob/living/simple_animal/hostile/dark_wizard,
- /mob/living/simple_animal/hostile/guardian,
- /mob/living/simple_animal/hostile/guardian/assassin,
- /mob/living/simple_animal/hostile/guardian/charger,
- /mob/living/simple_animal/hostile/guardian/dextrous,
- /mob/living/simple_animal/hostile/guardian/explosive,
- /mob/living/simple_animal/hostile/guardian/gaseous,
- /mob/living/simple_animal/hostile/guardian/gravitokinetic,
- /mob/living/simple_animal/hostile/guardian/lightning,
- /mob/living/simple_animal/hostile/guardian/protector,
- /mob/living/simple_animal/hostile/guardian/ranged,
- /mob/living/simple_animal/hostile/guardian/standard,
- /mob/living/simple_animal/hostile/guardian/support,
/mob/living/simple_animal/hostile/illusion,
/mob/living/simple_animal/hostile/illusion/escape,
/mob/living/simple_animal/hostile/illusion/mirage,
diff --git a/code/modules/unit_tests/spell_shapeshift.dm b/code/modules/unit_tests/spell_shapeshift.dm
index 9b5804e3520..3b598e99942 100644
--- a/code/modules/unit_tests/spell_shapeshift.dm
+++ b/code/modules/unit_tests/spell_shapeshift.dm
@@ -87,7 +87,7 @@
shift.shapeshift_type = shift.possible_shapes[1]
shift.Grant(dummy)
- var/mob/living/simple_animal/hostile/guardian/test_stand = allocate(/mob/living/simple_animal/hostile/guardian)
+ var/mob/living/basic/guardian/test_stand = allocate(/mob/living/basic/guardian)
test_stand.set_summoner(dummy)
// The stand's summoner is dummy.
diff --git a/code/modules/uplink/uplink_items/dangerous.dm b/code/modules/uplink/uplink_items/dangerous.dm
index f1788c6e1de..e84657e7884 100644
--- a/code/modules/uplink/uplink_items/dangerous.dm
+++ b/code/modules/uplink/uplink_items/dangerous.dm
@@ -82,7 +82,7 @@
desc = "Though capable of near sorcerous feats via use of hardlight holograms and nanomachines, they require an \
organic host as a home base and source of fuel. Holoparasites come in various types and share damage with their host."
progression_minimum = 30 MINUTES
- item = /obj/item/guardiancreator/tech/choose/traitor
+ item = /obj/item/guardian_creator/tech
cost = 18
surplus = 0
purchasable_from = ~(UPLINK_NUKE_OPS | UPLINK_CLOWN_OPS)
diff --git a/modular_skyrat/modules/moretraitoritems/code/syndicate.dm b/modular_skyrat/modules/moretraitoritems/code/syndicate.dm
index 93561ec72da..7f352bb5603 100644
--- a/modular_skyrat/modules/moretraitoritems/code/syndicate.dm
+++ b/modular_skyrat/modules/moretraitoritems/code/syndicate.dm
@@ -140,8 +140,8 @@
wound = 5
//More items
-/obj/item/guardiancreator/tech/choose/traitor/opfor
- allowling = TRUE
+/obj/item/guardian_creator/tech/choose/traitor/opfor
+ allow_changeling = TRUE
/obj/item/codeword_granter
name = "codeword manual"
diff --git a/modular_skyrat/modules/opposing_force/code/equipment/gadgets.dm b/modular_skyrat/modules/opposing_force/code/equipment/gadgets.dm
index 0a81d7fb918..6b35ace5096 100644
--- a/modular_skyrat/modules/opposing_force/code/equipment/gadgets.dm
+++ b/modular_skyrat/modules/opposing_force/code/equipment/gadgets.dm
@@ -22,7 +22,7 @@
item_type = /obj/item/storage/box/syndie_kit/nuke
/datum/opposing_force_equipment/gadget/holoparasite
- item_type = /obj/item/guardiancreator/tech/choose/traitor
+ item_type = /obj/item/guardian_creator/tech/choose/traitor
admin_note = "Lets a ghost take control of a guardian spirit bound to the user. RRs both the ghost and user on death."
/datum/opposing_force_equipment/gadget/stimpack
diff --git a/strings/names/guardian_descriptions.txt b/strings/names/guardian_descriptions.txt
new file mode 100644
index 00000000000..678ec61fef6
--- /dev/null
+++ b/strings/names/guardian_descriptions.txt
@@ -0,0 +1,24 @@
+Black
+Blazing
+Bloody
+Blue
+Bronze
+Dawn
+Dusk
+Gold
+Green
+Grey
+Iron
+Midnight
+Orange
+Pink
+Plastitanium
+Purple
+Red
+Shimmering
+Shining
+Silver
+Sparkling
+Steel
+White
+Yellow
diff --git a/strings/names/guardian_gamepieces.txt b/strings/names/guardian_gamepieces.txt
new file mode 100644
index 00000000000..10a99cf38fb
--- /dev/null
+++ b/strings/names/guardian_gamepieces.txt
@@ -0,0 +1,13 @@
+Ace
+Bishop
+Club
+Diamond
+Heart
+Jack
+Joker
+King
+Knight
+Pawn
+Queen
+Rook
+Spade
diff --git a/strings/names/guardian_tarot.txt b/strings/names/guardian_tarot.txt
new file mode 100644
index 00000000000..5772c90d7ae
--- /dev/null
+++ b/strings/names/guardian_tarot.txt
@@ -0,0 +1,23 @@
+Chariot
+Death
+Devil
+Emperor
+Empress
+Fool
+Fortune
+Hangman
+Hermit
+Hierophant
+Judgement
+Justice
+Lover
+Magician
+Moon
+Priestess
+Star
+Strength
+Sun
+Temperance
+Tower
+Wheel
+World
diff --git a/tgstation.dme b/tgstation.dme
index 310b9c85f44..f549dcfb980 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -363,6 +363,7 @@
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_arcade.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_basic.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_carbon.dm"
+#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_guardian.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_living.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_main.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_silicon.dm"
@@ -1114,10 +1115,12 @@
#include "code\datums\components\curse_of_polymorph.dm"
#include "code\datums\components\customizable_reagent_holder.dm"
#include "code\datums\components\damage_aura.dm"
+#include "code\datums\components\damage_chain.dm"
#include "code\datums\components\deadchat_control.dm"
#include "code\datums\components\death_linked.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\deployable.dm"
+#include "code\datums\components\direct_explosive_trap.dm"
#include "code\datums\components\drift.dm"
#include "code\datums\components\earprotection.dm"
#include "code\datums\components\echolocation.dm"
@@ -1166,6 +1169,7 @@
#include "code\datums\components\knockoff.dm"
#include "code\datums\components\label.dm"
#include "code\datums\components\leash.dm"
+#include "code\datums\components\life_link.dm"
#include "code\datums\components\light_eater.dm"
#include "code\datums\components\ling_decoy_brain.dm"
#include "code\datums\components\lock_on_cursor.dm"
@@ -4553,6 +4557,22 @@
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_accessories.dm"
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_ai.dm"
#include "code\modules\mob\living\basic\farm_animals\gorilla\gorilla_emotes.dm"
+#include "code\modules\mob\living\basic\guardian\guardian.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_creator.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_fluff.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_helpers.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_verbs.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\assassin.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\charger.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\dextrous.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\explosive.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\gaseous.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\gravitokinetic.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\lightning.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\protector.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\ranged.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\standard.dm"
+#include "code\modules\mob\living\basic\guardian\guardian_types\support.dm"
#include "code\modules\mob\living\basic\heretic\_heretic_summon.dm"
#include "code\modules\mob\living\basic\heretic\ash_spirit.dm"
#include "code\modules\mob\living\basic\heretic\fire_shark.dm"
@@ -4898,19 +4918,6 @@
#include "code\modules\mob\living\simple_animal\friendly\cat.dm"
#include "code\modules\mob\living\simple_animal\friendly\gondola.dm"
#include "code\modules\mob\living\simple_animal\friendly\pet.dm"
-#include "code\modules\mob\living\simple_animal\guardian\guardian.dm"
-#include "code\modules\mob\living\simple_animal\guardian\guardian_creator.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\assassin.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\charger.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\dextrous.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\explosive.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\gaseous.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\gravitokinetic.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\lightning.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\protector.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\ranged.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\standard.dm"
-#include "code\modules\mob\living\simple_animal\guardian\types\support.dm"
#include "code\modules\mob\living\simple_animal\hostile\alien.dm"
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
index 9db6c85e7a8..e3f4ecd2feb 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -506,6 +506,11 @@ em {
font-size: 90%;
}
+.bolditalic {
+ font-style: italic;
+ font-weight: bold;
+}
+
.boldnotice {
color: #6685f5;
font-weight: bold;
diff --git a/tools/UpdatePaths/Scripts/79473_guardian_creator_repath.txt b/tools/UpdatePaths/Scripts/79473_guardian_creator_repath.txt
new file mode 100644
index 00000000000..92f9e379d9a
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/79473_guardian_creator_repath.txt
@@ -0,0 +1,5 @@
+/obj/item/guardiancreator/choose/wizard : /obj/item/guardian_creator/wizard{@OLD}
+/obj/item/guardiancreator/tech/@SUBTYPES : /obj/item/guardian_creator/tech/@SUBTYPES{@OLD}
+/obj/item/guardiancreator/carp/@SUBTYPES : /obj/item/guardian_creator/carp/@SUBTYPES{@OLD}
+/obj/item/guardian_creator/miner/@SUBTYPES : /obj/item/guardian_creator/miner/@SUBTYPES{@OLD}
+/obj/item/guardiancreator/@SUBTYPES : /obj/item/guardian_creator/@SUBTYPES{@OLD}