diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm
index 443a51a7eb7..b1f5d4fac30 100644
--- a/code/__DEFINES/ai/ai_blackboard.dm
+++ b/code/__DEFINES/ai/ai_blackboard.dm
@@ -9,6 +9,9 @@
///Path we should use next time we use the JPS movement datum
#define BB_PATH_TO_USE "BB_path_to_use"
+///Turf we want a mob to move to
+#define BB_TRAVEL_DESTINATION "BB_travel_destination"
+
///song instrument blackboard, set by instrument subtrees
#define BB_SONG_INSTRUMENT "BB_SONG_INSTRUMENT"
///song lines blackboard, set by default on controllers
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
index d7063b0df5f..0c9523b9c20 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movement.dm
@@ -17,6 +17,9 @@
#define COMSIG_ATOM_AFTER_SHUTTLE_MOVE "movable_after_shuttle_move"
///called on a movable (NOT living) when it starts pulling (atom/movable/pulled, state, force)
#define COMSIG_ATOM_START_PULL "movable_start_pull"
+/// called on /atom when something attempts to pass through it (atom/movable/source, atom/movable/passing, dir)
+#define COMSIG_ATOM_TRIED_PASS "atom_tried_pass"
+ #define COMSIG_COMPONENT_PERMIT_PASSAGE (1 << 0)
///called on /living when someone starts pulling (atom/movable/pulled, state, force)
#define COMSIG_LIVING_START_PULL "living_start_pull"
///called on /living when someone is pulled (mob/living/puller)
diff --git a/code/__DEFINES/dcs/signals/signals_blob.dm b/code/__DEFINES/dcs/signals/signals_blob.dm
new file mode 100644
index 00000000000..afd4737bdd9
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_blob.dm
@@ -0,0 +1,4 @@
+/// Signal sent when a blob overmind picked a new strain (/mob/camera/blob/overmind, /datum/blobstrain/new_strain)
+#define COMSIG_BLOB_SELECTED_STRAIN "blob_selected_strain"
+/// Signal sent by a blob spore when it creates a zombie (/mob/living/basic/blob_minion/spore/spore, //mob/living/basic/blob_minion/zombie/zombie)
+#define COMSIG_BLOB_ZOMBIFIED "blob_zombified"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index d4c3c659c68..7dffe24331a 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -7,6 +7,8 @@
#define COMSIG_MOB_LOGIN "mob_login"
///from base of /mob/Logout(): ()
#define COMSIG_MOB_LOGOUT "mob_logout"
+///from base of /mob/mind_initialize
+#define COMSIG_MOB_MIND_INITIALIZED "mob_mind_inited"
///from base of mob/set_stat(): (new_stat, old_stat)
#define COMSIG_MOB_STATCHANGE "mob_statchange"
///from base of mob/reagent_check(): (datum/reagent/chem, seconds_per_tick, times_fired)
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index a52d2d809e6..e909c23bb90 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -278,8 +278,6 @@ GLOBAL_LIST_INIT(glass_sheet_types, typecacheof(list(
#define isholoeffect(O) (istype(O, /obj/effect/holodeck_effect))
-#define isblobmonster(O) (istype(O, /mob/living/simple_animal/hostile/blob))
-
#define isshuttleturf(T) (!isnull(T.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle)))
#define isProbablyWallMounted(O) (O.pixel_x > 20 || O.pixel_x < -20 || O.pixel_y > 20 || O.pixel_y < -20)
diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm
index 22cf26e19a5..7b23869988b 100644
--- a/code/__DEFINES/span.dm
+++ b/code/__DEFINES/span.dm
@@ -17,6 +17,7 @@
#define span_bigicon(str) ("" + str + "")
#define span_binarysay(str) ("" + str + "")
#define span_blob(str) ("" + str + "")
+#define span_blobannounce(str) ("" + str + "")
#define span_blue(str) ("" + str + "")
#define span_blueteamradio(str) ("" + str + "")
#define span_bold(str) ("" + str + "")
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index cbd60fea4c9..9be450e381e 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -1144,6 +1144,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define AUTOPSY_TRAIT "autopsy_trait"
/// Trait given by [/datum/status_effect/blessing_of_insanity]
#define MAD_WIZARD_TRAIT "mad_wizard_trait"
+/// Isn't attacked harmfully by blob structures
+#define TRAIT_BLOB_ALLY "blob_ally"
/**
* Trait granted by [/mob/living/carbon/Initialize] and
diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
index e853547ec73..31f9ad271d3 100644
--- a/code/__HELPERS/chat.dm
+++ b/code/__HELPERS/chat.dm
@@ -81,3 +81,19 @@ it will be sent to all connected chats.
/// Handles text formatting for item use hints in examine text
#define EXAMINE_HINT(text) ("" + text + "")
+
+/// Sends a message to all dead and observing players, if a source is provided a follow link will be attached.
+/proc/send_to_observers(message, source)
+ var/list/all_observers = GLOB.dead_player_list + GLOB.current_observers_list
+ for(var/mob/observer as anything in all_observers)
+ if (isnull(source))
+ to_chat(observer, "[message]")
+ continue
+ var/link = FOLLOW_LINK(observer, source)
+ to_chat(observer, "[link] [message]")
+
+/// Sends a message to everyone with blob telepathy, and all observers
+/proc/blob_telepathy(message, source)
+ for(var/mob/creature as anything in GLOB.blob_telepathy_mobs)
+ to_chat(creature, message)
+ send_to_observers(message, source)
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 6f7d9c987f7..146a83fa8d2 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -52,6 +52,9 @@ GLOBAL_LIST_EMPTY(current_living_antags)
/// All observers with clients that joined as observers.
GLOBAL_LIST_EMPTY(current_observers_list)
+/// All living mobs which can hear blob telepathy
+GLOBAL_LIST_EMPTY(blob_telepathy_mobs)
+
///underages who have been reported to security for trying to buy things they shouldn't, so they can't spam
GLOBAL_LIST_EMPTY(narcd_underages)
diff --git a/code/_globalvars/lists/poll_ignore.dm b/code/_globalvars/lists/poll_ignore.dm
index bbd07582e99..7d532ec3aea 100644
--- a/code/_globalvars/lists/poll_ignore.dm
+++ b/code/_globalvars/lists/poll_ignore.dm
@@ -4,6 +4,7 @@
#define POLL_IGNORE_ALIEN_LARVA "alien_larva"
#define POLL_IGNORE_ASH_SPIRIT "ash_spirit"
#define POLL_IGNORE_ASHWALKER "ashwalker"
+#define POLL_IGNORE_BLOB "blob"
#define POLL_IGNORE_BOTS "bots"
#define POLL_IGNORE_CARGORILLA "cargorilla"
#define POLL_IGNORE_CONTRACTOR_SUPPORT "contractor_support"
@@ -41,6 +42,7 @@ GLOBAL_LIST_INIT(poll_ignore_desc, list(
POLL_IGNORE_ALIEN_LARVA = "Xenomorph larva",
POLL_IGNORE_ASH_SPIRIT = "Ash Spirit",
POLL_IGNORE_ASHWALKER = "Ashwalker eggs",
+ POLL_IGNORE_BLOB = "Blob spores",
POLL_IGNORE_BOTS = "Bots",
POLL_IGNORE_CARGORILLA = "Cargorilla",
POLL_IGNORE_CONTRACTOR_SUPPORT = "Contractor Support Unit",
diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm
index 7067b3abbfc..6ff9d39d531 100644
--- a/code/controllers/subsystem/movement/movement_types.dm
+++ b/code/controllers/subsystem/movement/movement_types.dm
@@ -384,7 +384,7 @@
src.simulated_only = simulated_only
src.avoid = avoid
src.skip_first = skip_first
- movement_path = initial_path.Copy()
+ movement_path = initial_path?.Copy()
if(isidcard(id))
RegisterSignal(id, COMSIG_QDELETING, PROC_REF(handle_no_id)) //I prefer erroring to harddels. If this breaks anything consider making id info into a datum or something
diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
index bbc1a43e322..55f6ef4c4c0 100644
--- a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
+++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm
@@ -6,6 +6,8 @@
/datum/ai_behavior/travel_towards
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+ /// If true we will get rid of our target on completion
+ var/clear_target = FALSE
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
. = ..()
@@ -16,7 +18,15 @@
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
. = ..()
- finish_action(controller, TRUE)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
+ . = ..()
+ if (clear_target)
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_behavior/travel_towards/stop_on_arrival
+ clear_target = TRUE
/**
* # Travel Towards Atom
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
new file mode 100644
index 00000000000..9ce7cc95c07
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/travel_to_point.dm
@@ -0,0 +1,18 @@
+/// Simply walk to a location
+/datum/ai_planning_subtree/travel_to_point
+ /// Blackboard key where we travel a place we walk to
+ var/location_key = BB_TRAVEL_DESTINATION
+ /// What do we do in order to travel
+ var/travel_behaviour = /datum/ai_behavior/travel_towards
+
+/datum/ai_planning_subtree/travel_to_point/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ var/atom/target = controller.blackboard[location_key]
+ if (QDELETED(target))
+ return
+ controller.queue_behavior(travel_behaviour, location_key)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+
+/datum/ai_planning_subtree/travel_to_point/and_clear_target
+ travel_behaviour = /datum/ai_behavior/travel_towards/stop_on_arrival
diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
index 036176dc85e..ba7c7f2ffba 100644
--- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm
+++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
@@ -93,7 +93,7 @@
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
- if(isnull(hunted))
+ if(QDELETED(hunted))
//Target is gone for some reason. forget about this task!
controller[hunting_target_key] = null
finish_action(controller, FALSE, hunting_target_key)
diff --git a/code/datums/ai/hunting_behavior/hunting_corpses.dm b/code/datums/ai/hunting_behavior/hunting_corpses.dm
new file mode 100644
index 00000000000..e720e4da947
--- /dev/null
+++ b/code/datums/ai/hunting_behavior/hunting_corpses.dm
@@ -0,0 +1,17 @@
+/// Find and attack corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target
+ hunt_targets = list(/mob/living)
+
+/// Find nearby dead mobs
+/datum/ai_behavior/find_hunt_target/corpses
+
+/datum/ai_behavior/find_hunt_target/corpses/valid_dinner(mob/living/source, mob/living/dinner, radius)
+ if (!isliving(dinner) || dinner.stat != DEAD)
+ return FALSE
+ return can_see(source, dinner, radius)
+
+/// Find and attack specifically human corpses
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/human
+ hunt_targets = list(/mob/living/carbon/human)
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
index 3523da7ecec..da46735ec36 100644
--- a/code/datums/ai/movement/ai_movement_jps.dm
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -2,7 +2,7 @@
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
- max_pathing_attempts = 4
+ max_pathing_attempts = 20
/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
. = ..()
@@ -12,13 +12,13 @@
var/datum/move_loop/loop = SSmove_manager.jps_move(moving,
current_movement_target,
delay,
- repath_delay = 2 SECONDS,
+ repath_delay = 0.5 SECONDS,
max_path_length = AI_MAX_PATH_LENGTH,
minimum_distance = controller.get_minimum_distance(),
id = controller.get_access(),
subsystem = SSai_movement,
extra_info = controller,
- initial_path = controller.blackboard[BB_PATH_TO_USE])
+ )
RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
new file mode 100644
index 00000000000..41f58231e2d
--- /dev/null
+++ b/code/datums/components/blob_minion.dm
@@ -0,0 +1,154 @@
+/**
+ * Common behaviour shared by things which are minions to a blob
+ */
+/datum/component/blob_minion
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Overmind who is our boss
+ var/mob/camera/blob/overmind
+ /// Callback to run if overmind strain changes
+ var/datum/callback/on_strain_changed
+
+/datum/component/blob_minion/Initialize(mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ . = ..()
+ if (!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/InheritComponent(datum/component/new_comp, i_am_original, mob/camera/blob/overmind, datum/callback/on_strain_changed)
+ if (!isnull(on_strain_changed))
+ src.on_strain_changed = on_strain_changed
+ register_overlord(overmind)
+
+/datum/component/blob_minion/proc/register_overlord(mob/camera/blob/overmind)
+ if (isnull(overmind))
+ return
+ src.overmind = overmind
+ overmind.register_new_minion(parent)
+ RegisterSignal(overmind, COMSIG_QDELETING, PROC_REF(overmind_deleted))
+ RegisterSignal(overmind, COMSIG_BLOB_SELECTED_STRAIN, PROC_REF(overmind_properties_changed))
+ overmind_properties_changed(overmind, overmind.blobstrain)
+
+/// Our overmind is gone, uh oh!
+/datum/component/blob_minion/proc/overmind_deleted()
+ SIGNAL_HANDLER
+ overmind = null
+ overmind_properties_changed()
+
+/// Our overmind has changed colour and properties
+/datum/component/blob_minion/proc/overmind_properties_changed(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ SIGNAL_HANDLER
+ var/mob/living/living_parent = parent
+ living_parent.update_appearance(UPDATE_ICON)
+ on_strain_changed?.Invoke(overmind, new_strain)
+
+/datum/component/blob_minion/RegisterWithParent()
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags |= PASSBLOB
+ living_parent.faction |= ROLE_BLOB
+ ADD_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ remove_verb(parent, /mob/living/verb/pulled) // No dragging people into the blob
+ RegisterSignal(parent, COMSIG_MOB_MIND_INITIALIZED, PROC_REF(on_mind_init))
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON, PROC_REF(on_update_appearance))
+ RegisterSignal(parent, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(on_update_status_tab))
+ RegisterSignal(parent, COMSIG_ATOM_BLOB_ACT, PROC_REF(on_blob_touched))
+ RegisterSignal(parent, COMSIG_ATOM_FIRE_ACT, PROC_REF(on_burned))
+ RegisterSignal(parent, COMSIG_ATOM_TRIED_PASS, PROC_REF(on_attempted_pass))
+ RegisterSignal(parent, COMSIG_MOVABLE_SPACEMOVE, PROC_REF(on_space_move))
+ RegisterSignal(parent, COMSIG_LIVING_TRY_SPEECH, PROC_REF(on_try_speech))
+ RegisterSignal(parent, COMSIG_MOB_CHANGED_TYPE, PROC_REF(on_transformed))
+ living_parent.update_appearance(UPDATE_ICON)
+ GLOB.blob_telepathy_mobs |= parent
+
+/datum/component/blob_minion/UnregisterFromParent()
+ if (!isnull(overmind))
+ overmind.blob_mobs -= parent
+ var/mob/living/living_parent = parent
+ living_parent.pass_flags &= ~PASSBLOB
+ living_parent.faction -= ROLE_BLOB
+ REMOVE_TRAIT(parent, TRAIT_BLOB_ALLY, REF(src))
+ add_verb(parent, /mob/living/verb/pulled)
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_BLOB_ACT,
+ COMSIG_ATOM_FIRE_ACT,
+ COMSIG_ATOM_TRIED_PASS,
+ COMSIG_ATOM_UPDATE_ICON,
+ COMSIG_LIVING_TRY_SPEECH,
+ COMSIG_MOB_CHANGED_TYPE,
+ COMSIG_MOB_GET_STATUS_TAB_ITEMS,
+ COMSIG_MOB_MIND_INITIALIZED,
+ COMSIG_MOVABLE_SPACEMOVE,
+ ))
+ GLOB.blob_telepathy_mobs -= parent
+
+/// Become blobpilled when we gain a mind
+/datum/component/blob_minion/proc/on_mind_init(mob/living/minion, datum/mind/new_mind)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ var/datum/antagonist/blob_minion/minion_motive = new(overmind)
+ new_mind.add_antag_datum(minion_motive)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_appearance(mob/living/minion)
+ SIGNAL_HANDLER
+ if(isnull(overmind))
+ minion.remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ return
+ minion.add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
+
+/// When our icon is updated, update our colour too
+/datum/component/blob_minion/proc/on_update_status_tab(mob/living/minion, list/status_items)
+ SIGNAL_HANDLER
+ if (isnull(overmind))
+ return
+ status_items += "Blobs to Win: [length(overmind.blobs_legit)]/[overmind.blobwincount]"
+
+/// If we feel the gentle caress of a blob, we feel better
+/datum/component/blob_minion/proc/on_blob_touched(mob/living/minion)
+ SIGNAL_HANDLER
+ if(minion.stat == DEAD || minion.health >= minion.maxHealth)
+ return COMPONENT_CANCEL_BLOB_ACT // Don't hurt us in order to heal us
+ for(var/i in 1 to 2)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(parent)) // hello yes you are being healed
+ heal_effect.color = isnull(overmind) ? COLOR_BLACK : overmind.blobstrain.complementary_color
+ minion.heal_overall_damage(minion.maxHealth * BLOBMOB_HEALING_MULTIPLIER)
+ return COMPONENT_CANCEL_BLOB_ACT
+
+/// If we feel the fearsome bite of open flame, we feel worse
+/datum/component/blob_minion/proc/on_burned(mob/living/minion, exposed_temperature, exposed_volume)
+ SIGNAL_HANDLER
+ if(isnull(exposed_temperature))
+ minion.adjustFireLoss(5)
+ return
+ minion.adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
+
+/// Someone is attempting to move through us, allow it if it is a blob tile
+/datum/component/blob_minion/proc/on_attempted_pass(mob/living/minion, atom/movable/incoming)
+ SIGNAL_HANDLER
+ if(istype(incoming, /obj/structure/blob))
+ return COMSIG_COMPONENT_PERMIT_PASSAGE
+
+/// If we're near a blob, stop drifting
+/datum/component/blob_minion/proc/on_space_move(mob/living/minion)
+ SIGNAL_HANDLER
+ var/obj/structure/blob/blob_handhold = locate() in range(1, parent)
+ if (!isnull(blob_handhold))
+ return COMSIG_MOVABLE_STOP_SPACEMOVE
+
+/// We only speak telepathically to blobs
+/datum/component/blob_minion/proc/on_try_speech(mob/living/minion, message, ignore_spam, forced)
+ SIGNAL_HANDLER
+ var/spanned_message = minion.say_quote(message)
+ var/rendered = span_blob("\[Blob Telepathy\] [minion.real_name] [spanned_message]")
+ blob_telepathy(rendered, minion)
+ return COMPONENT_CANNOT_SPEAK
+
+/// Called when a blob minion is transformed into something else, hopefully a spore into a zombie
+/datum/component/blob_minion/proc/on_transformed(mob/living/minion, mob/living/replacement)
+ SIGNAL_HANDLER
+ overmind?.assume_direct_control(replacement)
+
+/datum/component/blob_minion/PostTransfer()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm
index 35d1a6683f3..66871aac2ec 100644
--- a/code/datums/memory/_memory.dm
+++ b/code/datums/memory/_memory.dm
@@ -246,6 +246,7 @@
var/static/list/something_pool = list(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -272,7 +273,6 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
/mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
/mob/living/simple_animal/hostile/retaliate/goat,
diff --git a/code/datums/mind/initialization.dm b/code/datums/mind/initialization.dm
index 12a5dddb229..eb622cc5af5 100644
--- a/code/datums/mind/initialization.dm
+++ b/code/datums/mind/initialization.dm
@@ -11,6 +11,7 @@
mind.set_current(src)
// There's nowhere else to set this up, mind code makes me depressed
mind.antag_hud = add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/antagonist_hud, "combo_hud", mind)
+ SEND_SIGNAL(src, COMSIG_MOB_MIND_INITIALIZED, mind)
/mob/living/carbon/mind_initialize()
..()
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 798d50c38f7..93edc15ac01 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -318,6 +318,8 @@
/atom/proc/CanPass(atom/movable/mover, border_dir)
SHOULD_CALL_PARENT(TRUE)
SHOULD_BE_PURE(TRUE)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_TRIED_PASS, mover, border_dir) & COMSIG_COMPONENT_PERMIT_PASSAGE)
+ return TRUE
if(mover.movement_type & PHASING)
return TRUE
. = CanAllowThrough(mover, border_dir)
diff --git a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
index dae527c3e6b..2d524e19042 100644
--- a/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -394,6 +394,15 @@
reagents.expose(smoker, INGEST, fraction)
return TRUE
+/// Helper to quickly create a cloud of reagent smoke
+/proc/do_chem_smoke(range = 0, amount = DIAMOND_AREA(range), atom/holder = null, location = null, reagent_type = /datum/reagent/water, reagent_volume = 10, log = FALSE)
+ var/datum/reagents/smoke_reagents = new/datum/reagents(reagent_volume)
+ smoke_reagents.add_reagent(reagent_type, reagent_volume)
+
+ var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
+ smoke.attach(location)
+ smoke.set_up(amount = amount, holder = holder, location = location, carry = smoke_reagents, silent = TRUE)
+ smoke.start(log = log)
/// A factory which produces clouds of chemical bearing smoke.
/datum/effect_system/fluid_spread/smoke/chem
diff --git a/code/modules/antagonists/blob/blob_minion.dm b/code/modules/antagonists/blob/blob_minion.dm
index 9ba8e7089b9..9bf37e961d5 100644
--- a/code/modules/antagonists/blob/blob_minion.dm
+++ b/code/modules/antagonists/blob/blob_minion.dm
@@ -34,9 +34,3 @@
objective.owner = owner
objective.overmind = overmind
objectives += objective
-
-/datum/antagonist/blob_minion/blobbernaut
- name = "\improper Blobbernaut"
-
-/datum/antagonist/blob_minion/blob_zombie
- name = "\improper Blob Zombie"
diff --git a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
index 6225eec9da9..6f01eb67aa7 100644
--- a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
+++ b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
@@ -96,12 +96,12 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
B.modify_max_integrity(B.max_integrity * max_structure_health_multiplier)
B.update_appearance()
- for(var/mob/living/simple_animal/hostile/blob/BM as anything in overmind.blob_mobs)
- BM.maxHealth *= max_mob_health_multiplier
- BM.health *= max_mob_health_multiplier
- BM.update_icons() //If it's getting a new strain, tell it what it does!
- to_chat(BM, "Your overmind's blob strain is now: [name]!")
- to_chat(BM, "The [name] strain [shortdesc ? "[shortdesc]" : "[description]"]")
+ for(var/mob/living/blob_mob as anything in overmind.blob_mobs)
+ blob_mob.maxHealth *= max_mob_health_multiplier
+ blob_mob.health *= max_mob_health_multiplier
+ blob_mob.update_icons() //If it's getting a new strain, tell it what it does!
+ to_chat(blob_mob, "Your overmind's blob strain is now: [name]!")
+ to_chat(blob_mob, "The [name] strain [shortdesc ? "[shortdesc]" : "[description]"]")
/datum/blobstrain/proc/on_lose()
if(overmind.blob_core)
@@ -122,9 +122,9 @@ GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/
for(var/obj/structure/blob/B as anything in overmind.all_blobs)
B.modify_max_integrity(B.max_integrity / max_structure_health_multiplier)
- for(var/mob/living/simple_animal/hostile/blob/BM as anything in overmind.blob_mobs)
- BM.maxHealth /= max_mob_health_multiplier
- BM.health /= max_mob_health_multiplier
+ for(var/mob/living/blob_mob as anything in overmind.blob_mobs)
+ blob_mob.maxHealth /= max_mob_health_multiplier
+ blob_mob.health /= max_mob_health_multiplier
/datum/blobstrain/proc/on_sporedeath(mob/living/spore)
diff --git a/code/modules/antagonists/blob/blobstrains/_reagent.dm b/code/modules/antagonists/blob/blobstrains/_reagent.dm
index aefd6c02f3c..05bc73f95c8 100644
--- a/code/modules/antagonists/blob/blobstrains/_reagent.dm
+++ b/code/modules/antagonists/blob/blobstrains/_reagent.dm
@@ -15,8 +15,9 @@
var/mob_protection = L.getarmor(null, BIO) * 0.01
reagent.expose_mob(L, VAPOR, BLOBMOB_BLOBBERNAUT_REAGENTATK_VOL+blobbernaut_reagentatk_bonus, FALSE, mob_protection, overmind)//this will do between 10 and 20 damage(reduced by mob protection), depending on chemical, plus 4 from base brute damage.
-/datum/blobstrain/reagent/on_sporedeath(mob/living/spore)
- spore.reagents.add_reagent(reagent.type, 10)
+/datum/blobstrain/reagent/on_sporedeath(mob/living/basic/spore)
+ var/burst_range = (spore.type == /mob/living/basic/blob_minion/spore) ? 1 : 0
+ do_chem_smoke(range = burst_range, holder = spore, location = get_turf(spore), reagent_type = reagent.type)
// These can only be applied by blobs. They are what (reagent) blobs are made out of.
/datum/reagent/blob
@@ -29,7 +30,7 @@
/// Used by blob reagents to calculate the reaction volume they should use when exposing mobs.
/datum/reagent/blob/proc/return_mob_expose_reac_volume(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
- if(exposed_mob.stat == DEAD || isblobmonster(exposed_mob))
+ if(exposed_mob.stat == DEAD || HAS_TRAIT(exposed_mob, TRAIT_BLOB_ALLY))
return 0 //the dead, and blob mobs, don't cause reactions
return round(reac_volume * min(1.5 - touch_protection, 1), 0.1) //full touch protection means 50% volume, any prot below 0.5 means 100% volume.
diff --git a/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
index f97599948a1..ea2bf54d769 100644
--- a/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
+++ b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
@@ -11,18 +11,16 @@
message_living = ", and you feel tired"
reagent = /datum/reagent/blob/distributed_neurons
-/datum/blobstrain/reagent/distributed_neurons/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
- if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && damage <= 20 && B.get_integrity() - damage <= 0 && prob(15)) //if the cause isn't fire or a bomb, the damage is less than 21, we're going to die from that damage, 15% chance of a shitty spore.
- B.visible_message(span_warning("A spore floats free of the blob!"))
- var/mob/living/simple_animal/hostile/blob/blobspore/weak/BS = new/mob/living/simple_animal/hostile/blob/blobspore/weak(B.loc)
- BS.overmind = B.overmind
- BS.update_icons()
- B.overmind.blob_mobs.Add(BS)
+/datum/blobstrain/reagent/distributed_neurons/damage_reaction(obj/structure/blob/blob_tile, damage, damage_type, damage_flag)
+ if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && damage <= 20 && blob_tile.get_integrity() - damage <= 0 && prob(15)) //if the cause isn't fire or a bomb, the damage is less than 21, we're going to die from that damage, 15% chance of a shitty spore.
+ blob_tile.visible_message(span_boldwarning("A spore floats free of the blob!"))
+ blob_tile.overmind.create_spore(blob_tile.loc, /mob/living/basic/blob_minion/spore/minion/weak)
return ..()
/datum/reagent/blob/distributed_neurons
name = "Distributed Neurons"
color = "#E88D5D"
+ taste_description = "fizzing"
/datum/reagent/blob/distributed_neurons/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
. = ..()
@@ -33,10 +31,7 @@
exposed_mob.investigate_log("has been killed by distributed neurons (blob).", INVESTIGATE_DEATHS)
exposed_mob.death() //sleeping in a fight? bad plan.
if(exposed_mob.stat == DEAD && overmind.can_buy(5))
- var/mob/living/simple_animal/hostile/blob/blobspore/spore = new/mob/living/simple_animal/hostile/blob/blobspore(get_turf(exposed_mob))
- spore.overmind = overmind
- spore.update_icons()
- overmind.blob_mobs.Add(spore)
+ var/mob/living/basic/blob_minion/spore/minion/spore = overmind.create_spore(get_turf(exposed_mob))
spore.zombify(exposed_mob)
overmind.add_points(-5)
to_chat(overmind, span_notice("Spent 5 resources for the zombification of [exposed_mob]."))
diff --git a/code/modules/antagonists/blob/blobstrains/reactive_spines.dm b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
index 53f336ca419..1c8cb893df8 100644
--- a/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
+++ b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
@@ -16,15 +16,15 @@
if(damage && ((damage_type == BRUTE) || (damage_type == BURN)) && B.get_integrity() - damage > 0 && COOLDOWN_FINISHED(src, retaliate_cooldown)) // Is there any damage, is it burn or brute, will we be alive, and has the cooldown finished?
COOLDOWN_START(src, retaliate_cooldown, 2.5 SECONDS) // 2.5 seconds before auto-retaliate can whack everything within 1 tile again
B.visible_message(span_boldwarning("The blob retaliates, lashing out!"))
- for(var/atom/A in range(1, B))
- if(!A.can_blob_attack())
+ for(var/atom/thing in range(1, B))
+ if(!thing.can_blob_attack())
continue
- var/attacked_turf = get_turf(A)
- if(isliving(A) && !isblobmonster(A)) // Make sure to inject strain-reagents with automatic attacks when needed.
+ var/attacked_turf = get_turf(thing)
+ if(isliving(thing) && !HAS_TRAIT(thing, TRAIT_BLOB_ALLY)) // Make sure to inject strain-reagents with automatic attacks when needed.
B.blob_attack_animation(attacked_turf, overmind)
- attack_living(A)
+ attack_living(thing)
- else if(A.blob_act(B)) // After checking for mobs, whack everything else with the standard attack
+ else if(thing.blob_act(B)) // After checking for mobs, whack everything else with the standard attack
B.blob_attack_animation(attacked_turf, overmind) // Only play the animation if the attack did something meaningful
return ..()
@@ -35,7 +35,7 @@
color = "#9ACD32"
/datum/reagent/blob/reactive_spines/return_mob_expose_reac_volume(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
- if(exposed_mob.stat == DEAD || isblobmonster(exposed_mob))
+ if(exposed_mob.stat == DEAD || HAS_TRAIT(exposed_mob, TRAIT_BLOB_ALLY))
return 0 //the dead, and blob mobs, don't cause reactions
return reac_volume
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
index 70a1e69e1e3..38e88817de2 100644
--- a/code/modules/antagonists/blob/overmind.dm
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -54,6 +54,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
var/list/strain_choices
/mob/camera/blob/Initialize(mapload, starting_points = OVERMIND_STARTING_POINTS)
+ ADD_TRAIT(src, TRAIT_BLOB_ALLY, INNATE_TRAIT)
validate_location()
blob_points = starting_points
manualplace_min_time += world.time
@@ -71,6 +72,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
SSshuttle.registerHostileEnvironment(src)
. = ..()
START_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs |= src
/mob/camera/blob/proc/validate_location()
var/turf/T = get_turf(src)
@@ -112,6 +114,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
to_chat(src, span_notice("The [blobstrain.name] strain [blobstrain.description]"))
if(blobstrain.effectdesc)
to_chat(src, span_notice("The [blobstrain.name] strain [blobstrain.effectdesc]"))
+ SEND_SIGNAL(src, COMSIG_BLOB_SELECTED_STRAIN, blobstrain)
/mob/camera/blob/can_z_move(direction, turf/start, turf/destination, z_move_flags = NONE, mob/living/rider)
if(placed) // The blob can't expand vertically (yet)
@@ -162,50 +165,70 @@ GLOBAL_LIST_EMPTY(blob_nodes)
priority_announce("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", ANNOUNCER_OUTBREAK5)
has_announced = TRUE
+/// Create a blob spore and link it to us
+/mob/camera/blob/proc/create_spore(turf/spore_turf, spore_type = /mob/living/basic/blob_minion/spore/minion)
+ var/mob/living/basic/blob_minion/spore/spore = new spore_type(spore_turf)
+ assume_direct_control(spore)
+ return spore
+
+/// Give our new minion the properties of a minion
+/mob/camera/blob/proc/assume_direct_control(mob/living/minion)
+ minion.AddComponent(/datum/component/blob_minion, src)
+
+/// Add something to our list of mobs and wait for it to die
+/mob/camera/blob/proc/register_new_minion(mob/living/minion)
+ blob_mobs |= minion
+ if (!istype(minion, /mob/living/basic/blob_minion/blobbernaut))
+ RegisterSignal(minion, COMSIG_LIVING_DEATH, PROC_REF(on_minion_death))
+
+/// When a spore (or zombie) dies then we do this
+/mob/camera/blob/proc/on_minion_death(mob/living/spore)
+ SIGNAL_HANDLER
+ blobstrain.on_sporedeath(spore)
+
/mob/camera/blob/proc/victory()
sound_to_playing_players('sound/machines/alarm.ogg')
sleep(10 SECONDS)
- for(var/i in GLOB.mob_living_list)
- var/mob/living/L = i
- var/turf/T = get_turf(L)
- if(!T || !is_station_level(T.z))
+ for(var/mob/living/live_guy as anything in GLOB.mob_living_list)
+ var/turf/guy_turf = get_turf(live_guy)
+ if(isnull(guy_turf) || !is_station_level(guy_turf.z))
continue
- if(L in GLOB.overminds || (L.pass_flags & PASSBLOB))
+ if(live_guy in GLOB.overminds || (live_guy.pass_flags & PASSBLOB))
continue
- var/area/Ablob = get_area(T)
-
- if(!(Ablob.area_flags & BLOBS_ALLOWED))
+ var/area/blob_area = get_area(guy_turf)
+ if(!(blob_area.area_flags & BLOBS_ALLOWED))
continue
- if(!(ROLE_BLOB in L.faction))
- playsound(L, 'sound/effects/splat.ogg', 50, TRUE)
- if(L.stat != DEAD)
- L.investigate_log("has died from blob takeover.", INVESTIGATE_DEATHS)
- L.death()
- new/mob/living/simple_animal/hostile/blob/blobspore(T)
+ if(!(ROLE_BLOB in live_guy.faction))
+ playsound(live_guy, 'sound/effects/splat.ogg', 50, TRUE)
+ if(live_guy.stat != DEAD)
+ live_guy.investigate_log("has died from blob takeover.", INVESTIGATE_DEATHS)
+ live_guy.death()
+ create_spore(guy_turf)
else
- L.fully_heal()
+ live_guy.fully_heal()
- for(var/area/A in GLOB.areas)
- if(!(A.type in GLOB.the_station_areas))
+ for(var/area/check_area in GLOB.areas)
+ if(!is_type_in_list(check_area, GLOB.the_station_areas))
continue
- if(!(A.area_flags & BLOBS_ALLOWED))
+ if(!(check_area.area_flags & BLOBS_ALLOWED))
continue
- A.color = blobstrain.color
- A.name = "blob"
- A.icon = 'icons/mob/nonhuman-player/blob.dmi'
- A.icon_state = "blob_shield"
- A.layer = BELOW_MOB_LAYER
- A.invisibility = 0
- A.blend_mode = 0
+ check_area.color = blobstrain.color
+ check_area.name = "blob"
+ check_area.icon = 'icons/mob/nonhuman-player/blob.dmi'
+ check_area.icon_state = "blob_shield"
+ check_area.layer = BELOW_MOB_LAYER
+ check_area.invisibility = 0
+ check_area.blend_mode = 0
+
var/datum/antagonist/blob/B = mind.has_antag_datum(/datum/antagonist/blob)
if(B)
var/datum/objective/blob_takeover/main_objective = locate() in B.objectives
if(main_objective)
main_objective.completed = TRUE
- to_chat(world, span_blob("[real_name] consumed the station in an unstoppable tide!"))
+ to_chat(world, span_blobannounce("[real_name] consumed the station in an unstoppable tide!"))
SSticker.news_report = BLOB_WIN
SSticker.force_ending = FORCE_END_ROUND
@@ -216,11 +239,6 @@ GLOBAL_LIST_EMPTY(blob_nodes)
if(B && B.overmind == src)
B.overmind = null
B.update_appearance() //reset anything that was ours
- for(var/BLO in blob_mobs)
- var/mob/living/simple_animal/hostile/blob/BM = BLO
- if(BM)
- BM.overmind = null
- BM.update_icons()
for(var/obj/structure/blob/blob_structure as anything in all_blobs)
blob_structure.overmind = null
all_blobs = null
@@ -233,6 +251,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
SSshuttle.clearHostileEnvironment(src)
STOP_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs -= src
return ..()
@@ -240,7 +259,7 @@ GLOBAL_LIST_EMPTY(blob_nodes)
. = ..()
if(!. || !client)
return FALSE
- to_chat(src, span_blob("You are the overmind!"))
+ to_chat(src, span_blobannounce("You are the overmind!"))
if(!placed && autoplace_max_time <= world.time)
to_chat(src, span_boldannounce("You will automatically place your blob core in [DisplayTimeText(autoplace_max_time - world.time)]."))
to_chat(src, span_boldannounce("You [manualplace_min_time ? "will be able to":"can"] manually place your blob core by pressing the Place Blob Core button in the bottom right corner of the screen."))
@@ -257,9 +276,11 @@ GLOBAL_LIST_EMPTY(blob_nodes)
return FALSE
var/current_health = round((blob_core.get_integrity() / blob_core.max_integrity) * 100)
hud_used.healths.maptext = MAPTEXT("
[current_health]%
")
- for(var/mob/living/simple_animal/hostile/blob/blobbernaut/blobbernaut in blob_mobs)
- if(blobbernaut.hud_used && blobbernaut.hud_used.blobpwrdisplay)
- blobbernaut.hud_used.blobpwrdisplay.maptext = MAPTEXT("[current_health]%
")
+ for(var/mob/living/basic/blob_minion/blobbernaut/blobbernaut in blob_mobs)
+ var/datum/hud/using_hud = blobbernaut.hud_used
+ if(!using_hud?.blobpwrdisplay)
+ continue
+ using_hud.blobpwrdisplay.maptext = MAPTEXT("[current_health]%
")
/mob/camera/blob/proc/add_points(points)
blob_points = clamp(blob_points + points, 0, max_blob_points)
@@ -291,14 +312,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
src.log_talk(message, LOG_SAY)
var/message_a = say_quote(message)
- var/rendered = span_big("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]")
-
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobmonster(M))
- to_chat(M, rendered)
- if(isobserver(M))
- var/link = FOLLOW_LINK(M, src)
- to_chat(M, "[link] [rendered]")
+ var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a]"))
+ blob_telepathy(rendered, src)
/mob/camera/blob/blob_act(obj/structure/blob/B)
return
@@ -324,8 +339,8 @@ GLOBAL_LIST_EMPTY(blob_nodes)
else
return FALSE
else
- var/area/A = get_area(NewLoc)
- if(isgroundlessturf(NewLoc) || istype(A, /area/shuttle)) //if unplaced, can't go on shuttles or goundless tiles
+ var/area/check_area = get_area(NewLoc)
+ if(isgroundlessturf(NewLoc) || istype(check_area, /area/shuttle)) //if unplaced, can't go on shuttles or groundless tiles
return FALSE
forceMove(NewLoc)
return TRUE
diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm
index c75a41a48ff..04054f6df85 100644
--- a/code/modules/antagonists/blob/powers.dm
+++ b/code/modules/antagonists/blob/powers.dm
@@ -196,38 +196,25 @@
var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as a [blobstrain.name] blobbernaut?", ROLE_BLOB, ROLE_BLOB, 50)
- factory.is_creating_blobbernaut = FALSE
-
if(!length(candidates))
to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later."))
add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
- factory.blobbernaut = null //players must answer rapidly
+ factory.assign_blobbernaut(null)
return FALSE
- factory.modify_max_integrity(initial(factory.max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
- factory.update_appearance()
- factory.visible_message(span_warning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
- playsound(factory.loc, 'sound/effects/splat.ogg', 50, TRUE)
-
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobber = new /mob/living/simple_animal/hostile/blob/blobbernaut(get_turf(factory))
- flick("blobbernaut_produce", blobber)
-
- factory.blobbernaut = blobber
- blobber.factory = factory
- blobber.overmind = src
- blobber.update_icons()
- blobber.adjustHealth(blobber.maxHealth * 0.5)
- blob_mobs += blobber
-
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory))
+ assume_direct_control(blobber)
+ factory.assign_blobbernaut(blobber)
var/mob/dead/observer/player = pick(candidates)
- blobber.key = player.key
+ blobber.assign_key(player.key, blobstrain)
+ RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked))
- SEND_SOUND(blobber, sound('sound/effects/blobattack.ogg'))
- SEND_SOUND(blobber, sound('sound/effects/attackblob.ogg'))
- to_chat(blobber, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
- to_chat(blobber, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
- to_chat(blobber, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
- to_chat(blobber, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+/// When one of our boys attacked something, we sometimes want to perform extra effects
+/mob/camera/blob/proc/on_blobbernaut_attacked(mob/living/basic/blobbynaut, atom/target, success)
+ SIGNAL_HANDLER
+ if (!success)
+ return
+ blobstrain.blobbernaut_attack(target, blobbynaut)
/** Moves the core */
/mob/camera/blob/proc/relocate_core()
@@ -358,10 +345,11 @@
var/list/surrounding_turfs = TURF_NEIGHBORS(tile)
if(!length(surrounding_turfs))
return FALSE
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore as anything in blob_mobs)
- if(isturf(spore.loc) && get_dist(spore, tile) <= 35 && !spore.key)
- spore.LoseTarget()
- spore.Goto(pick(surrounding_turfs), spore.move_to_delay)
+ for(var/mob/living/basic/blob_mob as anything in blob_mobs)
+ if(!isturf(blob_mob.loc) || get_dist(blob_mob, tile) > 35 || blob_mob.key)
+ continue
+ blob_mob.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
+ blob_mob.ai_controller.set_blackboard_key(BB_TRAVEL_DESTINATION, pick(surrounding_turfs))
/** Opens the reroll menu to change strains */
/mob/camera/blob/proc/strain_reroll()
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
index 543da00d2f5..e206d97c26b 100644
--- a/code/modules/antagonists/blob/structures/_blob.dm
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -129,13 +129,13 @@
return FALSE //oh no we failed
/obj/structure/blob/proc/ConsumeTile()
- for(var/atom/A in loc)
- if(!A.can_blob_attack())
+ for(var/atom/thing in loc)
+ if(!thing.can_blob_attack())
continue
- if(isliving(A) && overmind && !isblobmonster(A)) // Make sure to inject strain-reagents with automatic attacks when needed.
- overmind.blobstrain.attack_living(A)
+ if(isliving(thing) && overmind && !HAS_TRAIT(thing, TRAIT_BLOB_ALLY)) // Make sure to inject strain-reagents with automatic attacks when needed.
+ overmind.blobstrain.attack_living(thing)
continue // Don't smack them twice though
- A.blob_act(src)
+ thing.blob_act(src)
if(iswallturf(loc))
loc.blob_act(src) //don't ask how a wall got on top of the core, just eat it
diff --git a/code/modules/antagonists/blob/structures/factory.dm b/code/modules/antagonists/blob/structures/factory.dm
index 7f28dcce224..cee7e9a0ac5 100644
--- a/code/modules/antagonists/blob/structures/factory.dm
+++ b/code/modules/antagonists/blob/structures/factory.dm
@@ -10,12 +10,12 @@
armor_type = /datum/armor/structure_blob/factory
///How many spores this factory can have.
var/max_spores = BLOB_FACTORY_MAX_SPORES
- ///The list of spores
- var/list/spores = list()
+ ///The list of spores and zombies
+ var/list/spores_and_zombies = list()
COOLDOWN_DECLARE(spore_delay)
var/spore_cooldown = BLOBMOB_SPORE_SPAWN_COOLDOWN
///Its Blobbernaut, if it has spawned any.
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobbernaut
+ var/mob/living/basic/blob_minion/blobbernaut/minion/blobbernaut
///Used in blob/powers.dm, checks if it's already trying to spawn a blobbernaut to prevent issues.
var/is_creating_blobbernaut = FALSE
@@ -32,15 +32,8 @@
overmind.factory_blobs += src
/obj/structure/blob/special/factory/Destroy()
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore in spores)
- to_chat(spore, span_userdanger("Your factory was destroyed! You can no longer sustain yourself."))
- spore.death()
- spores = null
- if(blobbernaut)
- blobbernaut.factory = null
- to_chat(blobbernaut, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
- blobbernaut.throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
- blobbernaut = null
+ spores_and_zombies = null
+ blobbernaut = null
if(overmind)
overmind.factory_blobs -= src
return ..()
@@ -49,13 +42,57 @@
. = ..()
if(blobbernaut)
return
- if(spores.len >= max_spores)
+ if(length(spores_and_zombies) >= max_spores)
return
if(!COOLDOWN_FINISHED(src, spore_delay))
return
COOLDOWN_START(src, spore_delay, spore_cooldown)
- var/mob/living/simple_animal/hostile/blob/blobspore/BS = new (loc, src)
- if(overmind) //if we don't have an overmind, we don't need to do anything but make a spore
- BS.overmind = overmind
- BS.update_icons()
- overmind.blob_mobs.Add(BS)
+ var/mob/living/basic/blob_minion/created_spore = (overmind) ? overmind.create_spore(loc) : new(loc)
+ register_mob(created_spore)
+ RegisterSignal(created_spore, COMSIG_BLOB_ZOMBIFIED, PROC_REF(on_zombie_created))
+
+/// Tracks the existence of a mob in our mobs list
+/obj/structure/blob/special/factory/proc/register_mob(mob/living/basic/blob_minion/blob_mob)
+ spores_and_zombies |= blob_mob
+ blob_mob.link_to_factory(src)
+ RegisterSignal(blob_mob, COMSIG_LIVING_DEATH, PROC_REF(on_spore_died))
+ RegisterSignal(blob_mob, COMSIG_QDELETING, PROC_REF(on_spore_lost))
+
+/// When a spore or zombie dies reset our spawn cooldown so we don't instantly replace it
+/obj/structure/blob/special/factory/proc/on_spore_died(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ COOLDOWN_START(src, spore_delay, spore_cooldown)
+
+/// When a spore is deleted remove it from our list
+/obj/structure/blob/special/factory/proc/on_spore_lost(mob/living/dead_spore)
+ SIGNAL_HANDLER
+ spores_and_zombies -= dead_spore
+
+/// When a spore makes a zombie add it to our mobs list
+/obj/structure/blob/special/factory/proc/on_zombie_created(mob/living/spore, mob/living/zombie)
+ SIGNAL_HANDLER
+ register_mob(zombie)
+
+/// Produce a blobbernaut
+/obj/structure/blob/special/factory/proc/assign_blobbernaut(mob/living/new_naut)
+ is_creating_blobbernaut = FALSE
+ if (isnull(new_naut))
+ return
+
+ modify_max_integrity(initial(max_integrity) * 0.25) //factories that produced a blobbernaut have much lower health
+ visible_message(span_boldwarning("The blobbernaut [pick("rips", "tears", "shreds")] its way out of the factory blob!"))
+ playsound(loc, 'sound/effects/splat.ogg', 50, TRUE)
+
+ blobbernaut = new_naut
+ blobbernaut.link_to_factory(src)
+ RegisterSignals(new_naut, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_blobbernaut_death))
+ update_appearance(UPDATE_ICON)
+
+/// When our brave soldier dies, reset our max integrity
+/obj/structure/blob/special/factory/proc/on_blobbernaut_death(mob/living/death_naut)
+ SIGNAL_HANDLER
+ if (isnull(blobbernaut) || blobbernaut != death_naut)
+ return
+ blobbernaut = null
+ max_integrity = initial(max_integrity)
+ update_appearance(UPDATE_ICON)
diff --git a/code/modules/events/wizard/blobies.dm b/code/modules/events/wizard/blobies.dm
index 307d01ff7eb..0a9c96d5135 100644
--- a/code/modules/events/wizard/blobies.dm
+++ b/code/modules/events/wizard/blobies.dm
@@ -10,4 +10,4 @@
/datum/round_event/wizard/blobies/start()
for(var/mob/living/carbon/human/H in GLOB.dead_mob_list)
- new /mob/living/simple_animal/hostile/blob/blobspore(H.loc)
+ new /mob/living/basic/blob_minion/spore/minion(H.loc) // Creates zombies which ghosts can control
diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm
index cc128a9f5e9..8afb40f2912 100644
--- a/code/modules/mob/living/basic/basic_defense.dm
+++ b/code/modules/mob/living/basic/basic_defense.dm
@@ -165,6 +165,9 @@
return TRUE
/mob/living/basic/blob_act(obj/structure/blob/attacking_blob)
+ . = ..()
+ if (!.)
+ return
apply_damage(20, damagetype = BRUTE)
/mob/living/basic/do_attack_animation(atom/attacked_atom, visual_effect_icon, used_item, no_effect)
diff --git a/code/modules/mob/living/basic/blob_minions/blob_ai.dm b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
new file mode 100644
index 00000000000..6168b7ca83b
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_ai.dm
@@ -0,0 +1,51 @@
+/**
+ * Extremely simple AI, this isn't a very smart boy
+ * Only notable quirk is that it uses JPS movement, simple avoidance would fail to realise it can path through blobs
+ */
+/datum/ai_controller/basic_controller/blobbernaut
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * Move to a point designated by the overmind, otherwise just slap people nearby
+ */
+/datum/ai_controller/basic_controller/blob_zombie
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/**
+ * As blob zombie but will prioritise attacking corpses to zombify them
+ */
+/datum/ai_controller/basic_controller/blob_spore
+ blackboard = list(
+ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/attack_until_dead,
+ )
+
+ ai_movement = /datum/ai_movement/jps
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses,
+ /datum/ai_planning_subtree/travel_to_point/and_clear_target,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
diff --git a/code/modules/mob/living/basic/blob_minions/blob_mob.dm b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
new file mode 100644
index 00000000000..35e41f09058
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_mob.dm
@@ -0,0 +1,37 @@
+/// Root of shared behaviour for mobs spawned by blobs, is abstract and should not be spawned
+/mob/living/basic/blob_minion
+ name = "Blob Error"
+ desc = "A nonfunctional fungal creature created by bad code or celestial mistake. Point and laugh."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blob_head"
+ unique_name = TRUE
+ pass_flags = PASSBLOB
+ faction = list(ROLE_BLOB)
+ combat_mode = TRUE
+ bubble_icon = "blob"
+ speak_emote = null
+ habitable_atmos = 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)
+ minimum_survivable_temperature = 0
+ maximum_survivable_temperature = INFINITY
+ lighting_cutoff_red = 20
+ lighting_cutoff_green = 40
+ lighting_cutoff_blue = 30
+ initial_language_holder = /datum/language_holder/empty
+
+/mob/living/basic/blob_minion/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_BLOB_ALLY, TRAIT_MUTE), INNATE_TRAIT)
+ AddComponent(/datum/component/blob_minion, on_strain_changed = CALLBACK(src, PROC_REF(on_strain_updated)))
+
+/// Called when our blob overmind changes their variant, update some of our mob properties
+/mob/living/basic/blob_minion/proc/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ return
+
+/// Associates this mob with a specific blob factory node
+/mob/living/basic/blob_minion/proc/link_to_factory(obj/structure/blob/special/factory/factory)
+ RegisterSignal(factory, COMSIG_QDELETING, PROC_REF(on_factory_destroyed))
+
+/// Called when our factory is destroyed
+/mob/living/basic/blob_minion/proc/on_factory_destroyed()
+ SIGNAL_HANDLER
+ to_chat(src, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
diff --git a/code/modules/mob/living/basic/blob_minions/blob_spore.dm b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
new file mode 100644
index 00000000000..f13a6a70f84
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_spore.dm
@@ -0,0 +1,118 @@
+/**
+ * A floating fungus which turns people into zombies and explodes into reagent clouds upon death.
+ */
+/mob/living/basic/blob_minion/spore
+ name = "blob spore"
+ desc = "A floating, fragile spore."
+ icon = 'icons/mob/nonhuman-player/blob.dmi'
+ icon_state = "blobpod"
+ icon_living = "blobpod"
+ health_doll_icon = "blobpod"
+ health = BLOBMOB_SPORE_HEALTH
+ maxHealth = BLOBMOB_SPORE_HEALTH
+ verb_say = "psychically pulses"
+ verb_ask = "psychically probes"
+ verb_exclaim = "psychically yells"
+ verb_yell = "psychically screams"
+ melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
+ melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
+ obj_damage = 0
+ attack_verb_continuous = "batters"
+ attack_verb_simple = "batter"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "explodes into a cloud of gas!"
+ gold_core_spawnable = HOSTILE_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_spore
+ /// Size of cloud produced from a dying spore
+ var/death_cloud_size = 1
+ /// Type of mob to create
+ var/mob/living/zombie_type = /mob/living/basic/blob_minion/zombie
+
+/mob/living/basic/blob_minion/spore/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/spore/death(gibbed)
+ . = ..()
+ death_burst()
+
+/mob/living/basic/blob_minion/spore/on_factory_destroyed()
+ death()
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/spore/proc/death_burst()
+ do_chem_smoke(range = death_cloud_size, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+
+/mob/living/basic/blob_minion/spore/melee_attack(mob/living/carbon/human/target, list/modifiers, ignore_cooldown)
+ . = ..()
+ if (!ishuman(target) || target.stat != DEAD)
+ return
+ zombify(target)
+
+/// Become a zombie
+/mob/living/basic/blob_minion/spore/proc/zombify(mob/living/carbon/human/target)
+ visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
+ var/mob/living/basic/blob_minion/zombie/blombie = change_mob_type(zombie_type, loc, new_name = initial(zombie_type.name))
+ blombie.set_name()
+ if (istype(blombie)) // In case of badmin
+ blombie.consume_corpse(target)
+ SEND_SIGNAL(src, COMSIG_BLOB_ZOMBIFIED, blombie)
+ qdel(src)
+
+/// Variant of the blob spore which is actually spawned by blob factories
+/mob/living/basic/blob_minion/spore/minion
+ gold_core_spawnable = NO_SPAWN
+ zombie_type = /mob/living/basic/blob_minion/zombie/controlled
+ /// We die if we leave the same turf as this z level
+ var/turf/z_turf
+
+/mob/living/basic/blob_minion/spore/minion/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_changed))
+
+/// When we z-move check that we're on the same z level as our factory was
+/mob/living/basic/blob_minion/spore/minion/proc/on_z_changed()
+ SIGNAL_HANDLER
+ if (isnull(z_turf))
+ return
+ if (!is_valid_z_level(get_turf(src), z_turf))
+ death()
+
+/// Mark the turf we need to track from our factory
+/mob/living/basic/blob_minion/spore/minion/link_to_factory(obj/structure/blob/special/factory/factory)
+ . = ..()
+ z_turf = get_turf(factory)
+
+/// If the blob changes to distributed neurons then you can control the spores
+/mob/living/basic/blob_minion/spore/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (istype(new_strain, /datum/blobstrain/reagent/distributed_neurons))
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+ else
+ qdel(GetComponent(/datum/component/ghost_direct_control))
+
+/mob/living/basic/blob_minion/spore/minion/death_burst()
+ return // This behaviour is superceded by the overmind's intervention
+
+
+/// Weakened spore spawned by distributed neurons, can't zombify people and makes a teeny explosion
+/mob/living/basic/blob_minion/spore/minion/weak
+ name = "fragile blob spore"
+ health = 15
+ maxHealth = 15
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ death_cloud_size = 0
+
+/mob/living/basic/blob_minion/spore/minion/weak/zombify()
+ return
+
+/mob/living/basic/blob_minion/spore/minion/weak/on_strain_updated()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blob_zombie.dm b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
new file mode 100644
index 00000000000..6de26605baa
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blob_zombie.dm
@@ -0,0 +1,97 @@
+/// A shambling mob made out of a crew member
+/mob/living/basic/blob_minion/zombie
+ name = "blob zombie"
+ desc = "A shambling corpse animated by the blob."
+ icon_state = "zombie"
+ icon_living = "zombie"
+ health_doll_icon = "blobpod"
+ mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
+ health = 70
+ maxHealth = 70
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+ obj_damage = 20
+ attack_verb_continuous = "punches"
+ attack_verb_simple = "punch"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ death_message = "collapses to the ground!"
+ gold_core_spawnable = NO_SPAWN
+ basic_mob_flags = DEL_ON_DEATH
+ ai_controller = /datum/ai_controller/basic_controller/blob_zombie
+ /// The dead body we have inside
+ var/mob/living/carbon/human/corpse
+
+/mob/living/basic/blob_minion/zombie/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/zombie/death(gibbed)
+ corpse?.forceMove(loc)
+ death_burst()
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/Exited(atom/movable/gone, direction)
+ . = ..()
+ if (gone != corpse)
+ return
+ corpse = null
+ death()
+
+/mob/living/basic/blob_minion/zombie/Destroy()
+ QDEL_NULL(corpse)
+ return ..()
+
+/mob/living/basic/blob_minion/zombie/on_factory_destroyed()
+ . = ..()
+ death()
+
+/mob/living/basic/blob_minion/zombie/update_overlays()
+ . = ..()
+ copy_overlays(corpse, TRUE)
+ var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
+ blob_head_overlay.color = LAZYACCESS(atom_colours, FIXED_COLOUR_PRIORITY) || COLOR_WHITE
+ color = initial(color) // reversing what our component did lol, but we needed the value for the overlay
+ . += blob_head_overlay
+
+/// Create an explosion of spores on death
+/mob/living/basic/blob_minion/zombie/proc/death_burst()
+ do_chem_smoke(range = 0, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+/// Store a body so that we can drop it on death
+/mob/living/basic/blob_minion/zombie/proc/consume_corpse(mob/living/carbon/human/new_corpse)
+ if(new_corpse.wear_suit)
+ maxHealth += new_corpse.get_armor_rating(MELEE)
+ health = maxHealth
+ new_corpse.set_facial_hairstyle("Shaved", update = FALSE)
+ new_corpse.set_hairstyle("Bald", update = TRUE)
+ new_corpse.forceMove(src)
+ corpse = new_corpse
+ update_appearance(UPDATE_ICON)
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+
+/// Dynamic changeling reentry
+/mob/living/basic/blob_minion/zombie/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ visible_message(span_boldwarning("[src] bursts from the inside!"))
+ death()
+
+/// Blob-created zombies will ping for player control when they make a zombie
+/mob/living/basic/blob_minion/zombie/controlled
+
+/mob/living/basic/blob_minion/zombie/controlled/consume_corpse(mob/living/carbon/human/new_corpse)
+ . = ..()
+ if (!isnull(client))
+ return
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB_INFECTION,\
+ poll_candidates = TRUE,\
+ poll_ignore_key = POLL_IGNORE_BLOB,\
+ )
+
+/mob/living/basic/blob_minion/zombie/controlled/death_burst()
+ return
diff --git a/code/modules/mob/living/basic/blob_minions/blobbernaut.dm b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
new file mode 100644
index 00000000000..da848f829cf
--- /dev/null
+++ b/code/modules/mob/living/basic/blob_minions/blobbernaut.dm
@@ -0,0 +1,108 @@
+/**
+ * Player-piloted brute mob. Mostly just a "move and click" kind of guy.
+ * Has a variant which takes damage when away from blob tiles
+ */
+/mob/living/basic/blob_minion/blobbernaut
+ name = "blobbernaut"
+ desc = "A hulking, mobile chunk of blobmass."
+ icon_state = "blobbernaut"
+ icon_living = "blobbernaut"
+ icon_dead = "blobbernaut_dead"
+ health = BLOBMOB_BLOBBERNAUT_HEALTH
+ maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
+ damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
+ obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
+ attack_verb_continuous = "slams"
+ attack_verb_simple = "slam"
+ attack_sound = 'sound/effects/blobattack.ogg'
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ force_threshold = 10
+ pressure_resistance = 50
+ mob_size = MOB_SIZE_LARGE
+ hud_type = /datum/hud/living/blobbernaut
+ gold_core_spawnable = HOSTILE_SPAWN
+ ai_controller = /datum/ai_controller/basic_controller/blobbernaut
+
+/mob/living/basic/blob_minion/blobbernaut/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
+
+/mob/living/basic/blob_minion/blobbernaut/death(gibbed)
+ flick("blobbernaut_death", src)
+ return ..()
+
+/// This variant is the one actually spawned by blob factories, takes damage when away from blob tiles
+/mob/living/basic/blob_minion/blobbernaut/minion
+ gold_core_spawnable = NO_SPAWN
+ /// Is our factory dead?
+ var/orphaned = FALSE
+
+/mob/living/basic/blob_minion/blobbernaut/minion/Life(seconds_per_tick, times_fired)
+ . = ..()
+ if (!.)
+ return FALSE
+ var/damage_sources = 0
+ var/list/blobs_in_area = range(2, src)
+
+ if (!(locate(/obj/structure/blob) in blobs_in_area))
+ damage_sources++
+
+ if (orphaned)
+ damage_sources++
+ else
+ var/particle_colour = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_BLACK
+ if (locate(/obj/structure/blob/special/core) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (locate(/obj/structure/blob/special/node) in blobs_in_area)
+ heal_overall_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
+ var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
+ heal_effect.color = particle_colour
+
+ if (damage_sources == 0)
+ return FALSE
+
+ // take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
+ apply_damage(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damage_sources * seconds_per_tick, damagetype = TOX) // We reduce brute damage
+ var/mutable_appearance/harming = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER + 0.01)
+ harming.appearance_flags = RESET_COLOR
+ harming.color = atom_colours[FIXED_COLOUR_PRIORITY] || COLOR_WHITE
+ harming.dir = dir
+ flick_overlay_view(harming, 0.8 SECONDS)
+ return TRUE
+
+/// Called by the blob creation power to give us a mind and a basic task orientation
+/mob/living/basic/blob_minion/blobbernaut/minion/proc/assign_key(ckey, datum/blobstrain/blobstrain)
+ key = ckey
+ flick("blobbernaut_produce", src)
+ health = maxHealth / 2 // Start out injured to encourage not beelining away from the blob
+ SEND_SOUND(src, sound('sound/effects/blobattack.ogg'))
+ SEND_SOUND(src, sound('sound/effects/attackblob.ogg'))
+ to_chat(src, span_infoplain("You are powerful, hard to kill, and slowly regenerate near nodes and cores, [span_cultlarge("but will slowly die if not near the blob")] or if the factory that made you is killed."))
+ to_chat(src, span_infoplain("You can communicate with other blobbernauts and overminds telepathically by attempting to speak normally"))
+ to_chat(src, span_infoplain("Your overmind's blob reagent is: [blobstrain.name]!"))
+ to_chat(src, span_infoplain("The [blobstrain.name] reagent [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]"))
+
+/// Set our attack damage based on blob's properties
+/mob/living/basic/blob_minion/blobbernaut/minion/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ if (isnull(overmind))
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ attack_verb_continuous = initial(attack_verb_continuous)
+ return
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
+ attack_verb_continuous = new_strain.blobbernaut_message
+
+/// Called by our factory to inform us that it's not going to support us financially any more
+/mob/living/basic/blob_minion/blobbernaut/minion/on_factory_destroyed()
+ . = ..()
+ orphaned = TRUE
+ throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
diff --git a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
index 5951bd6b7fe..47280af4281 100644
--- a/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
+++ b/code/modules/mob/living/basic/icemoon/ice_whelp/ice_whelp_ai.dm
@@ -13,53 +13,33 @@
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/sculpt_statues,
- /datum/ai_planning_subtree/find_and_hunt_target/cannibalize,
+ /datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp,
/datum/ai_planning_subtree/burn_trees,
)
-
-/datum/ai_planning_subtree/find_and_hunt_target/cannibalize
+/datum/ai_planning_subtree/find_and_hunt_target/corpses/ice_whelp
target_key = BB_TARGET_CANNIBAL
- hunting_behavior = /datum/ai_behavior/cannibalize
- finding_behavior = /datum/ai_behavior/find_hunt_target/dragon_corpse
+ finding_behavior = /datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
hunt_targets = list(/mob/living/basic/mining/ice_whelp)
hunt_range = 10
-/datum/ai_behavior/find_hunt_target/dragon_corpse
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
-/datum/ai_behavior/find_hunt_target/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
- if(dinner.stat != DEAD)
- return FALSE
+/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
if(dinner.pulledby) //someone already got him before us
return FALSE
+ return ..()
- return can_see(source, dinner, radius)
-
-/datum/ai_behavior/cannibalize
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
-/datum/ai_behavior/cannibalize/setup(datum/ai_controller/controller, target_key)
- . = ..()
- var/atom/target = controller.blackboard[target_key]
- if(QDELETED(target))
- return FALSE
- set_movement_target(controller, target)
-
-/datum/ai_behavior/cannibalize/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
- . = ..()
- var/mob/living/basic/living_pawn = controller.pawn
+/datum/ai_behavior/hunt_target/unarmed_attack_target/dragon_cannibalise/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
var/mob/living/target = controller.blackboard[target_key]
-
- if(QDELETED(target))
+ if(QDELETED(target) || target.stat != DEAD || target.pulledby) //we were too slow
finish_action(controller, FALSE)
return
-
- if(target.stat != DEAD || target.pulledby) //we were too slow
- finish_action(controller, FALSE)
- return
-
- living_pawn.melee_attack(target)
- finish_action(controller, TRUE)
+ return ..()
/datum/ai_behavior/cannibalize/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index f5928e354b9..77822b82d23 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1404,6 +1404,7 @@
var/picked_animal = pick(
/mob/living/basic/bat,
/mob/living/basic/bear,
+ /mob/living/basic/blob_minion/blobbernaut,
/mob/living/basic/butterfly,
/mob/living/basic/carp,
/mob/living/basic/carp/magic,
@@ -1429,7 +1430,6 @@
/mob/living/basic/statue,
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
/mob/living/simple_animal/hostile/gorilla,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
/mob/living/simple_animal/hostile/retaliate/goat,
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index d7b929fe30a..3a9f95a14d7 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -164,6 +164,7 @@
return ..()
/mob/living/fire_act()
+ . = ..()
adjust_fire_stacks(3)
ignite_mob()
diff --git a/code/modules/mob/living/simple_animal/hostile/blob.dm b/code/modules/mob/living/simple_animal/hostile/blob.dm
deleted file mode 100644
index 7a40a01239c..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blob.dm
+++ /dev/null
@@ -1,101 +0,0 @@
-//Do not spawn
-/mob/living/simple_animal/hostile/blob
- icon = 'icons/mob/nonhuman-player/blob.dmi'
- pass_flags = PASSBLOB
- faction = list(ROLE_BLOB)
- bubble_icon = "blob"
- speak_emote = null //so we use verb_yell/verb_say/etc
- 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
- unique_name = 1
- combat_mode = TRUE
- // ... Blob colored lighting
- lighting_cutoff_red = 20
- lighting_cutoff_green = 40
- lighting_cutoff_blue = 30
- initial_language_holder = /datum/language_holder/empty
- retreat_distance = null //! retreat doesn't obey pass_flags, so won't work on blob mobs.
- /// Blob camera that controls the blob
- var/mob/camera/blob/overmind = null
- /// If this is related to anything else
- var/independent = FALSE
- /// The factory blob tile that generated this blob minion
- var/obj/structure/blob/special/factory/factory
-
-/mob/living/simple_animal/hostile/blob/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
-
-/mob/living/simple_animal/hostile/blob/Initialize(mapload)
- . = ..()
- if(!independent) //no pulling people deep into the blob
- remove_verb(src, /mob/living/verb/pulled)
- else
- pass_flags &= ~PASSBLOB
-
-/mob/living/simple_animal/hostile/blob/death()
- factory = null
- if(overmind)
- overmind.blob_mobs -= src
- overmind = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/get_status_tab_items()
- . = ..()
- if(overmind)
- . += "Blobs to Win: [overmind.blobs_legit.len]/[overmind.blobwincount]"
-
-/mob/living/simple_animal/hostile/blob/blob_act(obj/structure/blob/B)
- if(stat != DEAD && health < maxHealth)
- for(var/unused in 1 to 2)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- adjustHealth(-maxHealth*BLOBMOB_HEALING_MULTIPLIER)
-
-/mob/living/simple_animal/hostile/blob/fire_act(exposed_temperature, exposed_volume)
- ..()
- if(exposed_temperature)
- adjustFireLoss(clamp(0.01 * exposed_temperature, 1, 5))
- else
- adjustFireLoss(5)
-
-/mob/living/simple_animal/hostile/blob/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /obj/structure/blob))
- return TRUE
-
-///override to use astar/JPS instead of walk_to so we can take our blob pass_flags into account.
-/mob/living/simple_animal/hostile/blob/Goto(target, delay, minimum_distance)
- if(prevent_goto_movement)
- return FALSE
- if(target == src.target)
- approaching_target = TRUE
- else
- approaching_target = FALSE
-
- SSmove_manager.jps_move(moving = src, chasing = target, delay = delay, repath_delay = 2 SECONDS, minimum_distance = minimum_distance, simulated_only = FALSE, skip_first = TRUE, timeout = 5 SECONDS, flags = MOVEMENT_LOOP_IGNORE_GLIDE)
- return TRUE
-
-/mob/living/simple_animal/hostile/blob/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- for(var/obj/structure/blob/blob in range(1, src))
- return 1
- return ..()
-
-/mob/living/simple_animal/hostile/blob/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null, message_range = 7, datum/saymode/saymode = null)
- if(sanitize)
- message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
- var/spanned_message = say_quote(message)
- var/rendered = "\[Blob Telepathy\] [real_name] [spanned_message]"
- for(var/creature in GLOB.mob_list)
- if(isovermind(creature) || isblobmonster(creature))
- to_chat(creature, rendered)
- if(isobserver(creature))
- var/link = FOLLOW_LINK(creature, src)
- to_chat(creature, "[link] [rendered]")
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm b/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
deleted file mode 100644
index dc1d038795f..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobbernaut.dm
+++ /dev/null
@@ -1,109 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobbernaut
- name = "blobbernaut"
- desc = "A hulking, mobile chunk of blobmass."
- icon_state = "blobbernaut"
- icon_living = "blobbernaut"
- icon_dead = "blobbernaut_dead"
- health = BLOBMOB_BLOBBERNAUT_HEALTH
- maxHealth = BLOBMOB_BLOBBERNAUT_HEALTH
- damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER
- obj_damage = BLOBMOB_BLOBBERNAUT_DMG_OBJ
- attack_verb_continuous = "slams"
- attack_verb_simple = "slam"
- attack_sound = 'sound/effects/blobattack.ogg'
- verb_say = "gurgles"
- verb_ask = "demands"
- verb_exclaim = "roars"
- verb_yell = "bellows"
- force_threshold = 10
- pressure_resistance = 50
- mob_size = MOB_SIZE_LARGE
- hud_type = /datum/hud/living/blobbernaut
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Initialize(mapload)
- . = ..()
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/mind_initialize()
- . = ..()
- if(independent | !overmind)
- return
- var/datum/antagonist/blob_minion/blobbernaut/naut = new(overmind)
- mind.add_antag_datum(naut)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBBERNAUT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!..())
- return FALSE
- var/list/blobs_in_area = range(2, src)
-
- if(independent)
- return FALSE // strong independent blobbernaut that don't need no blob
-
- var/damagesources = 0
-
- if(!(locate(/obj/structure/blob) in blobs_in_area))
- damagesources++
-
- if(!factory)
- damagesources++
- else
- if(locate(/obj/structure/blob/special/core) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_CORE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
- if(locate(/obj/structure/blob/special/node) in blobs_in_area)
- adjustHealth(-maxHealth*BLOBMOB_BLOBBERNAUT_HEALING_NODE * seconds_per_tick)
- var/obj/effect/temp_visual/heal/heal_effect = new /obj/effect/temp_visual/heal(get_turf(src))
- if(overmind)
- heal_effect.color = overmind.blobstrain.complementary_color
- else
- heal_effect.color = "#000000"
-
- if(!damagesources)
- return FALSE
-
- adjustHealth(maxHealth * BLOBMOB_BLOBBERNAUT_HEALTH_DECAY * damagesources * seconds_per_tick) //take 2.5% of max health as damage when not near the blob or if the naut has no factory, 5% if both
- var/mutable_appearance/healing = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "nautdamage", MOB_LAYER+0.01)
- healing.appearance_flags = RESET_COLOR
-
- if(overmind)
- healing.color = overmind.blobstrain.complementary_color
-
- flick_overlay_view(healing, 0.8 SECONDS)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/AttackingTarget()
- . = ..()
- if(. && isliving(target) && overmind)
- overmind.blobstrain.blobbernaut_attack(target, src)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/update_icons()
- ..()
- if(overmind) //if we have an overmind, we're doing chemical reactions instead of pure damage
- melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
- melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
- attack_verb_continuous = overmind.blobstrain.blobbernaut_message
- else
- melee_damage_lower = initial(melee_damage_lower)
- melee_damage_upper = initial(melee_damage_upper)
- attack_verb_continuous = initial(attack_verb_continuous)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/death(gibbed)
- if(factory)
- factory.blobbernaut = null //remove this blobbernaut from its factory
- factory.max_integrity = initial(factory.max_integrity)
- flick("blobbernaut_death", src)
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/independent
- independent = TRUE
- gold_core_spawnable = HOSTILE_SPAWN
-
-
diff --git a/code/modules/mob/living/simple_animal/hostile/blobspore.dm b/code/modules/mob/living/simple_animal/hostile/blobspore.dm
deleted file mode 100644
index 501afde41b0..00000000000
--- a/code/modules/mob/living/simple_animal/hostile/blobspore.dm
+++ /dev/null
@@ -1,168 +0,0 @@
-/mob/living/simple_animal/hostile/blob/blobspore
- name = "blob spore"
- desc = "A floating, fragile spore."
- icon_state = "blobpod"
- icon_living = "blobpod"
- health_doll_icon = "blobpod"
- health = BLOBMOB_SPORE_HEALTH
- maxHealth = BLOBMOB_SPORE_HEALTH
- verb_say = "psychically pulses"
- verb_ask = "psychically probes"
- verb_exclaim = "psychically yells"
- verb_yell = "psychically screams"
- melee_damage_lower = BLOBMOB_SPORE_DMG_LOWER
- melee_damage_upper = BLOBMOB_SPORE_DMG_UPPER
- environment_smash = ENVIRONMENT_SMASH_NONE
- obj_damage = 0
- attack_verb_continuous = "hits"
- attack_verb_simple = "hit"
- attack_sound = 'sound/weapons/genhit1.ogg'
- del_on_death = TRUE
- death_message = "explodes into a cloud of gas!"
- gold_core_spawnable = NO_SPAWN //gold slime cores should only spawn the independent subtype
- /// Size of cloud produced from a dying spore
- var/death_cloud_size = 1
- /// The attached person
- var/mob/living/carbon/human/corpse
- /// If this is attached to a person
- var/is_zombie = FALSE
- /// Whether or not this is a fragile spore from Distributed Neurons
- var/is_weak = FALSE
-
-/mob/living/simple_animal/hostile/blob/blobspore/Initialize(mapload, obj/structure/blob/special/linked_node)
- . = ..()
- AddElement(/datum/element/simple_flying)
-
- if(!istype(linked_node))
- return
-
- factory = linked_node
- factory.spores += src
- if(linked_node.overmind && istype(linked_node.overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons) && !istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak))
- notify_ghosts("A controllable spore has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Sentient Spore Created")
- add_cell_sample()
-
-/mob/living/simple_animal/hostile/blob/blobspore/mind_initialize()
- . = ..()
- if(independent || !overmind)
- return FALSE
- var/datum/antagonist/blob_minion/blob_zombie/zombie = new(overmind)
- mind.add_antag_datum(zombie)
-
-/mob/living/simple_animal/hostile/blob/blobspore/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!is_zombie && isturf(loc))
- for(var/mob/living/carbon/human/target in view(src,1)) //Only for corpse right next to/on same tile
- if(!is_weak && target.stat == DEAD)
- zombify(target)
- break
- if(factory && !is_valid_z_level(get_turf(src), get_turf(factory)))
- death()
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/attack_ghost(mob/user)
- . = ..()
- if(.)
- return
- humanize_pod(user)
-
-/mob/living/simple_animal/hostile/blob/blobspore/death(gibbed)
- // On death, create a small smoke of harmful gas (s-Acid)
- var/datum/effect_system/fluid_spread/smoke/chem/spores = new
- var/turf/location = get_turf(src)
-
- // Create the reagents to put into the air
- create_reagents(10)
-
- if(overmind?.blobstrain)
- overmind.blobstrain.on_sporedeath(src)
- else
- reagents.add_reagent(/datum/reagent/toxin/spore, 10)
-
- // Attach the smoke spreader and setup/start it.
- spores.attach(location)
- spores.set_up(death_cloud_size, holder = src, location = location, carry = reagents, silent = TRUE)
- spores.start()
- if(factory)
- factory.spore_delay = world.time + factory.spore_cooldown //put the factory on cooldown
-
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/death()
- if(factory)
- factory.spores -= src
- corpse?.forceMove(loc)
- corpse = null
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/update_icons()
- if(overmind)
- add_atom_colour(overmind.blobstrain.complementary_color, FIXED_COLOUR_PRIORITY)
- else
- remove_atom_colour(FIXED_COLOUR_PRIORITY)
- if(!is_zombie)
- return FALSE
-
- copy_overlays(corpse, TRUE)
- var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/nonhuman-player/blob.dmi', "blob_head")
- if(overmind)
- blob_head_overlay.color = overmind.blobstrain.complementary_color
- color = initial(color) // looks better.
- add_overlay(blob_head_overlay)
-
-/mob/living/simple_animal/hostile/blob/blobspore/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_BLOBSPORE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-/mob/living/simple_animal/hostile/blob/blobspore/independent
- gold_core_spawnable = HOSTILE_SPAWN
- independent = TRUE
-
-/mob/living/simple_animal/hostile/blob/blobspore/weak
- name = "fragile blob spore"
- health = 15
- maxHealth = 15
- melee_damage_lower = 1
- melee_damage_upper = 2
- death_cloud_size = 0
- is_weak = TRUE
-
-/** Ghost control a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/humanize_pod(mob/user)
- if((!overmind || istype(src, /mob/living/simple_animal/hostile/blob/blobspore/weak) || !istype(overmind.blobstrain, /datum/blobstrain/reagent/distributed_neurons)) && !is_zombie)
- return FALSE
- if(key || stat)
- return FALSE
- var/pod_ask = tgui_alert(usr, "Are you bulbous enough?", "Blob Spore", list("Yes", "No"))
- if(pod_ask != "Yes" || QDELETED(src))
- return FALSE
- if(key)
- to_chat(user, span_warning("Someone else already took this spore!"))
- return FALSE
- key = user.key
- log_message("took control of [name].", LOG_GAME)
-
-/** Zombifies a dead mob, turning it into a blob zombie */
-/mob/living/simple_animal/hostile/blob/blobspore/proc/zombify(mob/living/carbon/human/target)
- is_zombie = 1
- if(target.wear_suit)
- maxHealth += target.get_armor_rating(MELEE) // That zombie's got armor, I want armor!
- maxHealth += 40
- health = maxHealth
- name = "blob zombie"
- desc = "A shambling corpse animated by the blob."
- mob_biotypes |= MOB_HUMANOID
- melee_damage_lower += 8
- melee_damage_upper += 11
- obj_damage = 20 // now that it has a corpse to puppet, it can properly attack structures
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- movement_type = GROUND
- death_cloud_size = 0
- icon = target.icon
- icon_state = "zombie"
- target.set_facial_hairstyle("Shaved", update = FALSE)
- target.set_hairstyle("Bald", update = TRUE)
- target.forceMove(src)
- corpse = target
- update_icons()
- visible_message(span_warning("The corpse of [target.name] suddenly rises!"))
- if(!key)
- notify_ghosts("\A [src] has been created in \the [get_area(src)].", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, header = "Blob Zombie Created")
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index 9bc6a5b22db..fe901b3ad9e 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -66,7 +66,7 @@
else
desired_mob.key = key
- SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE)
+ SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE, desired_mob)
if(delete_old_mob)
QDEL_IN(src, 1)
return desired_mob
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
index abb764d0689..11cf2e6789e 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -263,7 +263,7 @@
/datum/reagent/medicine/psicodine = -2) //Blob zombies likely wouldn't appreciate psicodine so why this is here
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobspore/independent = 2) //These are useless so we might as well spawn 2.
+ resulting_atoms = list(/mob/living/basic/blob_minion/spore = 2) //These are useless so we might as well spawn 2.
/datum/micro_organism/cell_line/blobbernaut
desc = "Blobular myocytes"
@@ -282,7 +282,7 @@
suppressive_reagents = list(/datum/reagent/consumable/tinlux = -6)
virus_suspectibility = 0
- resulting_atoms = list(/mob/living/simple_animal/hostile/blob/blobbernaut/independent = 1)
+ resulting_atoms = list(/mob/living/basic/blob_minion/blobbernaut = 1)
/datum/micro_organism/cell_line/gelatinous_cube
desc = "Cubic ooze particles"
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 56910fe08da..ad1eacc3e6d 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -80,12 +80,6 @@
/mob/living/simple_animal/hostile/asteroid/polarbear/lesser,
/mob/living/simple_animal/hostile/asteroid/wolf,
/mob/living/simple_animal/hostile/big_legion,
- /mob/living/simple_animal/hostile/blob,
- /mob/living/simple_animal/hostile/blob/blobbernaut,
- /mob/living/simple_animal/hostile/blob/blobbernaut/independent,
- /mob/living/simple_animal/hostile/blob/blobspore,
- /mob/living/simple_animal/hostile/blob/blobspore/independent,
- /mob/living/simple_animal/hostile/blob/blobspore/weak,
/mob/living/simple_animal/hostile/construct,
/mob/living/simple_animal/hostile/construct/artificer,
/mob/living/simple_animal/hostile/construct/artificer/angelic,
diff --git a/tgstation.dme b/tgstation.dme
index 1de6da4b1bc..31e35da7321 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -279,6 +279,7 @@
#include "code\__DEFINES\dcs\signals\signals_backpack.dm"
#include "code\__DEFINES\dcs\signals\signals_beam.dm"
#include "code\__DEFINES\dcs\signals\signals_bitrunning.dm"
+#include "code\__DEFINES\dcs\signals\signals_blob.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_camera.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
@@ -833,6 +834,7 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\targeted_mob_ability.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\tipped_subtree.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\travel_to_point.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm"
#include "code\datums\ai\basic_mobs\pet_commands\fetch.dm"
#include "code\datums\ai\basic_mobs\pet_commands\pet_command_planning.dm"
@@ -854,6 +856,7 @@
#include "code\datums\ai\hauntium\hauntium_subtrees.dm"
#include "code\datums\ai\hunting_behavior\hunting_behaviors.dm"
#include "code\datums\ai\hunting_behavior\hunting_cockroach.dm"
+#include "code\datums\ai\hunting_behavior\hunting_corpses.dm"
#include "code\datums\ai\hunting_behavior\hunting_lights.dm"
#include "code\datums\ai\hunting_behavior\hunting_mouse.dm"
#include "code\datums\ai\idle_behaviors\_idle_behavior.dm"
@@ -930,6 +933,7 @@
#include "code\datums\components\basic_mob_attack_telegraph.dm"
#include "code\datums\components\basic_ranged_ready_overlay.dm"
#include "code\datums\components\beetlejuice.dm"
+#include "code\datums\components\blob_minion.dm"
#include "code\datums\components\blood_walk.dm"
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\boomerang.dm"
@@ -4316,6 +4320,11 @@
#include "code\modules\mob\living\basic\festivus_pole.dm"
#include "code\modules\mob\living\basic\health_adjustment.dm"
#include "code\modules\mob\living\basic\tree.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_ai.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_mob.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_spore.dm"
+#include "code\modules\mob\living\basic\blob_minions\blob_zombie.dm"
+#include "code\modules\mob\living\basic\blob_minions\blobbernaut.dm"
#include "code\modules\mob\living\basic\clown\clown.dm"
#include "code\modules\mob\living\basic\clown\clown_ai.dm"
#include "code\modules\mob\living\basic\farm_animals\deer.dm"
@@ -4652,9 +4661,6 @@
#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\blob.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobbernaut.dm"
-#include "code\modules\mob\living\simple_animal\hostile\blobspore.dm"
#include "code\modules\mob\living\simple_animal\hostile\dark_wizard.dm"
#include "code\modules\mob\living\simple_animal\hostile\heretic_monsters.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 e1c692d8c06..09d6c93f497 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss
@@ -534,6 +534,10 @@ em {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #556b2f;
font-weight: bold;
font-size: 185%;
diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
index 30841d9429a..60654f76e96 100644
--- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
+++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss
@@ -593,6 +593,10 @@ h2.alert {
}
.blob {
+ color: #ee4000;
+}
+
+.blobannounce {
color: #323f1c;
font-weight: bold;
font-size: 185%;