diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm
index 397ab30cc3a..504bcb60d22 100644
--- a/code/__DEFINES/MC.dm
+++ b/code/__DEFINES/MC.dm
@@ -98,6 +98,14 @@
/datum/controller/subsystem/processing/##X/fire() {..() /*just so it shows up on the profiler*/} \
/datum/controller/subsystem/processing/##X
+#define FLUID_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/fluids/##X);\
+/datum/controller/subsystem/fluids/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+ PreInit();\
+}\
+/datum/controller/subsystem/fluids/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/fluids/##X
+
#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\
/datum/controller/subsystem/timer/##X/New(){\
NEW_SS_GLOBAL(SS##X);\
diff --git a/code/__DEFINES/blob.dm b/code/__DEFINES/blob.dm
index 27f64f17f0a..55947bf38e2 100644
--- a/code/__DEFINES/blob.dm
+++ b/code/__DEFINES/blob.dm
@@ -13,6 +13,7 @@
#define THIRD_STAGE_COEF 0.75
#define FIRST_STAGE_THRESHOLD 300
#define SECOND_STAGE_THRESHOLD 400
+#define THIRD_STAGE_DELTA_THRESHOLD 250
#define BLOB_STAGE_NONE -1
#define BLOB_STAGE_ZERO 0
#define BLOB_STAGE_FIRST 1
@@ -48,9 +49,145 @@
#define FIRST_STAGE_WARN span_userdanger("Вы чувствуете усталость и раздутость.")
#define SECOND_STAGE_WARN span_userdanger("Вы чувствуете, что вот-вот лопнете.")
-#define isblobbernaut(M) istype((M), /mob/living/simple_animal/hostile/blob/blobbernaut)
+#define isblobbernaut(M) istype((M), /mob/living/simple_animal/hostile/blob_minion/blobbernaut)
+
+#define TOTAL_BLOB_MASS SSticker?.mode?.legit_blobs?.len
+#define NEEDED_BLOB_MASS SSticker?.mode?.blob_win_count
//Few global vars to track the blob
GLOBAL_LIST_EMPTY(blobs)
GLOBAL_LIST_EMPTY(blob_cores)
GLOBAL_LIST_EMPTY(blob_nodes)
+
+// Overmind defines
+
+#define OVERMIND_MAX_POINTS_DEFAULT 100 // Max point storage
+#define OVERMIND_STARTING_POINTS 60 // Points granted upon start
+#define OVERMIND_STARTING_REROLLS 1 // Free strain rerolls at the start
+#define OVERMIND_MAX_CAMERA_STRAY "3x3" // How far the overmind camera is allowed to stray from blob tiles. 3x3 is 1 tile away, 5x5 2 tiles etc
+
+
+// Generic blob defines
+
+#define BLOB_BASE_POINT_RATE 2 // Base amount of points per process()
+#define BLOB_EXPAND_COST 4 // Price to expand onto a new tile
+#define BLOB_ZOMBIFICATION_COST 5
+#define BLOB_ATTACK_REFUND 2 // Points 'refunded' when the expand attempt actually attacks something instead
+#define BLOB_BRUTE_RESIST 0.5 // Brute damage taken gets multiplied by this value
+#define BLOB_FIRE_RESIST 1 // Burn damage taken gets multiplied by this value
+#define BLOB_EXPAND_CHANCE_MULTIPLIER 1 // Increase this value to make blobs naturally expand faster
+#define BLOB_REINFORCE_CHANCE 2.5 // The seconds_per_tick chance for cores/nodes to reinforce their surroundings
+#define BLOB_REAGENTATK_VOL 25 // Amount of strain-reagents that get injected when the blob attacks: main source of blob damage
+#define BLOB_REAGENSPORE_VOL 10
+#define BLOB_BONUS_POINTS 60
+#define BLOB_REAGENTS_METABOLISM 1
+
+
+// Structure properties
+
+#define BLOB_CORE_MAX_HP 400
+#define BLOB_CORE_HP_REGEN 2 // Bases health regeneration rate every process(), can be added on by strains
+#define BLOB_CORE_CLAIM_RANGE 12 // Range in which blob tiles are 'claimed' (converted from dead to alive, rarely useful)
+#define BLOB_CORE_PULSE_RANGE 4 // The radius up to which the core activates structures, and up to which structures can be built
+#define BLOB_CORE_EXPAND_RANGE 3 // Radius of automatic expansion
+#define BLOB_CORE_STRONG_REINFORCE_RANGE 1 // The radius of tiles surrounding the core that get upgraded
+#define BLOB_CORE_REFLECTOR_REINFORCE_RANGE 0
+#define BLOB_CORE_FIRE_RESIST 2
+#define BLOB_CORE_POINT_RATE 2
+
+#define BLOB_NODE_MAX_HP 200
+#define BLOB_NODE_HP_REGEN 3
+#define BLOB_NODE_MIN_DISTANCE 5 // Minimum distance between nodes
+#define BLOB_NODE_CLAIM_RANGE 10
+#define BLOB_NODE_PULSE_RANGE 3 // The radius up to which the core activates structures, and up to which structures can be built
+#define BLOB_NODE_EXPAND_RANGE 2 // Radius of automatic expansion
+#define BLOB_NODE_STRONG_REINFORCE_RANGE 0 // The radius of tiles surrounding the node that get upgraded
+#define BLOB_NODE_REFLECTOR_REINFORCE_RANGE 0
+
+#define BLOB_FACTORY_MAX_HP 200
+#define BLOB_FACTORY_HP_REGEN 1
+#define BLOB_FACTORY_MIN_DISTANCE 7 // Minimum distance between factories
+#define BLOB_FACTORY_MAX_SPORES 3
+
+#define BLOB_RESOURCE_MAX_HP 60
+#define BLOB_RESOURCE_HP_REGEN 15
+#define BLOB_RESOURCE_MIN_DISTANCE 4 // Minimum distance between resource blobs
+#define BLOB_RESOURCE_GATHER_DELAY (4 SECONDS) // Gather points when pulsed outside this interval
+#define BLOB_RESOURCE_GATHER_ADDED_DELAY (0.25 SECONDS) // Every additional resource blob adds this amount to the gather delay
+#define BLOB_RESOURCE_POINT_RATE 1
+
+#define BLOB_REGULAR_MAX_HP 25
+#define BLOB_REGULAR_HP_INIT 21 // The starting HP of a normal blob tile
+#define BLOB_REGULAR_HP_REGEN 1 // Health regenerated when pulsed by a node/core
+
+#define BLOB_STRONG_MAX_HP 150
+#define BLOB_STRONG_HP_REGEN 2
+#define BLOB_STRONG_BRUTE_RESIST 0.25 // Brute damage taken gets multiplied by this value
+
+#define BLOB_CAP_NUKE_MAX_HP 100
+#define BLOB_CAP_NUKE_HP_REGEN 1
+
+#define BLOB_REFLECTOR_MAX_HP 150
+#define BLOB_REFLECTOR_HP_REGEN 2
+
+#define BLOB_STORAGE_MAX_HP 30
+#define BLOB_STORAGE_MAX_POINTS_BONUS 50
+#define BLOB_STORAGE_MIN_DISTANCE 3
+#define BLOB_STORAGE_FIRE_RESIST 2
+
+
+// Structure purchasing
+
+#define BLOB_UPGRADE_STRONG_COST 15 // Upgrade and build costs here
+#define BLOB_UPGRADE_REFLECTOR_COST 15
+#define BLOB_STRUCTURE_RESOURCE_COST 40
+#define BLOB_STRUCTURE_STORAGE_COST 40
+#define BLOB_STRUCTURE_FACTORY_COST 60
+#define BLOB_STRUCTURE_NODE_COST 50
+#define BLOB_CORE_SPLIT_COST 100
+
+#define BLOB_REFUND_STRONG_COST 4 // Points refunded when destroying the structure
+#define BLOB_REFUND_REFLECTOR_COST 4
+#define BLOB_REFUND_RESOURCE_COST 15
+#define BLOB_REFUND_FACTORY_COST 25
+#define BLOB_REFUND_NODE_COST 25
+#define BLOB_REFUND_STORAGE_COST 12
+#define BLOB_REFUND_CORE_COST -1
+#define BLOB_REFUND_CAP_NUKE_COST 0
+
+// Blob power properties
+
+#define BLOB_POWER_RELOCATE_COST 80 // Resource cost to move your core to a different node
+#define BLOB_POWER_REROLL_COST 40 // Strain reroll
+#define BLOB_POWER_REROLL_FREE_TIME (4 MINUTES) // Gain a free strain reroll every x minutes
+#define BLOB_POWER_REROLL_CHOICES 6 // Possibilities to choose from; keep in mind increasing this might fuck with the radial menu
+
+
+// Mob defines
+
+#define BLOBMOB_HEALING_MULTIPLIER 0.0125 // Multiplies by -maxHealth and heals the blob by this amount every blob_act
+#define BLOBMOB_SPORE_HEALTH 30 // Base spore health
+#define BLOBMOB_SPORE_SPAWN_COOLDOWN (8 SECONDS)
+#define BLOBMOB_SPORE_DMG_LOWER 3
+#define BLOBMOB_SPORE_DMG_UPPER 7
+#define BLOBMOB_SPORE_OBJ_DMG 20
+#define BLOBMOB_SPORE_SPEED_MOD -1.5
+#define BLOBMOB_ZOMBIE_HEALTH 70 // Base spore health
+#define BLOBMOB_ZOMBIE_DMG_LOWER 10
+#define BLOBMOB_ZOMBIE_DMG_UPPER 15
+#define BLOBMOB_ZOMBIE_OBJ_DMG 20
+#define BLOBMOB_ZOMBIE_SPEED_MOD -1
+#define BLOBMOB_BLOBBERNAUT_RESOURCE_COST 40 // Purchase price for making a blobbernaut
+#define BLOBMOB_BLOBBERNAUT_HEALTH 200 // Base blobbernaut health
+#define BLOBMOB_BLOBBERNAUT_DMG_SOLO_LOWER 20 // Damage without active overmind (core dead or xenobio mob)
+#define BLOBMOB_BLOBBERNAUT_DMG_SOLO_UPPER 20
+#define BLOBMOB_BLOBBERNAUT_DMG_LOWER 4 // Damage dealt with active overmind (most damage comes from strain chems)
+#define BLOBMOB_BLOBBERNAUT_DMG_UPPER 4
+#define BLOBMOB_BLOBBERNAUT_REAGENTATK_VOL 20 // Amounts of strain reagents applied on attack -- basically the main damage stat
+#define BLOBMOB_BLOBBERNAUT_DMG_OBJ 60 // Damage dealth to objects/machines
+#define BLOBMOB_BLOBBERNAUT_HEALING_CORE 0.05 // Percentage multiplier HP restored on Life() when within 2 tiles of the blob core
+#define BLOBMOB_BLOBBERNAUT_HEALING_NODE 0.025 // Same, but for a nearby node
+#define BLOBMOB_BLOBBERNAUT_HEALING_TILE 0.0125 // Same, but for a nearby blob tile
+#define BLOBMOB_BLOBBERNAUT_HEALTH_DECAY 0.0125 // Percentage multiplier HP lost when not near blob tiles or without factory
+
+#define BLOB_ACT_PROTECTION_TIME 2 SECONDS
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index aa521e47314..8bb6aa740ce 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -139,6 +139,8 @@
#define COMSIG_ATOM_BULLET_ACT "atom_bullet_act"
///from base of atom/blob_act(): (/obj/structure/blob)
#define COMSIG_ATOM_BLOB_ACT "atom_blob_act"
+ /// if returned, forces nothing to happen when the atom is attacked by a blob
+ #define COMPONENT_CANCEL_BLOB_ACT (1<<0)
///from base of atom/acid_act(): (acidpwr, acid_volume)
#define COMSIG_ATOM_ACID_ACT "atom_acid_act"
///from base of atom/emag_act(): (/mob/user)
@@ -387,6 +389,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/death(): (gibbed)
#define COMSIG_MOB_DEATH "mob_death"
///from base of mob/ghostize(): (mob/dead/observer/ghost)
@@ -470,6 +474,9 @@
////from mob/living/adjust_fire_stacks()
#define COMSIG_MOB_ADJUST_FIRE "mob_adjust_fire"
+////from mob/living/adjust_wet_stacks()
+#define COMSIG_MOB_ADJUST_WET "mob_adjust_wet"
+
///from base of /mob/living/toggle_move_intent(): (old_move_intent)
#define COMSIG_MOB_MOVE_INTENT_TOGGLE "mob_move_intent_toggle"
#define COMPONENT_BLOCK_INTENT_TOGGLE (1<<0)
@@ -508,6 +515,9 @@
/// Performed after the hands are swapped.
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
+/// from mob/get_status_tab_items(): (list/items)
+#define COMSIG_MOB_GET_STATUS_TAB_ITEMS "mob_get_status_tab_items"
+
///From base of mob/update_movespeed():area
#define COMSIG_MOB_MOVESPEED_UPDATED "mob_update_movespeed"
@@ -520,6 +530,8 @@
#define COMSIG_CLIENT_SET_EYE "client_set_eye"
// from /client/proc/change_view() : (new_size)
#define COMSIG_VIEW_SET "view_set"
+/// from /mob/proc/change_mob_type() : ()
+#define COMSIG_MOB_CHANGED_TYPE "mob_changed_type"
// /mob/living signals
@@ -527,6 +539,8 @@
#define COMSIG_LIVING_RESIST "living_resist"
///from base of mob/living/IgniteMob() (/mob/living)
#define COMSIG_LIVING_IGNITED "living_ignite"
+///from base of mob/living/WetMob() (/mob/living)
+#define COMSIG_LIVING_WET "living_weted"
///from base of mob/living/ExtinguishMob() (/mob/living)
#define COMSIG_LIVING_EXTINGUISHED "living_extinguished"
///from base of mob/living/electrocute_act(): (shock_damage, source, siemens_coeff, flags)
@@ -559,6 +573,8 @@
#define COMSIG_BORG_SAFE_DECONSTRUCT "borg_safe_decon"
///sent from living mobs every tick of fire
#define COMSIG_LIVING_FIRE_TICK "living_fire_tick"
+///sent from living mobs every tick of wet
+#define COMSIG_LIVING_WET_TICK "living_wet_tick"
//sent from living mobs when they are ahealed
#define COMSIG_LIVING_AHEAL "living_aheal"
///From living/Life(). (deltatime, times_fired)
@@ -579,6 +595,10 @@
#define COMSIG_LIVING_RESTING "living_resting"
///from base of mob/update_transform()
#define COMSIG_LIVING_POST_UPDATE_TRANSFORM "living_post_update_transform"
+/// From mob/living/try_speak(): (message)
+#define COMSIG_MOB_TRY_SPEECH "living_vocal_speech"
+ /// Return if the mob cannot speak.
+ #define COMPONENT_CANNOT_SPEAK (1<<0)
///called on /living when someone starts pulling (atom/movable/pulled, state, force)
#define COMSIG_LIVING_START_PULL "living_start_pull"
@@ -593,6 +613,8 @@
/// Called from /mob/living/PushAM -- Called when this mob is about to push a movable, but before it moves
/// (aotm/movable/being_pushed)
#define COMSIG_LIVING_PUSHING_MOVABLE "living_pushing_movable"
+///from base of /mob/living/examine(): (mob/user, list/.)
+#define COMSIG_LIVING_EXAMINE "living_examine"
///from base of mob/living/Stun() (amount, ignore_canstun)
#define COMSIG_LIVING_STATUS_STUN "living_stun"
@@ -678,7 +700,10 @@
#define COMSIG_CARBON_APPLY_OVERLAY "carbon_apply_overlay"
///Called from remove_overlay(cache_index, overlay)
#define COMSIG_CARBON_REMOVE_OVERLAY "carbon_remove_overlay"
-
+#define COMSIG_CARBON_UPDATING_HEALTH_HUD "carbon_health_hud_update"
+#define COMSIG_HUMAN_UPDATING_HEALTH_HUD "human_health_hud_update"
+ /// Return if you override the carbon's or human's health hud with something else
+ #define COMPONENT_OVERRIDE_HEALTH_HUD (1<<0)
// /mob/living/simple_animal signals
///from /mob/living/attack_animal(): (mob/living/simple_animal/M)
#define COMSIG_SIMPLE_ANIMAL_ATTACKEDBY "simple_animal_attackedby"
@@ -688,6 +713,9 @@
#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget"
#define COMPONENT_HOSTILE_NO_ATTACK (1<<0)
+///after attackingtarget has happened, source is the attacker and target is the attacked, extra argument for if the attackingtarget was successful
+#define COMSIG_HOSTILE_POST_ATTACKINGTARGET "hostile_post_attackingtarget"
+
/// Called when a /mob/living/simple_animal/hostile fines a new target: (atom/source, give_target)
#define COMSIG_HOSTILE_FOUND_TARGET "comsig_hostile_found_target"
@@ -1200,3 +1228,7 @@
/// Source: /mob/living/simple_animal/borer, listening in datum/antagonist/borer
#define COMSIG_BORER_ENTERED_HOST "borer_on_enter" // when borer entered host
#define COMSIG_BORER_LEFT_HOST "borer_on_leave" // when borer left host
+///from /datum/spawners_menu/ui_act(): (mob/user)
+#define COMSIG_IS_GHOST_CONTROLABLE "is_ghost_controllable"
+ /// Return this to signal that the mob can be controlled by ghosts
+ #define COMPONENT_GHOST_CONTROLABLE (1<<0)
diff --git a/code/__DEFINES/dcs/signals_blob.dm b/code/__DEFINES/dcs/signals_blob.dm
new file mode 100644
index 00000000000..3fd9a3ec608
--- /dev/null
+++ b/code/__DEFINES/dcs/signals_blob.dm
@@ -0,0 +1,9 @@
+/// 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"
+
+/// Signal sent by a blob when it try expand
+#define COMSIG_TRY_CONSUME_TURF "try_consume_turf"
+ /// Component blocks consuming
+ #define COMPONENT_CANT_CONSUME (1<<0)
diff --git a/code/__DEFINES/dcs/signals_object.dm b/code/__DEFINES/dcs/signals_object.dm
index ce5845f447c..1d81249ab54 100644
--- a/code/__DEFINES/dcs/signals_object.dm
+++ b/code/__DEFINES/dcs/signals_object.dm
@@ -16,7 +16,9 @@
/// Return to prevent the default behavior (attack_selfing) from ocurring.
#define COMPONENT_ITEM_ACTION_SLOT_INVALID (1<<0)
-/// from base of /obj/item/slimepotion/speed/interact_with_atom(): (obj/target, /obj/src, mob/user)
#define COMSIG_SPEED_POTION_APPLIED "speed_potion"
#define SPEED_POTION_STOP (1<<0)
+
+///from base of [/obj/proc/update_integrity]: (old_value, new_value)
+#define COMSIG_OBJ_INTEGRITY_CHANGED "obj_integrity_changed"
diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index b224f0706f6..a8b6fe1092f 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -37,6 +37,10 @@
/// Update the atom's icon
#define UPDATE_ICON (UPDATE_ICON_STATE|UPDATE_OVERLAYS)
+/// If the thing can reflect light (lasers/energy)
+#define RICOCHET_SHINY (1<<0)
+/// If the thing can reflect matter (bullets/bomb shrapnel)
+#define RICOCHET_HARD (1<<1)
//Reagent flags
#define REAGENT_NOREACT 1
@@ -186,7 +190,7 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
#define MOBILITY_FLAGS_REST_CAPABLE_DEFAULT (MOBILITY_MOVE|MOBILITY_STAND|MOBILITY_PICKUP|MOBILITY_USE|MOBILITY_UI|MOBILITY_STORAGE|MOBILITY_PULL|MOBILITY_REST|MOBILITY_LIEDOWN)
-//MINING AREA FLAGS
+//AREA FLAGS
/// If mining tunnel generation is allowed in this area
#define CAVES_ALLOWED (1<<1)
/// If flora are allowed to spawn in this area randomly through tunnel generation
@@ -195,6 +199,9 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
#define MOB_SPAWN_ALLOWED (1<<3)
/// If megafauna can be spawned by natural random generation
#define MEGAFAUNA_SPAWN_ALLOWED (1<<4)
+/// If blobs can spawn there and if it counts towards their score.
+#define BLOBS_ALLOWED (1<<5)
+
// timed_action_flags parameter for [/proc/do_after()]
diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm
index 2f2f64db4f5..42884694433 100644
--- a/code/__DEFINES/gamemode.dm
+++ b/code/__DEFINES/gamemode.dm
@@ -31,6 +31,7 @@
#define SPECIAL_ROLE_ABDUCTOR_SCIENTIST "Abductor Scientist"
#define SPECIAL_ROLE_BLOB "Blob"
#define SPECIAL_ROLE_BLOB_OVERMIND "Blob Overmind"
+#define SPECIAL_ROLE_BLOB_MINION "Blob Minion"
#define SPECIAL_ROLE_BORER "Borer"
#define SPECIAL_ROLE_CARP "Space Carp"
#define SPECIAL_ROLE_CHANGELING "Changeling"
diff --git a/code/__DEFINES/generators.dm b/code/__DEFINES/generators.dm
new file mode 100644
index 00000000000..3ad34d39f2c
--- /dev/null
+++ b/code/__DEFINES/generators.dm
@@ -0,0 +1,12 @@
+//generator types
+#define GEN_NUM "num"
+#define GEN_VECTOR "vector"
+#define GEN_BOX "box"
+#define GEN_CIRCLE "circle"
+#define GEN_SPHERE "sphere"
+
+///particle editor var modifiers
+#define P_DATA_GENERATOR "generator"
+#define P_DATA_ICON_ADD "icon_add"
+#define P_DATA_ICON_REMOVE "icon_remove"
+#define P_DATA_ICON_WEIGHT "icon_edit"
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 8a69bac57f7..7ee61dccdfb 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -97,3 +97,6 @@
#define PLANE_GROUP_MAIN "main"
/// A secondary group, used when a client views a generic window
#define PLANE_GROUP_POPUP_WINDOW(screen) "popup-[screen.UID()]"
+
+//Blobbernauts
+#define ui_blobbernaut_overmind_health "EAST-1:28,CENTER+0:19"
diff --git a/code/__DEFINES/math.dm b/code/__DEFINES/math.dm
index f1322a4b7d3..95a7f556d9c 100644
--- a/code/__DEFINES/math.dm
+++ b/code/__DEFINES/math.dm
@@ -34,6 +34,9 @@
// Similar to clamp but the bottom rolls around to the top and vice versa. min is inclusive, max is exclusive
#define WRAP(val, min, max) clamp(( min == max ? min : (val) - (round(((val) - (min))/((max) - (min))) * ((max) - (min))) ),min,max)
+/// Increments a value and wraps it if it exceeds some value. Can be used to circularly iterate through a list through `idx = WRAP_UP(idx, length_of_list)`.
+#define WRAP_UP(val, max) (((val) % (max)) + 1)
+
// Real modulus that handles decimals
#define MODULUS(x, y) ( (x) - FLOOR(x, y))
@@ -119,3 +122,6 @@
/// Like SPT_PROB_RATE but easier to use, simply put `if(SPT_PROB(10, 5))`
#define SPT_PROB(prob_per_second_percent, seconds_per_tick) (prob(100*SPT_PROB_RATE((prob_per_second_percent)/100, (seconds_per_tick))))
// )
+
+/// The number of cells in a taxicab circle (rasterized diamond) of radius X.
+#define DIAMOND_AREA(X) (1 + 2*(X)*((X)+1))
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index ee9f75b8798..9cf4e73fd7a 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -109,6 +109,15 @@
min(CENTER.x + (H_RADIUS), world.maxx), min(CENTER.y + (V_RADIUS), world.maxy), CENTER.z \
)
+#define RANGE_TURFS_MULTIZ(RADIUS, CENTER, Z_MIN, Z_MAX) \
+ RECT_TURFS_MULTIZ(RADIUS, RADIUS, Z_MIN, Z_MAX, CENTER)
+
+#define RECT_TURFS_MULTIZ(H_RADIUS, V_RADIUS, Z_MIN, Z_MAX, CENTER) \
+ block( \
+ max(CENTER.x - (H_RADIUS), 1), max(CENTER.y - (V_RADIUS), 1), Z_MIN, \
+ min(CENTER.x + (H_RADIUS), world.maxx), min(CENTER.y + (V_RADIUS), world.maxy), Z_MAX \
+ )
+
/// Returns the turfs on the edge of a square with CENTER in the middle and with the given RADIUS. If used near the edge of the map, will still work fine.
// order of the additions: top edge + bottom edge + left edge + right edge
#define RANGE_EDGE_TURFS(RADIUS, CENTER)\
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index 089ea970b09..a0655ea26f5 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -61,6 +61,8 @@
////////REAGENT STUFF////////
// How many units of reagent are consumed per tick, by default.
#define REAGENTS_METABOLISM 0.4
+#define REAGENTS_EFFECT_MULTIPLIER (REAGENTS_METABOLISM / 0.4) // By defining the effect multiplier this way, it'll exactly adjust all effects according to how they originally were with the 0.4 metabolism
+#define REM REAGENTS_EFFECT_MULTIPLIER //! Shorthand for the above define for ease of use in equations and the like
// Factor of how fast mob nutrition decreases
#define HUNGER_FACTOR 0.1
@@ -318,6 +320,8 @@
#define isAIEye(A) (istype((A), /mob/camera/aiEye))
#define isovermind(A) (istype((A), /mob/camera/blob))
+#define isminion(A) (istype((A), /mob/living/simple_animal/hostile/blob_minion))
+
#define isSpirit(A) (istype((A), /mob/spirit))
#define ismask(A) (istype((A), /mob/spirit/mask))
@@ -441,6 +445,7 @@
/// Makes the weaken into a knockdown
#define SHOCK_KNOCKDOWN (1<<7)
+
/// Vomit defines
#define VOMIT_NUTRITION_LOSS 10
#define VOMIT_STUN_TIME (8 SECONDS)
diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm
index dd64f3f88af..625ea9e3d55 100644
--- a/code/__DEFINES/obj_flags.dm
+++ b/code/__DEFINES/obj_flags.dm
@@ -16,6 +16,8 @@
#define NODECONSTRUCT (1<<5)
/// Objects will ignore item attacks
#define IGNORE_HITS (1<<6)
+/// Objects will ignore blob_act
+#define IGNORE_BLOB_ACT (1<<7)
// Flags for the item_flags var on /obj/item
diff --git a/code/__DEFINES/particles.dm b/code/__DEFINES/particles.dm
new file mode 100644
index 00000000000..5657566a63b
--- /dev/null
+++ b/code/__DEFINES/particles.dm
@@ -0,0 +1,5 @@
+// /obj/effect/abstract/particle_holder/var/particle_flags
+// Flags that effect how a particle holder displays something
+
+/// If we're inside something inside a mob, display off that mob too
+#define PARTICLE_ATTACH_MOB (1<<0)
diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm
new file mode 100644
index 00000000000..990108e1550
--- /dev/null
+++ b/code/__DEFINES/say.dm
@@ -0,0 +1,4 @@
+// A link given to ghost alice to follow bob
+#define FOLLOW_LINK(alice, bob) "(F)"
+#define TURF_LINK(alice, turfy) "(T)"
+#define FOLLOW_OR_TURF_LINK(alice, bob, turfy) "(F)"
diff --git a/code/__DEFINES/span.dm b/code/__DEFINES/span.dm
index 5c13748e141..0e40365ee2b 100644
--- a/code/__DEFINES/span.dm
+++ b/code/__DEFINES/span.dm
@@ -18,6 +18,7 @@
#define span_alien(str) ("" + str + "")
#define span_announce(str) ("" + str + "")
#define span_big(str) ("" + str + "")
+#define span_blob(str) ("" + str + "")
//#define span_bigicon(str) ("" + str + "")
//#define span_binarysay(str) ("" + str + "")
//#define span_blue(str) ("" + str + "")
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 5cb1e4ae138..2e06c7645f1 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -130,6 +130,7 @@
#define FIRE_PRIORITY_BURNING 40
#define FIRE_PRIORITY_DEFAULT 50
#define FIRE_PRIORITY_PARALLAX 65
+#define FIRE_PRIORITY_FLUIDS 80
#define FIRE_PRIORITY_MOBS 100
#define FIRE_PRIORITY_ASSETS 105
#define FIRE_PRIORITY_TGUI 110
diff --git a/code/__DEFINES/tools.dm b/code/__DEFINES/tools.dm
index 4e9b6571a4e..1cb58f9f41a 100644
--- a/code/__DEFINES/tools.dm
+++ b/code/__DEFINES/tools.dm
@@ -5,6 +5,7 @@
#define TOOL_WIRECUTTER "wirecutter"
#define TOOL_WRENCH "wrench"
#define TOOL_WELDER "welder"
+#define TOOL_ANALYZER "analyzer"
// Surgery tools
#define TOOL_RETRACTOR "retractor"
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index f6eefaa8686..62445dfd3fb 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -26,6 +26,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_CHASM_STOPPER "chasm_stopper"
/// `do_teleport` will not allow this atom to teleport
#define TRAIT_NO_TELEPORT "no-teleport"
+
+/// This atom is a secluded location, which is counted as out of bounds.
+/// Anything that enters this atom's contents should react if it wants to stay in bounds.
+#define TRAIT_SECLUDED_LOCATION "secluded_loc"
+
#define TRAIT_SILENT_FOOTSTEPS "silent_footsteps"
//turf traits
@@ -85,6 +90,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_HUSK "husk"
#define TRAIT_SKELETON "skeleton"
#define TRAIT_NO_CLONE "no_clone"
+/// Isn't attacked harmfully by blob structures
+#define TRAIT_BLOB_ALLY "blob_ally"
+/// Objects with this trait are deleted if they fall into chasms, rather than entering abstract storage
+#define TRAIT_CHASM_DESTROYED "chasm_destroyed"
/// "Magic" trait that blocks the mob from moving or interacting with anything. Used for transient stuff like mob transformations or incorporality in special cases.
/// Will block movement, `Life()` (!!!), and other stuff based on the mob.
@@ -268,3 +277,5 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SPECIES_LIMBS "only_species_limbs"
/// Phohibits using the "Book Of Babel"
#define TRAIT_NO_BABEL "cannot_use_babel"
+
+#define TRAIT_BLOB_ZOMBIFIED "blob_zombified"
diff --git a/code/__DEFINES/traits/sources.dm b/code/__DEFINES/traits/sources.dm
index 92565ab1354..e7a5d9d1837 100644
--- a/code/__DEFINES/traits/sources.dm
+++ b/code/__DEFINES/traits/sources.dm
@@ -153,7 +153,11 @@
/// inherited from riding vehicles
#define VEHICLE_TRAIT "vehicle"
-// blob trait sourses
+/// blob trait sourses
#define BLOB_INFECTED_TRAIT "blob_infected"
#define VENDOR_FLATTENING_TRAIT "vendor_flattening"
+
+#define WET_TRAIT "wet"
+
+#define BLOB_ZOMBIE_TRAIT "blob_zombie_trait"
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index d9f5946d34e..ef5b83587a8 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -880,6 +880,8 @@ proc/dd_sortedObjectList(list/incoming)
///Returns the list if it's actually a valid list, otherwise will initialize it
#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
+///Qdel every item in the list before setting the list to null
+#define QDEL_LAZYLIST(L) for(var/I in L) qdel(I); L = null;
///Adds to the item K the value V, if the list is null it will initialize it
#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += V;
///This is used to add onto lazy assoc list when the value you're adding is a /list/. This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects)
diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm
index 41433156e43..5d61aa41ad7 100644
--- a/code/__HELPERS/_logging.dm
+++ b/code/__HELPERS/_logging.dm
@@ -396,3 +396,16 @@ GLOBAL_PROTECT(log_end)
else
user.mob.create_log(OOC_LOG, text)
log_ooc(text, user)
+
+/proc/loc_name(atom/A)
+ if(!istype(A))
+ return "(INVALID LOCATION)"
+
+ var/turf/T = A
+ if(!istype(T))
+ T = get_turf(A)
+
+ if(istype(T))
+ return "([AREACOORD(T)])"
+ else if(A.loc)
+ return "(UNKNOWN (?, ?, ?))"
diff --git a/code/__HELPERS/atoms.dm b/code/__HELPERS/atoms.dm
index e801df08a6e..a5c34d5dc70 100644
--- a/code/__HELPERS/atoms.dm
+++ b/code/__HELPERS/atoms.dm
@@ -132,3 +132,14 @@
return FALSE
return (mover.pass_flags & passflag)
+
+///Returns a list of all locations (except the area) the movable is within.
+/proc/get_nested_locs(atom/movable/atom_on_location, include_turf = FALSE)
+ . = list()
+ var/atom/location = atom_on_location.loc
+ var/turf/our_turf = get_turf(atom_on_location)
+ while(location && location != our_turf)
+ . += location
+ location = location.loc
+ if(our_turf && include_turf) //At this point, only the turf is left, provided it exists.
+ . += our_turf
diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
new file mode 100644
index 00000000000..3f9679e3433
--- /dev/null
+++ b/code/__HELPERS/chat.dm
@@ -0,0 +1,15 @@
+/// 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 within the list, as well as all observers.
+/proc/relay_to_list_and_observers(message, list/mob_list, source)
+ for(var/mob/creature as anything in mob_list)
+ to_chat(creature, message)
+ send_to_observers(message, source)
diff --git a/code/__HELPERS/level_traits.dm b/code/__HELPERS/level_traits.dm
index ae935df35c7..f2b1bd40c11 100644
--- a/code/__HELPERS/level_traits.dm
+++ b/code/__HELPERS/level_traits.dm
@@ -1,5 +1,5 @@
/proc/is_level_reachable(z)
- return check_level_trait(z, REACHABLE)
+ return check_level_trait(z, REACHABLE)
/proc/is_station_level(z)
return check_level_trait(z, STATION_LEVEL)
diff --git a/code/__HELPERS/ref.dm b/code/__HELPERS/ref.dm
new file mode 100644
index 00000000000..365df03dc88
--- /dev/null
+++ b/code/__HELPERS/ref.dm
@@ -0,0 +1,15 @@
+/**
+ * \ref behaviour got changed in 512 so this is necesary to replicate old behaviour.
+ * If it ever becomes necesary to get a more performant REF(), this lies here in wait
+ * #define REF(thing) (thing && isdatum(thing) && (thing:datum_flags & DF_USE_TAG) && thing:tag ? "[thing:tag]" : text_ref(thing))
+**/
+/proc/REF(input)
+ if(isdatum(input))
+ var/datum/thing = input
+ if(thing.datum_flags & DF_USE_TAG)
+ if(!thing.tag)
+ stack_trace("A ref was requested of an object with DF_USE_TAG set but no tag: [thing]")
+ thing.datum_flags &= ~DF_USE_TAG
+ else
+ return "\[[url_encode(thing.tag)]\]"
+ return text_ref(input)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 631b480129c..37a664d8b2f 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -511,8 +511,8 @@ Returns 1 if the chain up to the area contains the given typepath
///Step-towards method of determining whether one atom can see another. Similar to viewers()
///note: this is a line of sight algorithm, view() does not do any sort of raycasting and cannot be emulated by it accurately
-/proc/can_see(atom/source, atom/target, length = 5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate.
- var/turf/current_turf = get_turf(source)
+/atom/proc/can_see(atom/target, length = 5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate.
+ var/turf/current_turf = get_turf(src)
var/turf/target_turf = get_turf(target)
if(!current_turf || !target_turf) // nullspace
return FALSE
@@ -1581,6 +1581,29 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
if(areas)
. |= T.loc
+/proc/urange_multiz(dist=0, atom/center=usr, orange=0, areas=0)
+ if(!dist)
+ if(!orange)
+ return list(center)
+ else
+ return list()
+ var/list/stations_z = levels_by_trait(STATION_LEVEL)
+ var/min_z = max(center.z - dist, stations_z[1])
+ var/max_z = min(center.z + dist, stations_z[length(stations_z)])
+ var/list/turfs = RANGE_TURFS_MULTIZ(dist, center, min_z, max_z)
+ if(orange)
+ turfs -= get_turf(center)
+ . = list()
+ for(var/V in turfs)
+ var/turf/T = V
+ . += T
+ . += T.contents
+ if(areas)
+ . |= T.loc
+
+/proc/is_there_multiz()
+ return SSmapping?.map_datum?.traits?.len > 1
+
/proc/screen_loc2turf(scr_loc, turf/origin)
var/tX = splittext(scr_loc, ",")
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 965ce2e5456..74d8bbfdd01 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -21,3 +21,11 @@ DEFINE_BITFIELD(datum_flags, list(
"DF_VAR_EDITED" = DF_VAR_EDITED,
"DF_USE_TAG" = DF_USE_TAG,
))
+
+DEFINE_BITFIELD(area_flags, list(
+ "BLOBS_ALLOWED" = BLOBS_ALLOWED,
+ "CAVES_ALLOWED" = CAVES_ALLOWED,
+ "FLORA_ALLOWED" = FLORA_ALLOWED,
+ "MEGAFAUNA_SPAWN_ALLOWED" = MEGAFAUNA_SPAWN_ALLOWED,
+ "MOB_SPAWN_ALLOWED" = MOB_SPAWN_ALLOWED,
+))
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index 86139603b36..dd7b45868e8 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -24,8 +24,16 @@ GLOBAL_LIST_EMPTY(human_list) //all instances of /mob/living/carbon/human and
GLOBAL_LIST_EMPTY(spirits) //List of all the spirits, including Masks
GLOBAL_LIST_EMPTY(alive_mob_list) //List of all alive mobs, including clientless. Excludes /mob/new_player
GLOBAL_LIST_EMPTY(dead_mob_list) //List of all dead mobs, including clientless. Excludes /mob/new_player
+/// All alive mobs with clients.
+GLOBAL_LIST_EMPTY(alive_player_list)
+/// All dead mobs with clients. Does not include observers.
+GLOBAL_LIST_EMPTY(dead_player_list)
+/// All observers with clients that joined as observers.
+GLOBAL_LIST_EMPTY(current_observers_list)
GLOBAL_LIST_EMPTY(respawnable_list) //List of all mobs, dead or in mindless creatures that still be respawned.
GLOBAL_LIST_EMPTY(non_respawnable_keys) //List of ckeys that are excluded from respawning for remainder of round.
+/// All living mobs which can hear blob telepathy
+GLOBAL_LIST_EMPTY(blob_telepathy_mobs)
/// One for each AI_* status define, List of all simple animals, including clientless
GLOBAL_LIST_INIT(simple_animals, list(list(), list(), list(), list()))
GLOBAL_LIST_EMPTY(bots_list) //List of all bots(beepsky, medibots,etc)
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 54ad01b0fe2..56bf8c5dede 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -27,6 +27,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_NO_IMMOBILIZE" = TRAIT_NO_IMMOBILIZE,
"TRAIT_NO_TELEPORT" = TRAIT_NO_TELEPORT,
"TRAIT_RADSTORM_IMMUNE" = TRAIT_RADSTORM_IMMUNE,
+ "TRAIT_SECLUDED_LOCATION" = TRAIT_SECLUDED_LOCATION,
"TRAIT_SILENT_FOOTSTEPS" = TRAIT_SILENT_FOOTSTEPS,
"TRAIT_SOLARFLARE_IMMUNE" = TRAIT_SOLARFLARE_IMMUNE,
"TRAIT_SNOWSTORM_IMMUNE" = TRAIT_SNOWSTORM_IMMUNE,
@@ -36,11 +37,14 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_AI_UNTRACKABLE" = TRAIT_AI_UNTRACKABLE,
"TRAIT_BADASS" = TRAIT_BADASS,
"TRAIT_BLIND" = TRAIT_BLIND,
+ "TRAIT_BLOB_ALLY" = TRAIT_BLOB_ALLY,
"TRAIT_BLOODCRAWL" = TRAIT_BLOODCRAWL,
"TRAIT_BLOODCRAWL_EAT" = TRAIT_BLOODCRAWL_EAT,
"TRAIT_CAN_STRIP" = TRAIT_CAN_STRIP,
"TRAIT_CANT_RIDE" = TRAIT_CANT_RIDE,
+ "TRAIT_CHASM_DESTROYED" = TRAIT_CHASM_DESTROYED,
"TRAIT_CHUNKYFINGERS" = TRAIT_CHUNKYFINGERS,
+ "TRAIT_NO_GUNS" = TRAIT_NO_GUNS,
"TRAIT_COLORBLIND" = TRAIT_COLORBLIND,
"TRAIT_COMIC" = TRAIT_COMIC,
"TRAIT_CLUMSY" = TRAIT_CLUMSY,
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index f815904dff8..63c98048437 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -44,7 +44,7 @@
Note that this proc can be overridden, and is in the case of screen objects.
*/
/atom/Click(location,control,params)
- usr.ClickOn(src, params)
+ usr.ClickOn(src, params, location)
/atom/DblClick(location,control,params)
usr.DblClickOn(src,params)
diff --git a/code/_onclick/hud/ai.dm b/code/_onclick/hud/ai.dm
index aeb47d7f953..b97d312700d 100644
--- a/code/_onclick/hud/ai.dm
+++ b/code/_onclick/hud/ai.dm
@@ -158,9 +158,6 @@
var/mob/living/silicon/ai/AI = usr
AI.move_down()
-/mob/living/silicon/ai/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/ai(src)
/datum/hud/ai/New(mob/owner)
..()
diff --git a/code/_onclick/hud/alien.dm b/code/_onclick/hud/alien.dm
index 9c05d0a72d5..cc9b1def284 100644
--- a/code/_onclick/hud/alien.dm
+++ b/code/_onclick/hud/alien.dm
@@ -26,10 +26,6 @@
screen_loc = ui_alienplasmadisplay
-/mob/living/carbon/alien/humanoid/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/alien(src)
-
/datum/hud/alien/New(mob/living/carbon/alien/humanoid/owner)
..()
diff --git a/code/_onclick/hud/alien_larva.dm b/code/_onclick/hud/alien_larva.dm
index 3be0fd9487d..229b290f268 100644
--- a/code/_onclick/hud/alien_larva.dm
+++ b/code/_onclick/hud/alien_larva.dm
@@ -1,7 +1,3 @@
-/mob/living/carbon/alien/larva/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/larva(src)
-
/datum/hud/larva/New(mob/owner)
..()
diff --git a/code/_onclick/hud/blob_overmind.dm b/code/_onclick/hud/blob_overmind.dm
index 91bd2e9e4db..7cd2290c683 100644
--- a/code/_onclick/hud/blob_overmind.dm
+++ b/code/_onclick/hud/blob_overmind.dm
@@ -1,9 +1,5 @@
-/mob/camera/blob/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/blob_overmind(src)
-
/atom/movable/screen/blob
- icon = 'icons/mob/blob.dmi'
+ icon = 'icons/hud/blob.dmi'
/atom/movable/screen/blob/MouseEntered(location,control,params)
openToolTip(usr,src,params,title = name,content = desc, theme = "blob")
@@ -13,7 +9,7 @@
/atom/movable/screen/blob/BlobHelp
icon_state = "ui_help"
- name = "Blob Help"
+ name = "Помощь"
desc = "Помощь по игре за блоба!"
/atom/movable/screen/blob/BlobHelp/Click()
@@ -23,7 +19,7 @@
/atom/movable/screen/blob/JumpToNode
icon_state = "ui_tonode"
- name = "Jump to Node"
+ name = "К узлу"
desc = "Перемещает вашу камеру к выбранному узлу."
/atom/movable/screen/blob/JumpToNode/Click()
@@ -33,7 +29,7 @@
/atom/movable/screen/blob/JumpToCore
icon_state = "ui_tocore"
- name = "Jump to Core"
+ name = "К ядру"
desc = "Перемещает вашу камеру к вашему ядру."
/atom/movable/screen/blob/JumpToCore/MouseEntered(location,control,params)
@@ -49,8 +45,13 @@
/atom/movable/screen/blob/Blobbernaut
icon_state = "ui_blobbernaut"
- name = "Produce Blobbernaut (60)"
- desc = "Производит сильного и умного блоббернаута из фабрики за 60 ресурсов.
Фабрика будет уничтожена в процессе."
+ name = "Создать блобернаута (ERROR)"
+ desc = "Создает сильного и умного блоббернаута из фабрики за ERROR ресурсов.
Фабрика станет хрупкой и не сможет производить споры."
+
+/atom/movable/screen/blob/Blobbernaut/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Создать блобернаута ([BLOBMOB_BLOBBERNAUT_RESOURCE_COST])"
+ desc = "Создает сильного и умного блоббернаута из фабрики за [BLOBMOB_BLOBBERNAUT_RESOURCE_COST] ресурсов.
Фабрика станет хрупкой и не сможет производить споры."
/atom/movable/screen/blob/Blobbernaut/Click()
if(isovermind(usr))
@@ -59,64 +60,93 @@
/atom/movable/screen/blob/StorageBlob
icon_state = "ui_storage"
- name = "Produce Storage Blob (40)"
- desc = "Производит хранилище за 40 ресурсов.
Хранилища увеличат ваш максимальный лимит ресурсов на 50."
+ name = "Создать хранилище (ERROR)"
+ desc = "Создает хранилище за ERROR ресурсов.
Хранилища увеличивают ваш максимальный лимит ресурсов на ERROR."
+
+/atom/movable/screen/blob/StorageBlob/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Создать хранилище ([BLOB_STRUCTURE_STORAGE_COST])"
+ desc = "Создает хранилище за [BLOB_STRUCTURE_STORAGE_COST] ресурсов.
Хранилища увеличивают ваш максимальный лимит ресурсов на [BLOB_STORAGE_MAX_POINTS_BONUS]."
/atom/movable/screen/blob/StorageBlob/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
- B.create_storage()
+ B.create_special(BLOB_STRUCTURE_STORAGE_COST, /obj/structure/blob/storage, BLOB_STORAGE_MIN_DISTANCE, TRUE)
/atom/movable/screen/blob/ResourceBlob
icon_state = "ui_resource"
- name = "Produce Resource Blob (40)"
- desc = "Производит ресурсную плитку за 40 ресурсов.
Ресурсные плитки будут приносить вам ресурсы каждые несколько секунд."
+ name = "Создать ресурсную плитку (ERROR)"
+ desc = "Создает ресурсную плитку за ERROR ресурсов.
Ресурсные плитки будут приносить вам ресурсы каждые несколько секунд."
+
+/atom/movable/screen/blob/ResourceBlob/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Создать ресурсную плитку ([BLOB_STRUCTURE_RESOURCE_COST])"
+ desc = "Создает ресурсную плитку за [BLOB_STRUCTURE_RESOURCE_COST] ресурсов.
Ресурсные плитки будут приносить вам ресурсы каждые несколько секунд."
/atom/movable/screen/blob/ResourceBlob/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
- B.create_resource()
+ B.create_special(BLOB_STRUCTURE_RESOURCE_COST, /obj/structure/blob/special/resource, BLOB_RESOURCE_MIN_DISTANCE, TRUE)
/atom/movable/screen/blob/NodeBlob
icon_state = "ui_node"
- name = "Produce Node Blob (60)"
- desc = "Производит узел за 60 ресурсов.
Узлы будут расширяться и активировать ближайшие ресурсные плитки и фабрики."
+ name = "Создать узел (ERROR)"
+ desc = "Создает узел за ERROR ресурсов.
Узлы будут расширяться и активировать ближайшие ресурсные плитки и фабрики."
+
+/atom/movable/screen/blob/NodeBlob/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Создать узел ([BLOB_STRUCTURE_NODE_COST])"
+ desc = "Создает узел за [BLOB_STRUCTURE_NODE_COST] ресурсов.
Узлы будут расширяться и активировать ближайшие ресурсные плитки и фабрики."
/atom/movable/screen/blob/NodeBlob/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
- B.create_node()
+ B.create_special(BLOB_STRUCTURE_NODE_COST, /obj/structure/blob/special/node, BLOB_NODE_MIN_DISTANCE, FALSE)
/atom/movable/screen/blob/FactoryBlob
icon_state = "ui_factory"
- name = "Produce Factory Blob (60)"
- desc = "Производит фабрику за 60 ресурсов.
Фабрики будут производить споры каждые несколько секунд."
+ name = "Создать фабрику (ERROR)"
+ desc = "Производит фабрику за ERROR ресурсов.
Фабрики будут производить споры каждые несколько секунд."
+
+
+/atom/movable/screen/blob/FactoryBlob/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Создать фабрику ([BLOB_STRUCTURE_FACTORY_COST])"
+ desc = "Создает фабрику за [BLOB_STRUCTURE_FACTORY_COST] ресурсов.
Фабрики будут производить споры каждые несколько секунд."
/atom/movable/screen/blob/FactoryBlob/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
- B.create_factory()
+ B.create_special(BLOB_STRUCTURE_FACTORY_COST, /obj/structure/blob/special/factory, BLOB_FACTORY_MIN_DISTANCE, TRUE)
+
-/atom/movable/screen/blob/ReadaptChemical
+/atom/movable/screen/blob/ReadaptStrain
icon_state = "ui_chemswap"
- name = "Readapt Chemical (50)"
- desc = "Случайно изменяет ваш химикат за 50 ресурсов."
+ name = "Реадаптация штамма"
+ desc = "Позволяет вам выбрать новый штамм из случайных вариантов за Error ресурсов."
-/atom/movable/screen/blob/ReadaptChemical/MouseEntered(location,control,params)
+/atom/movable/screen/blob/ReadaptStrain/MouseEntered(location,control,params)
if(hud && hud.mymob && isovermind(hud.mymob))
- name = initial(name)
- desc = initial(desc)
+ var/mob/camera/blob/B = hud.mymob
+ var/cost = (B.free_strain_rerolls)? "FREE" : BLOB_POWER_REROLL_COST
+ name = "[initial(name)] ([cost])"
+ desc = "Позволяет вам выбрать новый штамм из [BLOB_POWER_REROLL_CHOICES] случайных вариантов за [cost] ресурсов."
..()
-/atom/movable/screen/blob/ReadaptChemical/Click()
+/atom/movable/screen/blob/ReadaptStrain/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
- B.chemical_reroll()
+ B.strain_reroll()
/atom/movable/screen/blob/RelocateCore
icon_state = "ui_swap"
- name = "Relocate Core (80)"
- desc = "Меняет местами узел и ваше ядро за 80 ресурсов."
+ name = "Переместить ядро (ERROR)"
+ desc = "Меняет местами узел и ваше ядро за ERROR ресурсов."
+
+/atom/movable/screen/blob/RelocateCore/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Переместить ядро ([BLOB_POWER_RELOCATE_COST])"
+ desc = "Меняет местами узел и ваше ядро за [BLOB_POWER_RELOCATE_COST] ресурсов."
/atom/movable/screen/blob/RelocateCore/Click()
if(isovermind(usr))
@@ -125,9 +155,13 @@
/atom/movable/screen/blob/Split
icon_state = "ui_split"
- name = "Split consciousness (100)"
+ name = "Разделить сознание (ERROR)"
desc = "Создаёт ещё одного блоба на выбранном узле. Может быть использовано 1 раз.
Потомки не могут использовать это умение."
+/atom/movable/screen/blob/Split/Initialize(mapload, datum/hud/hud_owner)
+ . = ..()
+ name = "Разделить сознание ([BLOB_CORE_SPLIT_COST])"
+
/atom/movable/screen/blob/Split/Click()
if(isovermind(usr))
var/mob/camera/blob/B = usr
@@ -146,10 +180,7 @@
SET_PLANE_EXPLICIT(blobpwrdisplay, ABOVE_HUD_PLANE, mymob)
static_inventory += blobpwrdisplay
- blobhealthdisplay = new /atom/movable/screen(null, src)
- blobhealthdisplay.name = "blob health"
- blobhealthdisplay.icon_state = "block"
- blobhealthdisplay.screen_loc = ui_internal
+ blobhealthdisplay = new /atom/movable/screen/healths/blob(null, src)
static_inventory += blobhealthdisplay
using = new /atom/movable/screen/blob/BlobHelp(null, src)
@@ -184,7 +215,7 @@
using.screen_loc = using.screen_loc = ui_lhand
static_inventory += using
- using = new /atom/movable/screen/blob/ReadaptChemical(null, src)
+ using = new /atom/movable/screen/blob/ReadaptStrain(null, src)
using.screen_loc = ui_storage1
static_inventory += using
diff --git a/code/_onclick/hud/blobbernaut.dm b/code/_onclick/hud/blobbernaut.dm
new file mode 100644
index 00000000000..5cb785537a7
--- /dev/null
+++ b/code/_onclick/hud/blobbernaut.dm
@@ -0,0 +1,5 @@
+/datum/hud/simple_animal/blobbernaut/New(mob/living/owner)
+ . = ..()
+
+ blobpwrdisplay = new /atom/movable/screen/healths/blob/overmind(null, src)
+ infodisplay += blobpwrdisplay
diff --git a/code/_onclick/hud/bot.dm b/code/_onclick/hud/bot.dm
index d64965cb136..1c8e2fb65fc 100644
--- a/code/_onclick/hud/bot.dm
+++ b/code/_onclick/hud/bot.dm
@@ -11,9 +11,6 @@
var/mob/living/simple_animal/bot/B = usr
B.Radio.interact(usr)
-/mob/living/simple_animal/bot/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/bot(src)
/datum/hud/bot/New(mob/owner)
..()
diff --git a/code/_onclick/hud/constructs.dm b/code/_onclick/hud/constructs.dm
index 7020061f7e7..1f3aad8e813 100644
--- a/code/_onclick/hud/constructs.dm
+++ b/code/_onclick/hud/constructs.dm
@@ -1,11 +1,3 @@
-/mob/living/simple_animal/hostile/construct/armoured/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/construct/armoured(src)
-
-/mob/living/simple_animal/hostile/construct/behemoth/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/construct/armoured(src)
-
/datum/hud/construct/armoured/New(mob/owner)
..()
mymob.healths = new /atom/movable/screen(null, src)
@@ -15,9 +7,6 @@
mymob.healths.screen_loc = ui_construct_health
infodisplay += mymob.healths
-/mob/living/simple_animal/hostile/construct/builder/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/construct/builder(src)
/datum/hud/construct/builder/New(mob/owner)
..()
@@ -28,9 +17,6 @@
mymob.healths.screen_loc = ui_construct_health
infodisplay += mymob.healths
-/mob/living/simple_animal/hostile/construct/wraith/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/construct/wraith(src)
/datum/hud/construct/wraith/New(mob/owner)
..()
@@ -41,9 +27,6 @@
mymob.healths.screen_loc = ui_construct_health
infodisplay += mymob.healths
-/mob/living/simple_animal/hostile/construct/harvester/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/construct/harvester(src)
/datum/hud/construct/harvester/New(mob/owner)
..()
@@ -54,6 +37,7 @@
mymob.healths.screen_loc = ui_construct_health
infodisplay += mymob.healths
+
/datum/hud/construct/New(mob/owner)
..()
mymob.pullin = new /atom/movable/screen/pull(null, src)
diff --git a/code/_onclick/hud/devil.dm b/code/_onclick/hud/devil.dm
index a7e73e7f456..dd6b7c4cf74 100644
--- a/code/_onclick/hud/devil.dm
+++ b/code/_onclick/hud/devil.dm
@@ -2,11 +2,13 @@
//Soul counter is stored with the humans, it does weird when you place it here apparently...
-/datum/hud/devil/New(mob/owner, ui_style = 'icons/mob/screen_midnight.dmi')
+/datum/hud/devil/New(mob/owner)
..()
var/atom/movable/screen/using
var/atom/movable/screen/inventory/inv_box
+ var/client/client = owner.client
+ var/ui_style = ui_style2icon(client.prefs.UI_style)
using = new /atom/movable/screen/drop(null, src)
using.icon = ui_style
@@ -80,7 +82,3 @@
D.r_hand.screen_loc = null
if(D.l_hand)
D.l_hand.screen_loc = null
-
-/mob/living/carbon/true_devil/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/devil(src, ui_style2icon(client.prefs.UI_style))
diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm
index d38655dd83c..53d6bb41c71 100644
--- a/code/_onclick/hud/ghost.dm
+++ b/code/_onclick/hud/ghost.dm
@@ -1,8 +1,3 @@
-/mob/dead/observer/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/ghost(src)
- SEND_SIGNAL(src, COMSIG_MOB_HUD_CREATED)
-
/atom/movable/screen/ghost
icon = 'icons/mob/screen_ghost.dmi'
diff --git a/code/_onclick/hud/guardian.dm b/code/_onclick/hud/guardian.dm
index 9ec6eed56fd..28732c27674 100644
--- a/code/_onclick/hud/guardian.dm
+++ b/code/_onclick/hud/guardian.dm
@@ -1,7 +1,3 @@
-/mob/living/simple_animal/hostile/guardian/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/guardian(src)
-
/datum/hud/guardian/New(mob/owner)
..()
var/atom/movable/screen/using
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index e4e268cb523..2c62c307d5b 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -209,7 +209,7 @@
/mob/proc/create_mob_hud()
if(!client || hud_used)
return
- hud_used = new /datum/hud(src)
+ hud_used = new hud_type(src)
update_sight()
SEND_SIGNAL(src, COMSIG_MOB_HUD_CREATED)
diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm
index 672ad955ea3..ceaced141e9 100644
--- a/code/_onclick/hud/human.dm
+++ b/code/_onclick/hud/human.dm
@@ -75,18 +75,18 @@
invisibility = INVISIBILITY_ABSTRACT
-/mob/living/carbon/human/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/human(src, ui_style2icon(client.prefs.UI_style), client.prefs.UI_style_color, client.prefs.UI_style_alpha)
-
/datum/hud/human
var/hud_alpha = 255
-/datum/hud/human/New(mob/living/carbon/human/owner, var/ui_style = 'icons/mob/screen_white.dmi', var/ui_color = "#ffffff", var/ui_alpha = 255)
+/datum/hud/human/New(mob/living/carbon/human/owner)
..()
owner.overlay_fullscreen("see_through_darkness", /atom/movable/screen/fullscreen/see_through_darkness)
var/atom/movable/screen/using
var/atom/movable/screen/inventory/inv_box
+ var/client/client = owner.client
+ var/ui_style = ui_style2icon(client.prefs.UI_style)
+ var/ui_color = client.prefs.UI_style_color
+ var/ui_alpha = client.prefs.UI_style_alpha
hud_alpha = ui_alpha
diff --git a/code/_onclick/hud/other_mobs.dm b/code/_onclick/hud/other_mobs.dm
index 8e78a8556aa..5cb0f412dda 100644
--- a/code/_onclick/hud/other_mobs.dm
+++ b/code/_onclick/hud/other_mobs.dm
@@ -1,7 +1,3 @@
-/mob/living/simple_animal/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/simple_animal(src)
-
/datum/hud/simple_animal/New(mob/user)
..()
@@ -14,10 +10,6 @@
static_inventory += using
action_intent = using
-//Ians
-/mob/living/simple_animal/pet/dog/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/corgi(src)
/datum/hud/corgi/New(mob/user)
..()
@@ -31,18 +23,6 @@
mymob.pullin.screen_loc = ui_construct_pull
static_inventory += mymob.pullin
-//spiders
-/mob/living/simple_animal/hostile/poison/giant_spider/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/simple_animal/spider(src)
-
-/mob/living/simple_animal/hostile/poison/terror_spider/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/simple_animal/spider(src)
-
-/mob/living/simple_animal/hostile/retaliate/araneus/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/simple_animal/spider(src)
/datum/hud/simple_animal/spider/New(mob/user)
..()
diff --git a/code/_onclick/hud/robot.dm b/code/_onclick/hud/robot.dm
index 3d635d00664..5686597225f 100644
--- a/code/_onclick/hud/robot.dm
+++ b/code/_onclick/hud/robot.dm
@@ -103,10 +103,6 @@
icon_state = initial(icon_state)
-/mob/living/silicon/robot/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/robot(src)
-
/datum/hud/robot/New(mob/user)
..()
user.overlay_fullscreen("see_through_darkness", /atom/movable/screen/fullscreen/see_through_darkness)
diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm
index dc4298f159c..1012e90ffe2 100644
--- a/code/_onclick/hud/screen_objects.dm
+++ b/code/_onclick/hud/screen_objects.dm
@@ -720,6 +720,18 @@
icon = 'icons/mob/screen_alien.dmi'
screen_loc = ui_alien_health
+/atom/movable/screen/healths/blob
+ name = "blob health"
+ icon_state = "block"
+ screen_loc = ui_internal
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/atom/movable/screen/healths/blob/overmind
+ name = "overmind health"
+ icon = 'icons/hud/blob.dmi'
+ icon_state = "corehealth"
+ screen_loc = ui_blobbernaut_overmind_health
+
/atom/movable/screen/healths/bot
icon = 'icons/mob/screen_bot.dmi'
screen_loc = ui_borg_health
diff --git a/code/_onclick/hud/slime.dm b/code/_onclick/hud/slime.dm
index 06bf2f6e100..4c4dd8a52f4 100644
--- a/code/_onclick/hud/slime.dm
+++ b/code/_onclick/hud/slime.dm
@@ -2,7 +2,3 @@
..()
mymob.healths = new /atom/movable/screen/healths/slime(null, src)
infodisplay += mymob.healths
-
-/mob/living/simple_animal/slime/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/slime(src)
diff --git a/code/_onclick/hud/swarmer.dm b/code/_onclick/hud/swarmer.dm
index 8623476d300..e80258e3fee 100644
--- a/code/_onclick/hud/swarmer.dm
+++ b/code/_onclick/hud/swarmer.dm
@@ -63,9 +63,6 @@
var/mob/living/simple_animal/hostile/swarmer/S = usr
S.ContactSwarmers()
-/mob/living/simple_animal/hostile/swarmer/create_mob_hud()
- if(client && !hud_used)
- hud_used = new /datum/hud/swarmer(src)
/datum/hud/swarmer/New(mob/owner)
..()
diff --git a/code/_onclick/overmind.dm b/code/_onclick/overmind.dm
index 419524c8711..881b3b06410 100644
--- a/code/_onclick/overmind.dm
+++ b/code/_onclick/overmind.dm
@@ -1,7 +1,7 @@
// Blob Overmind Controls
-/mob/camera/blob/ClickOn(var/atom/A, var/params) //Expand blob
+/mob/camera/blob/ClickOn(var/atom/A, var/params, atom/location) //Expand blob
var/list/modifiers = params2list(params)
if(modifiers["middle"])
MiddleClickOn(A)
@@ -17,7 +17,7 @@
return
var/turf/T = get_turf(A)
if(T)
- expand_blob(T)
+ expand_blob(T, location)
/mob/camera/blob/MiddleClickOn(atom/A) //Rally spores
var/turf/T = get_turf(A)
diff --git a/code/controllers/subsystem/fluids.dm b/code/controllers/subsystem/fluids.dm
new file mode 100644
index 00000000000..111e2f3af1b
--- /dev/null
+++ b/code/controllers/subsystem/fluids.dm
@@ -0,0 +1,244 @@
+// Flags indicating what parts of the fluid the subsystem processes.
+/// Indicates that a fluid subsystem processes fluid spreading.
+#define SS_PROCESSES_SPREADING (1<<0)
+/// Indicates that a fluid subsystem processes fluid effects.
+#define SS_PROCESSES_EFFECTS (1<<1)
+
+/**
+ * # Fluid Subsystem
+ *
+ * A subsystem that processes the propagation and effects of a particular fluid.
+ *
+ * Both fluid spread and effect processing are handled through a carousel system.
+ * Fluids being spread and fluids being processed are organized into buckets.
+ * Each fresh (non-resumed) fire one bucket of each is selected to be processed.
+ * These selected buckets are then fully processed.
+ * The next fresh fire selects the next bucket in each set for processing.
+ * If this would walk off the end of a carousel list we wrap back to the first element.
+ * This effectively makes each set a circular list, hence a carousel.
+ */
+SUBSYSTEM_DEF(fluids)
+ name = "Fluid"
+ wait = 0 // Will be autoset to whatever makes the most sense given the spread and effect waits.
+ flags = SS_KEEP_TIMING
+ runlevels = RUNLEVEL_GAME|RUNLEVEL_POSTGAME
+ priority = FIRE_PRIORITY_FLUIDS
+
+ // Fluid spread processing:
+ /// The amount of time (in deciseconds) before a fluid node is created and when it spreads.
+ var/spread_wait = 1 SECONDS
+ /// The number of buckets in the spread carousel.
+ var/num_spread_buckets
+ /// The set of buckets containing fluid nodes to spread.
+ var/list/spread_carousel
+ /// The index of the spread carousel bucket currently being processed.
+ var/spread_bucket_index
+ /// The set of fluid nodes we are currently processing spreading for.
+ var/list/currently_spreading
+
+ // Fluid effect processing:
+ /// The amount of time (in deciseconds) between effect processing ticks for each fluid node.
+ var/effect_wait = 1 SECONDS
+ /// The number of buckets in the effect carousel.
+ var/num_effect_buckets
+ /// The set of buckets containing fluid nodes to process effects for.
+ var/list/effect_carousel
+ /// The index of the currently processing bucket on the effect carousel.
+ var/effect_bucket_index
+ /// The set of fluid nodes we are currently processing effects for.
+ var/list/currently_processing
+
+/datum/controller/subsystem/fluids/Initialize()
+ initialize_waits()
+ initialize_spread_carousel()
+ initialize_effect_carousel()
+ return SS_INIT_SUCCESS
+
+/**
+ * Initializes the subsystem waits.
+ *
+ * Ensures that the subsystem's fire wait evenly splits the spread and effect waits.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_waits()
+ if(spread_wait <= 0)
+ WARNING("[src] has the invalid spread wait [spread_wait].")
+ spread_wait = 1 SECONDS
+ if(effect_wait <= 0)
+ WARNING("[src] has the invalid effect wait [effect_wait].")
+ spread_wait = 1 SECONDS
+
+ // Sets the overall wait of the subsystem to evenly divide both the effect and spread waits.
+ var/max_wait = Gcd(spread_wait, effect_wait)
+ if(max_wait < wait || wait <= 0)
+ wait = max_wait
+ else
+ // If the wait of the subsystem overall is set to a valid value make the actual wait of the subsystem evenly divide that as well.
+ // Makes effect bubbling possible with identical spread and effect waits.
+ wait = Gcd(wait, max_wait)
+
+
+/**
+ * Initializes the carousel used to process fluid spreading.
+ *
+ * Synchronizes the spread delta time with the actual target spread tick rate.
+ * Builds the carousel buckets used to queue spreads.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_spread_carousel()
+ // Make absolutely certain that the spread wait is in sync with the target spread tick rate.
+ num_spread_buckets = round(spread_wait / wait)
+ spread_wait = wait * num_spread_buckets
+
+ spread_carousel = list()
+ spread_carousel.len = num_spread_buckets
+ for(var/i in 1 to num_spread_buckets)
+ spread_carousel[i] = list()
+ currently_spreading = list()
+ spread_bucket_index = 1
+
+/**
+ * Initializes the carousel used to process fluid effects.
+ *
+ * Synchronizes the spread delta time with the actual target spread tick rate.
+ * Builds the carousel buckets used to bubble processing.
+ */
+/datum/controller/subsystem/fluids/proc/initialize_effect_carousel()
+ // Make absolutely certain that the effect wait is in sync with the target effect tick rate.
+ num_effect_buckets = round(effect_wait / wait)
+ effect_wait = wait * num_effect_buckets
+
+ effect_carousel = list()
+ effect_carousel.len = num_effect_buckets
+ for(var/i in 1 to num_effect_buckets)
+ effect_carousel[i] = list()
+ currently_processing = list()
+ effect_bucket_index = 1
+
+
+/datum/controller/subsystem/fluids/fire(resumed)
+ var/seconds_per_tick
+ var/cached_bucket_index
+ var/list/obj/effect/particle_effect/fluid/currentrun
+ // Ok so like I get the lighting style splittick but why are we doing this churn thing
+ // It seems like a bad idea for processing to get out of step with spreading
+ MC_SPLIT_TICK_INIT(2)
+
+ MC_SPLIT_TICK // Start processing fluid spread (we take a lot of cpu for ourselves, spreading is more important after all)
+ if(!resumed)
+ spread_bucket_index = WRAP_UP(spread_bucket_index, num_spread_buckets)
+ currently_spreading = spread_carousel[spread_bucket_index]
+ spread_carousel[spread_bucket_index] = list() // Reset the bucket so we don't process an _entire station's worth of foam_ spreading every 2 ticks when the foam flood event happens.
+
+ seconds_per_tick = spread_wait / (1 SECONDS)
+ currentrun = currently_spreading
+ while(currentrun.len)
+ var/obj/effect/particle_effect/fluid/to_spread = currentrun[currentrun.len]
+ currentrun.len--
+
+ if(!QDELETED(to_spread))
+ to_spread.spread(seconds_per_tick)
+ to_spread.spread_bucket = null
+
+ if(MC_TICK_CHECK)
+ break
+
+ MC_SPLIT_TICK // Start processing fluid effects:
+ if(!resumed)
+ effect_bucket_index = WRAP_UP(effect_bucket_index, num_effect_buckets)
+ var/list/tmp_list = effect_carousel[effect_bucket_index]
+ currently_processing = tmp_list.Copy()
+
+ seconds_per_tick = effect_wait / (1 SECONDS)
+ cached_bucket_index = effect_bucket_index
+ currentrun = currently_processing
+ while(currentrun.len)
+ var/obj/effect/particle_effect/fluid/to_process = currentrun[currentrun.len]
+ currentrun.len--
+
+ if(QDELETED(to_process) || to_process.process(seconds_per_tick) == PROCESS_KILL)
+ effect_carousel[cached_bucket_index] -= to_process
+ to_process.effect_bucket = null
+ to_process.datum_flags &= ~DF_ISPROCESSING
+
+ if(MC_TICK_CHECK)
+ break
+
+/**
+ * Queues a fluid node to spread later after one full carousel rotation.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to queue to spread.
+ */
+/datum/controller/subsystem/fluids/proc/queue_spread(obj/effect/particle_effect/fluid/node)
+ if(node.spread_bucket)
+ return
+
+ spread_carousel[spread_bucket_index] += node
+ node.spread_bucket = spread_bucket_index
+
+/**
+ * Cancels a queued spread of a fluid node.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to cancel the spread of.
+ */
+/datum/controller/subsystem/fluids/proc/cancel_spread(obj/effect/particle_effect/fluid/node)
+ if(!node.spread_bucket)
+ return
+
+ var/bucket_index = node.spread_bucket
+ spread_carousel[bucket_index] -= node
+ if(bucket_index == spread_bucket_index)
+ currently_spreading -= node
+
+ node.spread_bucket = null
+
+/**
+ * Starts processing the effects of a fluid node.
+ *
+ * The fluid node will next process after one full bucket rotation.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to start processing.
+ */
+/datum/controller/subsystem/fluids/proc/start_processing(obj/effect/particle_effect/fluid/node)
+ if(node.datum_flags & DF_ISPROCESSING || node.effect_bucket)
+ return
+
+ // Edit this value to make all fluids process effects (at the same time|offset by when they started processing| -> offset by a random amount <- )
+ var/bucket_index = rand(1, num_effect_buckets)
+ effect_carousel[bucket_index] += node
+ node.effect_bucket = bucket_index
+ node.datum_flags |= DF_ISPROCESSING
+
+/**
+ * Stops processing the effects of a fluid node.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The node to stop processing.
+ */
+/datum/controller/subsystem/fluids/proc/stop_processing(obj/effect/particle_effect/fluid/node)
+ if(!(node.datum_flags & DF_ISPROCESSING))
+ return
+
+ var/bucket_index = node.effect_bucket
+ if(!bucket_index)
+ return
+
+ effect_carousel[bucket_index] -= node
+ if(bucket_index == effect_bucket_index)
+ currently_processing -= node
+
+ node.effect_bucket = null
+ node.datum_flags &= ~DF_ISPROCESSING
+
+#undef SS_PROCESSES_SPREADING
+#undef SS_PROCESSES_EFFECTS
+
+
+// Subtypes:
+
+/// The subsystem responsible for processing smoke propagation and effects.
+FLUID_SUBSYSTEM_DEF(smoke)
+ name = "Smoke"
+ spread_wait = 0.1 SECONDS
+ effect_wait = 2.0 SECONDS
diff --git a/code/datums/components/blob_minion.dm b/code/datums/components/blob_minion.dm
new file mode 100644
index 00000000000..dcbf7b274b2
--- /dev/null
+++ b/code/datums/components/blob_minion.dm
@@ -0,0 +1,157 @@
+/**
+ * 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(!isminion(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))
+ living_parent.stop_pulling()
+ 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_MOB_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))
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_BLOB_ACT,
+ COMSIG_ATOM_FIRE_ACT,
+ COMSIG_ATOM_TRIED_PASS,
+ COMSIG_ATOM_UPDATE_ICON,
+ COMSIG_MOB_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) || new_mind.has_antag_datum(/datum/antagonist/blob_minion))
+ return
+
+ var/datum_type = (isblobbernaut(minion))? /datum/antagonist/blob_minion/blobernaut : /datum/antagonist/blob_minion
+ var/datum/antagonist/blob_minion/minion_motive = new datum_type(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 += list(list("Критическая Масса:", "[TOTAL_BLOB_MASS]/[NEEDED_BLOB_MASS]"))
+
+/// 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)
+ if(minion.on_fire)
+ minion.adjust_fire_stacks(-1)
+ 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], [message]")
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, 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/components/blob_turf_consuming.dm b/code/datums/components/blob_turf_consuming.dm
new file mode 100644
index 00000000000..7ad814047d8
--- /dev/null
+++ b/code/datums/components/blob_turf_consuming.dm
@@ -0,0 +1,34 @@
+/datum/component/blob_turf_consuming
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Number of attempts of consume neede for consume
+ var/consumes_needed = 0
+ /// Total number of attempts of consume
+ var/total_consumes = 0
+
+/datum/component/blob_turf_consuming/Initialize(_consumes_needed)
+ if(!isturf(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ consumes_needed = _consumes_needed
+
+/datum/component/blob_turf_consuming/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_TRY_CONSUME_TURF, PROC_REF(on_try_consume))
+
+/datum/component/blob_turf_consuming/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_TRY_CONSUME_TURF)
+
+
+/datum/component/blob_turf_consuming/InheritComponent(datum/component/blob_turf_consuming/new_comp , i_am_original, _consumes_needed)
+ if(new_comp)
+ consumes_needed = new_comp.consumes_needed
+ else
+ consumes_needed = _consumes_needed
+
+
+/datum/component/blob_turf_consuming/proc/on_try_consume()
+ total_consumes++
+ if(total_consumes >= consumes_needed)
+ var/turf/total_turf = parent
+ total_turf.blob_consume()
+ return
+ return COMPONENT_CANT_CONSUME
diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm
index 7884935d3c1..68fc4b3f348 100644
--- a/code/datums/components/chasm.dm
+++ b/code/datums/components/chasm.dm
@@ -174,6 +174,9 @@
return // We're already handling this
if(below_turf)
+ if(HAS_TRAIT(dropped_thing, TRAIT_CHASM_DESTROYED))
+ qdel(dropped_thing)
+ return
// send to the turf below
dropped_thing.visible_message(span_boldwarning("[dropped_thing] falls into [atom_parent]!"), span_userdanger("[fall_message]"))
below_turf.visible_message(span_boldwarning("[dropped_thing] falls from above!"))
@@ -215,6 +218,10 @@
if(QDELETED(dropped_thing))
return
+ if(HAS_TRAIT(dropped_thing, TRAIT_CHASM_DESTROYED))
+ qdel(dropped_thing)
+ return
+
if(isrobot(dropped_thing))
var/mob/living/silicon/robot/robot = dropped_thing
qdel(robot.mmi)
diff --git a/code/datums/components/connect_containers.dm b/code/datums/components/connect_containers.dm
new file mode 100644
index 00000000000..6f793c860e1
--- /dev/null
+++ b/code/datums/components/connect_containers.dm
@@ -0,0 +1,68 @@
+/// This component behaves similar to connect_loc_behalf, but it's nested and hooks a signal onto all MOVABLES containing this atom.
+/datum/component/connect_containers
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+
+ /// An assoc list of signal -> procpath to register to the loc this object is on.
+ var/list/connections
+ /**
+ * The atom the component is tracking. The component will delete itself if the tracked is deleted.
+ * Signals will also be updated whenever it moves.
+ */
+ var/atom/movable/tracked
+
+/datum/component/connect_containers/Initialize(atom/movable/tracked, list/connections)
+ . = ..()
+ if(!ismovable(tracked))
+ return COMPONENT_INCOMPATIBLE
+
+ src.connections = connections
+ set_tracked(tracked)
+
+/datum/component/connect_containers/Destroy()
+ set_tracked(null)
+ return ..()
+
+/datum/component/connect_containers/InheritComponent(datum/component/component, original, atom/movable/tracked, list/connections)
+ // Not equivalent. Checks if they are not the same list via shallow comparison.
+ if(!compare_list(src.connections, connections))
+ stack_trace("connect_containers component attached to [parent] tried to inherit another connect_containers component with different connections")
+ return
+ if(src.tracked != tracked)
+ set_tracked(tracked)
+
+/datum/component/connect_containers/proc/set_tracked(atom/movable/new_tracked)
+ if(tracked)
+ UnregisterSignal(tracked, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
+ unregister_signals(tracked.loc)
+ tracked = new_tracked
+ if(!tracked)
+ return
+ RegisterSignal(tracked, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ RegisterSignal(tracked, COMSIG_QDELETING, PROC_REF(handle_tracked_qdel))
+ update_signals(tracked)
+
+/datum/component/connect_containers/proc/handle_tracked_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/connect_containers/proc/update_signals(atom/movable/listener)
+ if(!ismovable(listener.loc))
+ return
+
+ for(var/atom/movable/container as anything in get_nested_locs(listener))
+ RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved))
+ for(var/signal in connections)
+ parent.RegisterSignal(container, signal, connections[signal])
+
+/datum/component/connect_containers/proc/unregister_signals(atom/movable/location)
+ if(!ismovable(location))
+ return
+
+ for(var/atom/movable/target as anything in (get_nested_locs(location) + location))
+ UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
+ parent.UnregisterSignal(target, connections)
+
+/datum/component/connect_containers/proc/on_moved(atom/movable/listener, atom/old_loc)
+ SIGNAL_HANDLER
+ unregister_signals(old_loc)
+ update_signals(listener)
diff --git a/code/datums/components/ghost_direct_control.dm b/code/datums/components/ghost_direct_control.dm
new file mode 100644
index 00000000000..6cd966cdcb1
--- /dev/null
+++ b/code/datums/components/ghost_direct_control.dm
@@ -0,0 +1,156 @@
+/**
+ * Component which lets ghosts click on a mob to take control of it
+ */
+/datum/component/ghost_direct_control
+ /// Message to display upon successful possession
+ var/assumed_control_message
+ /// Type of ban you can get to prevent you from accepting this role
+ var/ban_type
+ /// Any extra checks which need to run before we take over
+ var/datum/callback/extra_control_checks
+ /// Callback run after someone successfully takes over the body
+ var/datum/callback/after_assumed_control
+ /// If we're currently awaiting the results of a ghost poll
+ var/awaiting_ghosts = FALSE
+
+/datum/component/ghost_direct_control/Initialize(
+ ban_type = ROLE_SENTIENT,
+ role_name = null,
+ poll_question = null,
+ poll_candidates = TRUE,
+ antag_age_check = TRUE,
+ check_antaghud = TRUE,
+ poll_length = 10 SECONDS,
+ assumed_control_message = null,
+ datum/callback/extra_control_checks,
+ datum/callback/after_assumed_control,
+)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.ban_type = ban_type
+ src.assumed_control_message = assumed_control_message || "You are [parent]!"
+ src.extra_control_checks = extra_control_checks
+ src.after_assumed_control = after_assumed_control
+
+ var/mob/mob_parent = parent
+ LAZYADD(GLOB.mob_spawners[format_text("[initial(mob_parent.name)]")], mob_parent)
+
+ if(poll_candidates)
+ INVOKE_ASYNC(src, PROC_REF(request_ghost_control), poll_question, role_name || "[parent]", poll_length, antag_age_check, check_antaghud)
+
+/datum/component/ghost_direct_control/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_ATOM_ATTACK_GHOST, PROC_REF(on_ghost_clicked))
+ RegisterSignal(parent, COMSIG_LIVING_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_login))
+ RegisterSignal(parent, COMSIG_IS_GHOST_CONTROLABLE, PROC_REF(on_ghost_controlable_check))
+
+/datum/component/ghost_direct_control/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ATOM_ATTACK_GHOST, COMSIG_LIVING_EXAMINE, COMSIG_MOB_LOGIN))
+ return ..()
+
+/datum/component/ghost_direct_control/Destroy(force)
+ extra_control_checks = null
+ after_assumed_control = null
+
+ var/mob/mob_parent = parent
+ var/list/spawners = GLOB.mob_spawners[format_text("[initial(mob_parent.name)]")]
+ LAZYREMOVE(spawners, mob_parent)
+ if(!LAZYLEN(spawners))
+ GLOB.mob_spawners -= format_text("[initial(mob_parent.name)]")
+ return ..()
+
+/// Inform ghosts that they can possess this
+/datum/component/ghost_direct_control/proc/on_examined(datum/source, mob/user, list/examine_text)
+ SIGNAL_HANDLER
+ if(!isobserver(user))
+ return
+ var/mob/living/our_mob = parent
+ if(our_mob.stat == DEAD || our_mob.key || awaiting_ghosts)
+ return
+ examine_text += span_boldnotice("You could take control of this mob by clicking on it.")
+
+/// Send out a request for a brain
+/datum/component/ghost_direct_control/proc/request_ghost_control(poll_question, role_name, poll_length, age_check, check_ahud)
+ awaiting_ghosts = TRUE
+ var/list/possible_ghosts = SSghost_spawns.poll_candidates(
+ question = poll_question,
+ role = ban_type,
+ poll_time = poll_length,
+ antag_age_check = age_check,
+ check_antaghud = check_ahud,
+ source = parent,
+ role_cleanname = role_name
+ )
+ var/mob/chosen_one = (possible_ghosts.len)? pick(possible_ghosts): null
+ awaiting_ghosts = FALSE
+ if(isnull(chosen_one))
+ return
+ assume_direct_control(chosen_one)
+
+/// A ghost clicked on us, they want to get in this body
+/datum/component/ghost_direct_control/proc/on_ghost_clicked(mob/our_mob, mob/dead/observer/hopeful_ghost)
+ SIGNAL_HANDLER
+ if(our_mob.key)
+ qdel(src)
+ return
+ if(!hopeful_ghost.client)
+ return
+ if(awaiting_ghosts)
+ to_chat(hopeful_ghost, span_warning("Ghost candidate selection currently in progress!"))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ if(!SSticker.HasRoundStarted())
+ to_chat(hopeful_ghost, span_warning("You cannot assume control of this until after the round has started!"))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+ INVOKE_ASYNC(src, PROC_REF(attempt_possession), our_mob, hopeful_ghost)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// We got far enough to establish that this mob is a valid target, let's try to posssess it
+/datum/component/ghost_direct_control/proc/attempt_possession(mob/our_mob, mob/dead/observer/hopeful_ghost)
+ var/ghost_asked = tgui_alert(usr, "Become [our_mob]?", "Are you sure?", list("Yes", "No"))
+ if(ghost_asked != "Yes" || QDELETED(our_mob))
+ return
+ assume_direct_control(hopeful_ghost)
+
+/// Grant possession of our mob, component is now no longer required
+/datum/component/ghost_direct_control/proc/assume_direct_control(mob/harbinger)
+ if(QDELETED(src))
+ to_chat(harbinger, span_warning("Offer to possess creature has expired!"))
+ return
+ if(jobban_isbanned(harbinger, ban_type))
+ to_chat(harbinger, span_warning("You are banned from playing as this role!"))
+ return
+ var/mob/living/new_body = parent
+ if(new_body.stat == DEAD)
+ to_chat(harbinger, span_warning("This body has passed away, it is of no use!"))
+ return
+ if(new_body.key)
+ to_chat(harbinger, span_warning("[parent] has already become sapient!"))
+ qdel(src)
+ return
+ if(extra_control_checks && !extra_control_checks.Invoke(harbinger))
+ return
+
+ add_game_logs("took control of [new_body].", harbinger)
+ // doesn't transfer mind because that transfers antag datum as well
+ new_body.key = harbinger.key
+
+ // Already qdels due to below proc but just in case
+ qdel(src)
+
+/// When someone assumes control, get rid of our component
+/datum/component/ghost_direct_control/proc/on_login(mob/harbinger)
+ SIGNAL_HANDLER
+ // This proc is called the very moment .key is set, so we need to force mind to initialize here if we want the invoke to affect the mind of the mob
+ if(isnull(harbinger.mind))
+ harbinger.mind_initialize()
+ to_chat(harbinger, span_boldnotice(assumed_control_message))
+ after_assumed_control?.Invoke(harbinger)
+ qdel(src)
+
+
+/datum/component/ghost_direct_control/proc/on_ghost_controlable_check(mob/user)
+ SIGNAL_HANDLER
+ return COMPONENT_GHOST_CONTROLABLE
diff --git a/code/datums/components/stationloving.dm b/code/datums/components/stationloving.dm
new file mode 100644
index 00000000000..d5cdfe0078d
--- /dev/null
+++ b/code/datums/components/stationloving.dm
@@ -0,0 +1,173 @@
+/// Teleports the movable atom back to a safe turf on the station if it leaves the z-level or becomes inaccessible.
+/datum/component/stationloving
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// If TRUE, notifies admins when parent is teleported back to the station.
+ var/inform_admins = FALSE
+ var/disallow_soul_imbue = TRUE
+ /// If FALSE, prevents parent from being qdel'd unless it's a force = TRUE qdel.
+ var/allow_item_destruction = FALSE
+
+/datum/component/stationloving/Initialize(inform_admins = FALSE, allow_item_destruction = FALSE)
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.inform_admins = inform_admins
+ src.allow_item_destruction = allow_item_destruction
+
+ // Just in case something is being created outside of station/centcom
+ if(!atom_in_bounds(parent))
+ relocate()
+
+/datum/component/stationloving/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_PREQDELETED, PROC_REF(on_parent_pre_qdeleted))
+ RegisterSignal(parent, COMSIG_ITEM_MARK_RETRIEVAL, PROC_REF(check_mark_retrieval))
+ // Relocate when we become unreachable
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_parent_moved))
+ // Relocate when our loc, or any of our loc's locs, becomes unreachable
+ var/static/list/loc_connections = list(
+ COMSIG_MOVABLE_MOVED = PROC_REF(on_parent_moved),
+ SIGNAL_ADDTRAIT(TRAIT_SECLUDED_LOCATION) = PROC_REF(on_loc_secluded),
+ )
+ AddComponent(/datum/component/connect_containers, parent, loc_connections)
+
+/datum/component/stationloving/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_MOVABLE_Z_CHANGED,
+ COMSIG_PREQDELETED,
+ COMSIG_ITEM_IMBUE_SOUL,
+ COMSIG_ITEM_MARK_RETRIEVAL,
+ COMSIG_MOVABLE_MOVED,
+ ))
+
+ qdel(GetComponent(/datum/component/connect_containers))
+
+/datum/component/stationloving/InheritComponent(datum/component/stationloving/newc, original, inform_admins, allow_death)
+ if(original)
+ if(newc)
+ inform_admins = newc.inform_admins
+ allow_death = newc.allow_item_destruction
+ else
+ inform_admins = inform_admins
+
+/// Teleports parent to a safe turf on the station z-level.
+/datum/component/stationloving/proc/relocate()
+
+ var/target_turf = find_safe_turf() //Fallback. Mostly for debug maps.
+
+ if(!target_turf)
+ if(GLOB.blobstart.len > 0)
+ target_turf = get_turf(pick(GLOB.blobstart))
+ else
+ CRASH("Unable to find a blobstart landmark for [type] to relocate [parent].")
+
+ var/atom/movable/movable_parent = parent
+ playsound(movable_parent, 'sound/machines/synth_no.ogg', 5, TRUE)
+
+ var/mob/holder = get(movable_parent, /mob)
+ if(holder)
+ to_chat(holder, span_danger("You can't help but feel that you just lost something back there..."))
+ holder.temporarily_remove_item_from_inventory(parent, TRUE) // prevents ghost diskie
+
+ movable_parent.forceMove(target_turf)
+
+ return target_turf
+
+/// Signal proc for [COMSIG_MOVABLE_MOVED], called when our parent moves, or our parent's loc, or our parent's loc loc...
+/// To check if our disk is moving somewhere it shouldn't be, such as off Z level, or into an invalid area
+/datum/component/stationloving/proc/on_parent_moved(atom/movable/source, turf/old_turf)
+ SIGNAL_HANDLER
+
+ if(atom_in_bounds(source))
+ return
+
+ var/turf/current_turf = get_turf(source)
+ var/turf/new_destination = relocate()
+ // Our turf actually didn't change, so it's more likely we became secluded
+ if(current_turf == old_turf)
+ log_game("[parent] moved out of bounds at [loc_name(current_turf)], becoming inaccessible / secluded. \
+ Moving it to [loc_name(new_destination)].")
+
+ if(inform_admins)
+ message_admins("[parent] moved out of bounds at [ADMIN_VERBOSEJMP(current_turf)], becoming inaccessible / secluded. \
+ Moving it to [ADMIN_VERBOSEJMP(new_destination)].")
+
+ // Our locs changes, we did in fact move somewhere
+ else
+ log_game("[parent] attempted to be moved out of bounds from [loc_name(old_turf)] \
+ to [loc_name(current_turf)]. Moving it to [loc_name(new_destination)].")
+
+ if(inform_admins)
+ message_admins("[parent] attempted to be moved out of bounds from [ADMIN_VERBOSEJMP(old_turf)] \
+ to [ADMIN_VERBOSEJMP(current_turf)]. Moving it to [ADMIN_VERBOSEJMP(new_destination)].")
+
+/// Signal proc for [SIGNAL_ADDTRAIT], via [TRAIT_SECLUDED_LOCATION] on our locs, to ensure nothing funky happens
+/datum/component/stationloving/proc/on_loc_secluded(atom/movable/source)
+ SIGNAL_HANDLER
+
+ var/turf/new_destination = relocate()
+ log_game("[parent] moved out of bounds at [loc_name(source)], becoming inaccessible / secluded. \
+ Moving it to [loc_name(new_destination)].")
+
+ if(inform_admins)
+ message_admins("[parent] moved out of bounds at [ADMIN_VERBOSEJMP(source)], becoming inaccessible / secluded. \
+ Moving it to [ADMIN_VERBOSEJMP(new_destination)].")
+
+/datum/component/stationloving/proc/check_mark_retrieval(datum/source)
+ SIGNAL_HANDLER
+
+ return COMPONENT_BLOCK_MARK_RETRIEVAL
+
+/// Checks whether a given atom's turf is within bounds. Returns TRUE if it is, FALSE if it isn't.
+/datum/component/stationloving/proc/atom_in_bounds(atom/atom_to_check)
+ // Typecache of shuttles that we allow the disk to stay on
+ var/static/list/allowed_shuttles = typecacheof(list(
+ /area/shuttle/syndicate,
+ /area/shuttle/escape,
+ /area/shuttle/pod_1,
+ /area/shuttle/pod_2,
+ /area/shuttle/pod_3,
+ /area/shuttle/pod_4,
+ ))
+ // Typecache of areas on the centcom Z-level that we allow the disk to stay on
+ var/static/list/disallowed_centcom_areas = typecacheof(list(
+ /area/abductor_ship
+ ))
+
+ // Our loc is a secluded location = not in bounds
+ if(atom_to_check.loc && HAS_TRAIT(atom_to_check.loc, TRAIT_SECLUDED_LOCATION))
+ return FALSE
+ // No turf below us = nullspace = not in bounds
+ var/turf/destination_turf = get_turf(atom_to_check)
+ if(!destination_turf)
+ return FALSE
+ if(is_station_level(destination_turf.z))
+ return TRUE
+ if(atom_to_check.onSyndieBase())
+ return TRUE
+
+ var/area/destination_area = destination_turf.loc
+ if(is_admin_level(destination_turf.z))
+ if(is_type_in_typecache(destination_area, disallowed_centcom_areas))
+ return FALSE
+ return TRUE
+ return FALSE
+
+/// Signal handler for before the parent is qdel'd. Can prevent the parent from being deleted where allow_item_destruction is FALSE and force is FALSE.
+/datum/component/stationloving/proc/on_parent_pre_qdeleted(datum/source, force)
+ SIGNAL_HANDLER
+
+ var/turf/current_turf = get_turf(parent)
+
+ if(force && inform_admins)
+ message_admins("[parent] has been !!force deleted!! in [ADMIN_VERBOSEJMP(current_turf)].")
+ log_game("[parent] has been !!force deleted!! in [loc_name(current_turf)].")
+
+ if(force || allow_item_destruction)
+ return FALSE
+
+ var/turf/new_turf = relocate()
+ log_game("[parent] has been destroyed in [loc_name(current_turf)]. \
+ Preventing destruction and moving it to [loc_name(new_turf)].")
+ if(inform_admins)
+ message_admins("[parent] has been destroyed in [ADMIN_VERBOSEJMP(current_turf)]. \
+ Preventing destruction and moving it to [ADMIN_VERBOSEJMP(new_turf)].")
+ return TRUE
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index a6de1c0034c..b840e4e4ffb 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -542,8 +542,13 @@
. += "|burst blob"
else if(isblobovermind(src))
var/mob/camera/blob/blob_overmind = current
- . += "|BLOB Overmind|"
- . += "
Total points: [blob_overmind.blob_points]/[blob_overmind.max_blob_points]"
+ if(istype(blob_overmind))
+ . += "|BLOB Overmind|"
+ . += "
Total points: [blob_overmind.blob_points]/[blob_overmind.max_blob_points]"
+ . += "
Infinity points: [(blob_overmind.is_infinity)? "ON" : "OFF"]"
+ . += "
Blob strain: [blob_overmind.blobstrain? "[blob_overmind?.blobstrain?.name]" : "None"]"
+ else if(isblobminion(src))
+ . += "|BLOB Minion|"
else if(current.can_be_blob())
. += "blobize|NO"
. += _memory_edit_role_enabled(ROLE_BLOB)
@@ -2436,9 +2441,9 @@
add_conversion_logs(current, "De-blobed")
if("blob")
- var/burst_time = input(usr, "Введите время до вылупления","Time:", TIME_TO_BURST_ADDED_HIGHT) as num|null
- var/need_new_blob = alert(usr,"Нужно ли выбирать блоба из экипажа в случае попытки вылупления за пределами станции?", "", "Да", "Нет") == "Нет"
- var/start_process = alert(usr,"Начинать отсчет до момента вылупления?", "", "Да", "Нет") == "Да"
+ var/burst_time = tgui_input_number(usr, "Введите время до вылупления","Time:", TIME_TO_BURST_ADDED_HIGHT)
+ var/need_new_blob = tgui_alert(usr, "Нужно ли выбирать блоба из экипажа в случае попытки вылупления за пределами станции?", "", list("Да", "Нет")) == "Нет"
+ var/start_process = tgui_alert(usr,"Начинать отсчет до момента вылупления?", "", list("Да", "Нет")) == "Да"
if(isnull(burst_time) || QDELETED(current) || current.stat == DEAD)
return
var/datum_type = get_blob_infected_type()
@@ -2452,9 +2457,9 @@
message_admins("[key_name_admin(usr)] has made [key_name_admin(current)] into a \"Blob\"")
if("burst")
- var/warn_blob = alert(usr,"Предупреждать блоба при попытке вылупления за пределами станции?", "", "Да", "Нет") != "Да"
- var/need_new_blob = alert(usr,"Нужно ли выбирать блоба из экипажа в случае попытки вылупления за пределами станции?", "", "Да", "Нет") == "Да"
- if(alert(usr,"Вы действительно хотите лопнуть блоба? Это уничтожит персонажа игрока и превратит его в блоба.", "", "Да", "Нет") == "Да")
+ var/warn_blob = tgui_alert(usr,"Предупреждать блоба при попытке вылупления за пределами станции?", "", list("Да", "Нет")) != "Да"
+ var/need_new_blob = tgui_alert(usr,"Нужно ли выбирать блоба из экипажа в случае попытки вылупления за пределами станции?", "", list("Да", "Нет")) == "Да"
+ if(tgui_alert(usr,"Вы действительно хотите лопнуть блоба? Это уничтожит персонажа игрока и превратит его в блоба.", "", list("Да", "Нет")) == "Да")
var/datum/antagonist/blob_infected/blob = has_antag_datum(/datum/antagonist/blob_infected)
if(!blob)
return
@@ -2468,13 +2473,34 @@
if(!isblobovermind(src))
return
var/mob/camera/blob/blob_overmind = current
- var/blob_points = input(usr, "Введите новое число очков в диапазоне от 0 до [blob_overmind.max_blob_points]","Count:", blob_overmind.blob_points) as num|null
+ var/blob_points = tgui_input_number(usr, "Введите новое число очков в диапазоне от 0 до [blob_overmind.max_blob_points]", "Count:", blob_overmind.blob_points, blob_overmind.max_blob_points, 0)
if(isnull(blob_points) || QDELETED(current) || current.stat == DEAD)
return
blob_overmind.blob_points = clamp(blob_points, 0, blob_overmind.max_blob_points)
log_admin("[key_name(usr)] set blob points to [key_name(current)] as [blob_overmind.blob_points]")
message_admins("[key_name_admin(usr)] set blob points to [key_name_admin(current)] as [blob_overmind.blob_points]")
+ if("inf_points")
+ if(!isblobovermind(src))
+ return
+ var/mob/camera/blob/blob_overmind = current
+ if(QDELETED(current) || current.stat == DEAD)
+ return
+ blob_overmind.is_infinity = !blob_overmind.is_infinity
+ log_admin("[key_name(usr)] make blob points [blob_overmind.is_infinity? "infinity" : "not infinity"] to [key_name(current)]")
+ message_admins("[key_name_admin(usr)] make blob points [blob_overmind.is_infinity? "infinity" : "not infinity"] to [key_name_admin(current)]")
+
+ if("select_strain")
+ if(!isblobovermind(src))
+ return
+ var/mob/camera/blob/blob_overmind = current
+ if(QDELETED(current) || current.stat == DEAD)
+ return
+ var/strain = tgui_input_list(usr, "Выберите штамм", "Выбор штамма", GLOB.valid_blobstrains, null)
+ if(ispath(strain))
+ blob_overmind.set_strain(strain)
+ log_admin("[key_name(usr)] changed the strain to [strain] for [key_name(current)]")
+ message_admins("[key_name_admin(usr)] changed the strain to [strain] for [key_name_admin(current)]")
else if(href_list["common"])
switch(href_list["common"])
@@ -3095,6 +3121,7 @@
if(!mind.name)
mind.name = real_name
mind.current = src
+ SEND_SIGNAL(src, COMSIG_MOB_MIND_INITIALIZED, mind)
//HUMAN
/mob/living/carbon/human/mind_initialize()
diff --git a/code/datums/spawners_menu.dm b/code/datums/spawners_menu.dm
index 934ef447204..daac771c9f0 100644
--- a/code/datums/spawners_menu.dm
+++ b/code/datums/spawners_menu.dm
@@ -47,7 +47,10 @@
if(!length(possible_spawners))
return
var/obj/effect/mob_spawn/MS = locate(pick(possible_spawners))
- if(!MS || !istype(MS))
+ if(!MS)
+ log_runtime(EXCEPTION("A ghost tried to interact with an invalid spawner, or the spawner didn't exist."))
+ return
+ if(!istype(MS) && !(SEND_SIGNAL(MS, COMSIG_IS_GHOST_CONTROLABLE, usr) & COMPONENT_GHOST_CONTROLABLE))
log_runtime(EXCEPTION("A ghost tried to interact with an invalid spawner, or the spawner didn't exist."))
return
switch(action)
diff --git a/code/datums/status_effects/screwy_hud.dm b/code/datums/status_effects/screwy_hud.dm
new file mode 100644
index 00000000000..09b736d7c22
--- /dev/null
+++ b/code/datums/status_effects/screwy_hud.dm
@@ -0,0 +1,101 @@
+/**
+ * Screwy hud status.
+ *
+ * Applied to carbons, it will make their health bar look like it's incorrect -
+ * in crit (SCREWYHUD_CRIT), dead (SCREWYHUD_DEAD), or fully healthy (SCREWYHUD_HEALTHY)
+ *
+ * Grouped status effect, so multiple sources can add a screwyhud without
+ * accidentally removing another source's hud.
+ */
+/datum/status_effect/grouped/screwy_hud
+ alert_type = null
+ /// The priority of this screwyhud over other screwyhuds.
+ var/priority = -1
+ /// The icon we override our owner's healths.icon_state with
+ var/override_icon
+ /// The icon prefix we override our owner's healthdoll
+ var/override_prefix
+ /// Сhange only species overlays
+ var/only_species = FALSE
+
+/datum/status_effect/grouped/screwy_hud/on_apply()
+ if(!iscarbon(owner))
+ return FALSE
+
+ RegisterSignal(owner, COMSIG_CARBON_UPDATING_HEALTH_HUD, PROC_REF(on_health_hud_updated))
+ if(ishuman(owner))
+ RegisterSignal(owner, COMSIG_HUMAN_UPDATING_HEALTH_HUD, PROC_REF(on_human_health_hud_updated))
+ owner.update_health_hud()
+ return TRUE
+
+/datum/status_effect/grouped/screwy_hud/on_remove()
+ UnregisterSignal(owner, list(COMSIG_CARBON_UPDATING_HEALTH_HUD, COMSIG_HUMAN_UPDATING_HEALTH_HUD))
+ owner.update_health_hud()
+
+/datum/status_effect/grouped/screwy_hud/proc/on_health_hud_updated(mob/living/carbon/source, shown_health_amount)
+ SIGNAL_HANDLER
+
+ // Shouldn't even be running if we're dead, but just in case...
+ if(source.stat == DEAD)
+ return
+
+ // It's entirely possible we have multiple screwy huds on one mob.
+ // Defer to priority to determine which to show. If our's is lower, don't show it.
+ for(var/datum/status_effect/grouped/screwy_hud/other_screwy_hud in source.status_effects)
+ if(other_screwy_hud.priority > priority)
+ return
+
+ source.healths.icon_state = override_icon
+ return COMPONENT_OVERRIDE_HEALTH_HUD
+
+/datum/status_effect/grouped/screwy_hud/proc/on_human_health_hud_updated(mob/living/carbon/human/source, shown_health_amount)
+ SIGNAL_HANDLER
+
+ // Shouldn't even be running if we're dead, but just in case...
+ if(source.stat == DEAD)
+ return
+
+ // It's entirely possible we have multiple screwy huds on one mob.
+ // Defer to priority to determine which to show. If our's is lower, don't show it.
+ for(var/datum/status_effect/grouped/screwy_hud/other_screwy_hud in source.status_effects)
+ if(other_screwy_hud.priority > priority)
+ return
+
+ source.healths.icon_state = override_icon
+ if(source.healthdoll)
+ var/list/new_overlays = list()
+
+ var/list/cached_overlays = source.healthdoll.cached_healthdoll_overlays
+ // Use the dead health doll as the base, since we have proper "healthy" overlays now
+ for(var/obj/item/organ/external/bodypart as anything in source.bodyparts)
+ var/icon_num = override_prefix
+ if(istype(bodypart, /obj/item/organ/external/tail) && bodypart.dna?.species.tail)
+ new_overlays |= "[bodypart.dna.species.tail][icon_num]"
+ if(istype(bodypart, /obj/item/organ/external/wing) && bodypart.dna?.species.tail)
+ new_overlays |= "[bodypart.dna.species.wing][icon_num]"
+ else if(!only_species)
+ new_overlays |= "[bodypart.limb_zone][icon_num]"
+
+ source.healthdoll.add_overlay(new_overlays - cached_overlays)
+ source.healthdoll.cut_overlay(cached_overlays - new_overlays)
+ source.healthdoll.cached_healthdoll_overlays = new_overlays
+ return COMPONENT_OVERRIDE_HEALTH_HUD
+
+/datum/status_effect/grouped/screwy_hud/fake_dead
+ id = "fake_hud_dead"
+ priority = 100 // death is absolute
+ override_icon = "health7"
+ override_prefix = "_DEAD"
+ only_species = TRUE
+
+/datum/status_effect/grouped/screwy_hud/fake_crit
+ id = "fake_hud_crit"
+ priority = 90 // crit is almost death, and death is absolute
+ override_icon = "health6"
+ override_prefix = 5
+
+/datum/status_effect/grouped/screwy_hud/fake_healthy
+ id = "fake_hud_healthy"
+ priority = 10 // fully healthy is the opposite of death, which is absolute
+ override_icon = "health0"
+ override_prefix = 0
diff --git a/code/datums/weather/weather_types/blob_storm.dm b/code/datums/weather/weather_types/blob_storm.dm
index 31321e210f3..a4d8d06f640 100644
--- a/code/datums/weather/weather_types/blob_storm.dm
+++ b/code/datums/weather/weather_types/blob_storm.dm
@@ -7,7 +7,7 @@
telegraph_message = "Вы замечаете мелкие частицы в воздухе"
weather_message = "Вы ощущаете поток неизвестных мелких частиц, которые проникают сквозь любую одежду. Спасти вас может только чудо."
- weather_overlay = "ash_storm"
+ weather_overlay = "blob_storm"
weather_duration_lower = 30 SECONDS
weather_duration_upper = 1 MINUTES
weather_color = COLOR_PALE_GREEN_GRAY
@@ -26,6 +26,20 @@
/datum/weather/blob_storm/telegraph()
+ var/list/blobs = SSticker?.mode?.blobs["infected"] + SSticker?.mode?.blobs["offsprings"]
+ var/color
+ var/mass = 0
+ for(var/datum/mind/blob in blobs)
+ var/mob/camera/blob/overmind = blob.current
+ if(QDELETED(overmind) || !istype(overmind) || overmind.stat == DEAD)
+ continue
+ if(overmind.blobs_legit.len > mass)
+ mass = overmind.blobs_legit.len
+ color = overmind.blobstrain.color
+
+ if(color)
+ weather_color = color
+
..()
status_alarm(TRUE)
GLOB.event_announcement.Announce("Биологической угроза пятого уровня достигла критической массы на борту [station_name()]. Выброс спор и массовое заражение неизбежно.",
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 374e9206374..d2806fdcb83 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -90,7 +90,7 @@
///This datum, if set, allows terrain generation behavior to be ran on Initialize() // This is unfinished, used in Lavaland
var/datum/map_generator/cave_generator/map_generator
- var/area_flags = 0
+ var/area_flags = BLOBS_ALLOWED
/area/New(loc, ...)
if(!there_can_be_many) // Has to be done in New else the maploader will fuck up and find subtypes for the parent
diff --git a/code/game/area/areas/depot-areas.dm b/code/game/area/areas/depot-areas.dm
index 5d22363b9cb..40ee49953ac 100644
--- a/code/game/area/areas/depot-areas.dm
+++ b/code/game/area/areas/depot-areas.dm
@@ -3,6 +3,7 @@
name = "Suspicious Supply Depot"
icon_state = "dark"
tele_proof = 1
+ area_flags = NONE
/area/syndicate_depot/core
icon_state = "red"
diff --git a/code/game/area/areas/mining.dm b/code/game/area/areas/mining.dm
index c62e8d3d749..77a6ff31241 100644
--- a/code/game/area/areas/mining.dm
+++ b/code/game/area/areas/mining.dm
@@ -4,6 +4,7 @@
icon_state = "mining"
has_gravity = STANDARD_GRAVITY
sound_environment = SOUND_AREA_STANDARD_STATION
+ area_flags = NONE
/area/mine/explored
name = "Mine"
@@ -162,7 +163,7 @@
/area/lavaland/surface/outdoors
name = "Lavaland Wastes"
outdoors = TRUE
- area_flags = FLORA_ALLOWED
+ area_flags = FLORA_ALLOWED | BLOBS_ALLOWED
/area/lavaland/surface/outdoors/unexplored // ruins spawn here
icon_state = "unexplored"
diff --git a/code/game/area/ss13_areas.dm b/code/game/area/ss13_areas.dm
index bd59b00b0e7..e1439d14a5d 100644
--- a/code/game/area/ss13_areas.dm
+++ b/code/game/area/ss13_areas.dm
@@ -31,6 +31,7 @@ This applies to all STANDARD station areas
base_lighting_alpha = 255
hide_attacklogs = TRUE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/adminconstruction
@@ -42,6 +43,7 @@ This applies to all STANDARD station areas
base_lighting_alpha = 255
hide_attacklogs = TRUE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/space
icon_state = "space"
@@ -103,6 +105,7 @@ This applies to all STANDARD station areas
/area/shuttle/auxillary_base
icon_state = "shuttle"
+ area_flags = NONE
/area/shuttle/escape
name = "Emergency Shuttle"
@@ -198,11 +201,13 @@ This applies to all STANDARD station areas
icon_state = "shuttle"
name = "Alien Shuttle Base"
requires_power = 1
+ area_flags = NONE
/area/shuttle/alien/mine
icon_state = "shuttle"
name = "Alien Shuttle Mine"
requires_power = 1
+ area_flags = NONE
/area/shuttle/gamma
icon_state = "shuttle"
@@ -225,6 +230,7 @@ This applies to all STANDARD station areas
name = "Special Ops Shuttle"
icon_state = "shuttlered"
parallax_movedir = EAST
+ area_flags = NONE
/area/shuttle/specops/centcom
name = "Special Ops Shuttle"
@@ -239,6 +245,7 @@ This applies to all STANDARD station areas
icon_state = "shuttlered"
nad_allowed = TRUE
parallax_movedir = SOUTH
+ area_flags = NONE
/area/shuttle/syndicate_elite/mothership
name = "Syndicate Elite Shuttle"
@@ -253,6 +260,7 @@ This applies to all STANDARD station areas
icon_state = "shuttlered"
nad_allowed = TRUE
parallax_movedir = SOUTH
+ area_flags = NONE
/area/shuttle/assault_pod
name = "Steel Rain"
@@ -266,6 +274,7 @@ This applies to all STANDARD station areas
name = "Nanotrasen Vessel"
icon_state = "shuttlered"
parallax_movedir = WEST
+ area_flags = NONE
/area/shuttle/administration/centcom
name = "Nanotrasen Vessel Centcom"
@@ -277,6 +286,7 @@ This applies to all STANDARD station areas
/area/shuttle/thunderdome
name = "honk"
+ area_flags = NONE
/area/shuttle/thunderdome/grnshuttle
name = "Thunderdome GRN Shuttle"
@@ -316,6 +326,7 @@ This applies to all STANDARD station areas
/area/shuttle/vox
name = "Vox Skipjack"
icon_state = "shuttle"
+ area_flags = NONE
/area/shuttle/vox/station
name = "Vox Skipjack"
@@ -324,6 +335,7 @@ This applies to all STANDARD station areas
/area/shuttle/salvage
name = "Salvage Ship"
icon_state = "yellow"
+ area_flags = NONE
/area/shuttle/salvage/start
name = "Middle of Nowhere"
@@ -381,27 +393,33 @@ This applies to all STANDARD station areas
/area/shuttle/supply
name = "Supply Shuttle"
icon_state = "shuttle3"
+ area_flags = NONE
/area/shuttle/ussp
name = "USSP Shuttle"
icon_state = "shuttle3"
+ area_flags = NONE
/area/shuttle/spacebar
name = "Space Bar Shuttle"
icon_state = "shuttle3"
+ area_flags = NONE
/area/shuttle/abandoned
name = "Abandoned Ship"
icon_state = "shuttle"
+ area_flags = NONE
/area/shuttle/syndicate
name = "Syndicate Nuclear Team Shuttle"
icon_state = "shuttle"
nad_allowed = TRUE
+ area_flags = NONE
/area/shuttle/trade
name = "Trade Shuttle"
icon_state = "shuttle"
+ area_flags = NONE
/area/shuttle/trade/sol
name = "Sol Freighter"
@@ -415,6 +433,7 @@ This applies to all STANDARD station areas
/area/shuttle/pirate_corvette
name = "Pirate Corvette"
icon_state = "shuttle"
+ area_flags = NONE
/area/airtunnel1/ // referenced in airtunnel.dm:759
@@ -441,6 +460,7 @@ This applies to all STANDARD station areas
base_lighting_alpha = 255
nad_allowed = TRUE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
// New CC
/area/centcom/bridge
@@ -513,6 +533,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
nad_allowed = TRUE
ambientsounds = HIGHSEC_SOUNDS
+ area_flags = NONE
/area/syndicate_mothership/outside
name = "Syndicate Controlled Territory"
@@ -552,6 +573,7 @@ This applies to all STANDARD station areas
base_lighting_alpha = 255
base_lighting_color = COLOR_WHITE
ambientsounds = HIGHSEC_SOUNDS
+ area_flags = NONE
// Chrono
@@ -564,6 +586,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
base_lighting_alpha = 255
nad_allowed = TRUE
+ area_flags = NONE
//EXTRA
@@ -576,6 +599,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
base_lighting_alpha = 255
nad_allowed = TRUE
+ area_flags = NONE
/area/asteroid // -- TLE
name = "Asteroid"
@@ -605,6 +629,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
base_lighting_alpha = 255
hide_attacklogs = TRUE
+ area_flags = NONE
/area/tdome/arena_source
@@ -644,6 +669,7 @@ This applies to all STANDARD station areas
icon_state = "green"
there_can_be_many = TRUE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
//Abductors
/area/abductor_ship
@@ -651,6 +677,7 @@ This applies to all STANDARD station areas
icon_state = "yellow"
requires_power = FALSE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/wizard_station
name = "Wizard's Den"
@@ -672,6 +699,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
sound_environment = SOUND_AREA_MEDIUM_SOFTFLOOR
nad_allowed = TRUE
+ area_flags = NONE
/area/ninja/outpost
name = "SpiderClan Dojo"
@@ -699,6 +727,7 @@ This applies to all STANDARD station areas
base_lighting_color = COLOR_WHITE
base_lighting_alpha = 255
no_teleportlocs = TRUE
+ area_flags = NONE
/area/trader_station
name = "Trade Base"
@@ -708,6 +737,7 @@ This applies to all STANDARD station areas
static_lighting = FALSE
base_lighting_alpha = 255
base_lighting_color = COLOR_WHITE
+ area_flags = NONE
/area/trader_station/sol
name = "Jupiter Station 6"
@@ -720,6 +750,7 @@ This applies to all STANDARD station areas
static_lighting = FALSE
base_lighting_alpha = 255
base_lighting_color = COLOR_WHITE
+ area_flags = NONE
/area/ussp_centcom/secretariat
name = "Soviet secretariat"
@@ -2654,6 +2685,7 @@ This applies to all STANDARD station areas
icon_state = "syndie_hall"
report_alerts = FALSE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/traitor/rnd
name = "Syndicate Research and Development"
@@ -2827,7 +2859,7 @@ This applies to all STANDARD station areas
has_gravity = STANDARD_GRAVITY
ambientsounds = AWAY_MISSION_SOUNDS
sound_environment = SOUND_ENVIRONMENT_ROOM
-
+ area_flags = NONE
/area/awaymission/example
name = "Strange Station"
icon_state = "away"
@@ -2855,6 +2887,7 @@ This applies to all STANDARD station areas
name = "moonoutpost"
has_gravity = STANDARD_GRAVITY
report_alerts = FALSE
+ area_flags = NONE
/area/moonoutpost19/mo19arrivals
name = "MO19 Arrivals"
@@ -2874,6 +2907,7 @@ This applies to all STANDARD station areas
power_light = FALSE
poweralm = FALSE
outdoors = TRUE
+ area_flags = NONE
/area/moonoutpost19/syndicateoutpost
name = "Syndicate Outpost"
@@ -2939,6 +2973,7 @@ This applies to all STANDARD station areas
name = "space"
report_alerts = FALSE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/awaycontent/a1
icon_state = "awaycontent1"
@@ -3058,6 +3093,7 @@ GLOBAL_LIST_INIT(centcom_areas, list(
static_lighting = TRUE
report_alerts = FALSE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/special_event/alpha
name = "Special event area Alpha"
diff --git a/code/game/area/vision_reset_areas.dm b/code/game/area/vision_reset_areas.dm
index cd8b5cbf552..c3ac0a13211 100644
--- a/code/game/area/vision_reset_areas.dm
+++ b/code/game/area/vision_reset_areas.dm
@@ -5,6 +5,7 @@
*/
/area/vision_change_area
+ area_flags = NONE
/area/vision_change_area/Entered(atom/movable/arrived, area/old_area)
. = ..()
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 755a19232bb..34ddf94ba44 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -13,6 +13,7 @@
var/level = 2
var/flags = NONE
var/flags_2 = NONE
+ var/flags_ricochet = NONE
var/list/fingerprints
var/list/fingerprints_time
var/list/fingerprintshidden
@@ -108,6 +109,7 @@
var/base_pixel_y = 0
var/tts_seed = "Arthas"
+ var/tts_atom_say_effect = SOUND_EFFECT_RADIO
/atom/New(loc, ...)
SHOULD_CALL_PARENT(TRUE)
@@ -630,8 +632,19 @@
/atom/proc/ex_act()
return
-/atom/proc/blob_act(obj/structure/blob/B)
- SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, B)
+/**
+ * React to a hit by a blob objecd
+ *
+ * default behaviour is to send the [COMSIG_ATOM_BLOB_ACT] signal
+ */
+/atom/proc/blob_act(obj/structure/blob/attacking_blob)
+ var/blob_act_result = SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, attacking_blob)
+ if(blob_act_result & COMPONENT_CANCEL_BLOB_ACT)
+ return FALSE
+ return TRUE
+
+/atom/proc/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ return TRUE
/atom/proc/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume, global_overlay = TRUE)
SEND_SIGNAL(src, COMSIG_ATOM_FIRE_ACT, exposed_temperature, exposed_volume)
@@ -1192,8 +1205,6 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
/atom/proc/ratvar_act()
return
-/atom/proc/handle_ricochet(obj/item/projectile/P)
- return
//This proc is called on the location of an atom when the atom is Destroy()'d
/atom/proc/handle_atom_del(atom/A)
@@ -1217,9 +1228,8 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
if(M.client.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
M.create_chat_message(src, message, list("italics"))
- var/effect = SOUND_EFFECT_RADIO
var/traits = TTS_TRAIT_RATE_MEDIUM
- INVOKE_ASYNC(GLOBAL_PROC, /proc/tts_cast, src, M, message_tts, tts_seed, TRUE, effect, traits)
+ INVOKE_ASYNC(GLOBAL_PROC, /proc/tts_cast, src, M, message_tts, tts_seed, TRUE, tts_atom_say_effect, traits)
if(length(speech_bubble_hearers))
var/image/I = image('icons/mob/talk.dmi', src, "[bubble_icon][say_test(message)]", FLY_LAYER)
@@ -1543,6 +1553,18 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
/atom/proc/get_visible_gender() // Used only in /mob/living/carbon/human and /mob/living/simple_animal/hostile/morph
return gender
+/atom/proc/handle_ricochet(obj/item/projectile/ricocheting_projectile)
+ var/turf/p_turf = get_turf(ricocheting_projectile)
+ var/face_direction = get_dir(src, p_turf) || get_dir(src, ricocheting_projectile)
+ var/face_angle = dir2angle(face_direction)
+ var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (ricocheting_projectile.Angle + 180))
+ var/a_incidence_s = abs(incidence_s)
+ if(a_incidence_s > 90 && a_incidence_s < 270)
+ return FALSE
+ var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
+ ricocheting_projectile.set_angle(new_angle_s)
+ visible_message(span_warning("[ricocheting_projectile] reflects off [src]!"))
+ return TRUE
/// Whether the mover object can avoid being blocked by this atom, while arriving from (or leaving through) the border_dir.
/atom/proc/CanPass(atom/movable/mover, border_dir)
diff --git a/code/game/gamemodes/blob/blob.dm b/code/game/gamemodes/blob/blob.dm
index 685abb965e8..e2c02546831 100644
--- a/code/game/gamemodes/blob/blob.dm
+++ b/code/game/gamemodes/blob/blob.dm
@@ -1,6 +1,6 @@
/datum/game_mode
/// List of of blobs, their offsprings and blobburnouts spawned by them
- var/list/blobs = list("infected"=list(), "offsprings"=list(), "blobernauts"=list())
+ var/list/blobs = list("infected"=list(), "offsprings"=list(), "minions"=list())
/// Count of blob tiles to blob win
var/blob_win_count = BLOB_BASE_TARGET_POINT
/// Number of resource produced by the core
@@ -15,6 +15,10 @@
var/off_auto_gamma = FALSE
/// Disables automatic nuke codes
var/off_auto_nuke_codes = FALSE
+ /// Is all blobs have infinity points
+ var/is_blob_infinity_points = FALSE
+ /// Is all blobs have infinity points
+ var/list/legit_blobs = list()
/// Total blobs objective
var/datum/objective/blob_critical_mass/blob_objective
@@ -110,7 +114,7 @@
/datum/game_mode/proc/update_blob_objective()
if(blob_objective && !blob_objective.completed)
- blob_objective.critical_mass = GLOB.blobs.len
+ blob_objective.critical_mass = legit_blobs.len
blob_objective.needed_critical_mass = blob_win_count
blob_objective.set_target()
@@ -126,7 +130,7 @@
blob_list.Add(value)
for(var/value in blobs["offsprings"])
blob_list.Add(value)
- for(var/value in blobs["blobernauts"])
+ for(var/value in blobs["minions"])
blob_list.Add(value)
return blob_list
@@ -196,35 +200,37 @@
return
if(blob_stage == BLOB_STAGE_NONE)
blob_stage = BLOB_STAGE_ZERO
- if(blob_stage == BLOB_STAGE_ZERO && GLOB.blobs.len >= min(FIRST_STAGE_COEF * blob_win_count, FIRST_STAGE_THRESHOLD))
+ if(blob_stage == BLOB_STAGE_ZERO && legit_blobs.len >= min(FIRST_STAGE_COEF * blob_win_count, FIRST_STAGE_THRESHOLD))
blob_stage = BLOB_STAGE_FIRST
send_intercept(BLOB_FIRST_REPORT)
SSshuttle?.emergency?.cancel()
SSshuttle?.lockdown_escape()
- if(blob_stage == BLOB_STAGE_FIRST && GLOB.blobs.len >= min(SECOND_STAGE_COEF * blob_win_count, SECOND_STAGE_THRESHOLD))
+ if(blob_stage == BLOB_STAGE_FIRST && legit_blobs.len >= min(SECOND_STAGE_COEF * blob_win_count, SECOND_STAGE_THRESHOLD))
blob_stage = BLOB_STAGE_SECOND
GLOB.event_announcement.Announce("Подтверждена вспышка биологической угрозы пятого уровня на борту [station_name()]. Весь персонал обязан локализовать угрозу.",
- "ВНИМАНИЕ: БИОЛОГИЧЕСКАЯ УГРОЗА.", 'sound/AI/outbreak5.ogg')
+ "ВНИМАНИЕ: БИОЛОГИЧЕСКАЯ УГРОЗА.", 'sound/AI/outbreak5.ogg')
if(!off_auto_gamma)
addtimer(CALLBACK(GLOBAL_PROC, /proc/set_security_level, SEC_LEVEL_GAMMA), TIME_TO_SWITCH_CODE)
- if(blob_stage == BLOB_STAGE_SECOND && GLOB.blobs.len >= THIRD_STAGE_COEF * blob_win_count)
+ if(blob_stage == BLOB_STAGE_SECOND && legit_blobs.len >= THIRD_STAGE_COEF * blob_win_count && (blob_win_count - legit_blobs.len) <= THIRD_STAGE_DELTA_THRESHOLD)
blob_stage = BLOB_STAGE_THIRD
send_intercept(BLOB_SECOND_REPORT)
- if(GLOB.blobs.len >= blob_win_count && blob_stage < BLOB_STAGE_STORM)
+ if(legit_blobs.len >= blob_win_count && blob_stage < BLOB_STAGE_STORM)
if(SSweather)
blob_stage = BLOB_STAGE_STORM
SSweather.run_weather(/datum/weather/blob_storm)
+ show_warning("Вы набрали критическую массу и ощущаете практически бесконечный приток ресурсов.")
+ is_blob_infinity_points = TRUE
addtimer(CALLBACK(src, PROC_REF(process_blob_stages)), STAGES_CALLBACK_TIME)
/datum/game_mode/proc/show_warning(message)
- for(var/datum/mind/blob in blobs["infected"])
+ for(var/datum/mind/blob in (blobs["infected"] + blobs["offsprings"]))
if(blob.current.stat != DEAD)
- to_chat(blob.current, "[message]")
+ to_chat(blob.current, span_warning("[message]"))
/datum/game_mode/proc/burst_blobs()
diff --git a/code/game/gamemodes/blob/blob_finish.dm b/code/game/gamemodes/blob/blob_finish.dm
index db27c5d382c..f674ccd52c7 100644
--- a/code/game/gamemodes/blob/blob_finish.dm
+++ b/code/game/gamemodes/blob/blob_finish.dm
@@ -8,7 +8,7 @@
return
update_blob_objective()
GLOB.event_announcement.Announce("Объект потерян. Причина: распостранение 5-ой биоугрозы. Взведение устройства самоуничтожения персоналом или внешними силами в данный момент не представляется возможным из-за высокого уровня заражения. Решение: оставить станцию в изоляции до принятия окончательных мер противодействия.",
- "Отчет об объекте [station_name()]")
+ "Отчет об объекте [station_name()]")
blob_stage = (delay_blob_end)? BLOB_STAGE_POST_END : BLOB_STAGE_END
if(blob_stage == BLOB_STAGE_END)
end_game()
@@ -62,7 +62,7 @@
/datum/game_mode/proc/auto_declare_completion_blob()
var/list/blob_infected = blobs["infected"]
var/list/blob_offsprings = blobs["offsprings"]
- var/list/blobernauts = blobs["blobernauts"]
+ var/list/minions = blobs["minions"]
if(blob_infected?.len)
declare_blob_completion()
var/text = "
Блоб[(blob_infected.len > 1 ? "ами были" : "ом был")]:"
@@ -75,9 +75,9 @@
for(var/datum/mind/blob in blob_offsprings)
text += "
[blob.key] был [blob.name]"
- if(blobernauts?.len)
- text += "
Блобернаут[(blobernauts.len > 1 ? "ами были" : "ом был")]:"
- for(var/datum/mind/blob in blobernauts)
+ if(minions?.len)
+ text += "
Миньен[(minions.len > 1 ? "ами были" : "ом был")]:"
+ for(var/datum/mind/blob in minions)
text += "
[blob.key] был [blob.name]"
to_chat(world, text)
diff --git a/code/game/gamemodes/blob/blob_report.dm b/code/game/gamemodes/blob/blob_report.dm
index 79f5bc1764d..fadbd95cda1 100644
--- a/code/game/gamemodes/blob/blob_report.dm
+++ b/code/game/gamemodes/blob/blob_report.dm
@@ -11,11 +11,11 @@
intercepttext += "Предварительный анализ организма классифицирует его как биологическую угрозу 5-го уровня. Его происхождение неизвестно.
"
intercepttext += "Nanotrasen выпустила директиву 7-10 для [station_name()]. Станцию следует считать закрытой на карантин.
"
intercepttext += "Приказы для всего персонала [station_name()] следующие:
"
- intercepttext += " 1. Не покидайте карантинную зону
"
- intercepttext += " 2. Обнаружить любые очаги угрозы на станции.
"
- intercepttext += " 3. При обнаружении используйте любые необходимые средства для сдерживания организма.
"
- intercepttext += " 4. Избегайте повреждения критической инфраструктуры станции.
"
- intercepttext += "
Примечание. в случае нарушения карантина или неконтролируемого распространения биологической опасности директива 7-10 может быть дополнена директивой 7-12.
"
+ intercepttext += " 1. Не покидать карантинную зону.
"
+ intercepttext += " 2. Обнаружить все очаги угрозы на станции.
"
+ intercepttext += " 3. При обнаружении использовать любые необходимые средства для сдерживания организмов.
"
+ intercepttext += " 4. Избегать повреждения критической инфраструктуры станции.
"
+ intercepttext += "
Примечание. в случае нарушения карантина или неконтролируемого распространения биологической угрозы директива 7-10 может быть дополнена директивой 7-12.
"
intercepttext += "Конец сообщения."
if(BLOB_SECOND_REPORT)
var/nukecode = rand(10000, 99999)
@@ -29,8 +29,8 @@
intercepttext += "Для [station_name()] была издана директива 7-12.
"
intercepttext += "Биологическая угроза вышла из-под контроля и скоро достигнет критической массы.
"
intercepttext += "Вам приказано следующее:
"
- intercepttext += " 1. Защитите диск ядерной аутентификации.
"
- intercepttext += " 2. Взорвите ядерную боеголовку, находящуюся в хранилище станции.
"
+ intercepttext += " 1. Защищать диск ядерной аутентификации.
"
+ intercepttext += " 2. Взорвать ядерную боеголовку, находящуюся в хранилище станции.
"
if(off_auto_nuke_codes)
intercepttext += "Код ядерной аутентификации будет выслан в скором времени отдельным сообщением. Ожидайте.
"
else
@@ -54,7 +54,7 @@
intercepttext += "Дирректива 7-12 была отменена для [station_name()].
"
intercepttext += "Биоугроза уничтожена, либо ее остаточные следы не представляют опасности.
"
intercepttext += "Вам приказано следующее:
"
- intercepttext += " 1. Уничтожьте все полученные засекреченные сообщения.
"
+ intercepttext += " 1. Уничтожить все полученные засекреченные сообщения.
"
intercepttext += " 2. В случае невозможности продолжать смену ввиду потерь среди экипажа или критического состояния станции, провести эвакуацию экипажа.
"
if(blob_stage == BLOB_STAGE_THIRD && !off_auto_nuke_codes)
intercepttext += " 3. Код от боеголовки, как и ее назначение необходимо держать в строжайшей секретности.
"
diff --git a/code/game/gamemodes/blob/blobs/blob_mobs.dm b/code/game/gamemodes/blob/blobs/blob_mobs.dm
deleted file mode 100644
index 24c3898a7cb..00000000000
--- a/code/game/gamemodes/blob/blobs/blob_mobs.dm
+++ /dev/null
@@ -1,262 +0,0 @@
-
-////////////////
-// BASE TYPE //
-////////////////
-
-//Do not spawn
-/mob/living/simple_animal/hostile/blob
- icon = 'icons/mob/blob.dmi'
- pass_flags = PASSBLOB
- status_flags = NONE //No throwing blobspores into deep space to despawn, or throwing blobbernaughts, which are bigger than you.
- faction = list(ROLE_BLOB)
- bubble_icon = "blob"
- atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
- universal_speak = 1 //So mobs can understand them when a blob uses Blob Broadcast
- sentience_type = SENTIENCE_OTHER
- gold_core_spawnable = NO_SPAWN
- can_be_on_fire = TRUE
- fire_damage = 3
- var/mob/camera/blob/overmind = null
- tts_seed = "Earth"
-
-/mob/living/simple_animal/hostile/blob/ComponentInitialize()
- AddComponent( \
- /datum/component/animal_temperature, \
- maxbodytemp = 360, \
- minbodytemp = 0, \
- )
-
-/mob/living/simple_animal/hostile/blob/proc/adjustcolors(var/a_color)
- if(a_color)
- color = a_color
-
-/mob/living/simple_animal/hostile/blob/blob_act()
- if(stat != DEAD && health < maxHealth)
- for(var/i in 1 to 2)
- var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal(get_turf(src)) //hello yes you are being healed
- if(overmind)
- H.color = overmind.blob_reagent_datum.complementary_color
- else
- H.color = "#000000"
- adjustHealth(-maxHealth * 0.0125)
-
-
-////////////////
-// BLOB SPORE //
-////////////////
-
-/mob/living/simple_animal/hostile/blob/blobspore
- name = "blob"
- desc = "Some blob thing."
- icon_state = "blobpod"
- icon_living = "blobpod"
- health = 40
- maxHealth = 40
- melee_damage_lower = 2
- melee_damage_upper = 4
- obj_damage = 20
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- attacktext = "ударяет"
- attack_sound = 'sound/weapons/genhit1.ogg'
- speak_emote = list("pulses")
- var/obj/structure/blob/factory/factory = null
- var/list/human_overlays
- var/mob/living/carbon/human/oldguy
- var/is_zombie = FALSE
-
-
-/mob/living/simple_animal/hostile/blob/blobspore/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(istype(mover, /obj/structure/blob))
- return TRUE
-
-
-/mob/living/simple_animal/hostile/blob/blobspore/New(loc, var/obj/structure/blob/factory/linked_node)
- if(istype(linked_node))
- factory = linked_node
- factory.spores += src
- ..()
-
-
-/mob/living/simple_animal/hostile/blob/blobspore/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
- AddElement(/datum/element/simple_flying)
-
-
-/mob/living/simple_animal/hostile/blob/blobspore/Life(seconds, times_fired)
-
- if(!is_zombie && isturf(src.loc))
- for(var/mob/living/carbon/human/H in oview(src, 1)) //Only for corpse right next to/on same tile
- if(H.stat == DEAD || (!H.check_death_method() && H.health <= HEALTH_THRESHOLD_DEAD))
- Zombify(H)
- break
- ..()
-
-/mob/living/simple_animal/hostile/blob/blobspore/proc/Zombify(mob/living/carbon/human/H)
- if(!H.check_death_method())
- H.death()
- var/obj/item/organ/external/head/head_organ = H.get_organ(BODY_ZONE_HEAD)
- is_zombie = TRUE
- if(H.wear_suit)
- var/obj/item/clothing/suit/armor/A = H.wear_suit
- if(A.armor && A.armor.getRating("melee"))
- maxHealth += A.armor.getRating("melee") //That zombie's got armor, I want armor!
- maxHealth += 40
- health = maxHealth
- name = "blob zombie"
- desc = "A shambling corpse animated by the blob."
- melee_damage_lower = 10
- melee_damage_upper = 15
- icon = H.icon
- speak_emote = list("groans")
- icon_state = "zombie2_s"
- if(head_organ)
- head_organ.h_style = null
- H.update_hair()
- human_overlays = H.overlays
- update_icons()
- H.forceMove(src)
- oldguy = H
- visible_message("The corpse of [H.name] suddenly rises!")
-
-/mob/living/simple_animal/hostile/blob/blobspore/death(gibbed)
- // Only execute the below if we successfuly died
- . = ..()
- if(!.)
- return FALSE
- // On death, create a small smoke of harmful gas (s-Acid)
- var/datum/effect_system/smoke_spread/chem/S = new
- var/turf/location = get_turf(src)
-
- // Create the reagents to put into the air
- create_reagents(350)
-
- if(overmind && overmind.blob_reagent_datum)
- reagents.add_reagent(overmind.blob_reagent_datum.id, 350)
- else
- reagents.add_reagent("spore", 350)
-
- // Setup up the smoke spreader and start it.
- S.set_up(reagents, location, TRUE)
- S.start()
- qdel(src)
-
-/mob/living/simple_animal/hostile/blob/blobspore/Destroy()
- if(factory)
- factory.spores -= src
- factory = null
- if(oldguy)
- oldguy.forceMove(get_turf(src))
- oldguy = null
- return ..()
-
-
-/mob/living/simple_animal/hostile/blob/blobspore/update_icons()
- ..()
-
- adjustcolors(overmind?.blob_reagent_datum?.complementary_color)
-
-/mob/living/simple_animal/hostile/blob/blobspore/adjustcolors(var/a_color)
- color = a_color
-
- if(is_zombie)
- cut_overlays()
- add_overlay(human_overlays)
- var/image/I = image('icons/mob/blob.dmi', icon_state = "blob_head")
- I.color = color
- add_overlay(I)
-
- if(blocks_emissive)
- add_overlay(get_emissive_block())
-
-
-/////////////////
-// BLOBBERNAUT //
-/////////////////
-
-/mob/living/simple_animal/hostile/blob/blobbernaut
- name = "blobbernaut"
- desc = "Some HUGE blob thing."
- icon_state = "blobbernaut"
- icon_living = "blobbernaut"
- icon_dead = "blobbernaut_dead"
- health = 200
- maxHealth = 200
- melee_damage_lower = 10
- melee_damage_upper = 15
- obj_damage = 60
- attacktext = "ударяет"
- attack_sound = 'sound/effects/blobattack.ogg'
- speak_emote = list("gurgles")
- force_threshold = 10
- mob_size = MOB_SIZE_LARGE
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- pressure_resistance = 50
- sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
- nightvision = 8
- lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
- move_resist = MOVE_FORCE_OVERPOWERING
-
-/mob/living/simple_animal/hostile/ancient_robot_leg/ComponentInitialize()
- AddComponent( \
- /datum/component/animal_temperature, \
- minbodytemp = 0, \
- maxbodytemp = 360, \
- )
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NEGATES_GRAVITY, INNATE_TRAIT)
-
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/experience_pressure_difference(pressure_difference, direction)
- if(!HAS_TRAIT(src, TRAIT_NEGATES_GRAVITY))
- return ..()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/proc/add_to_gamemode()
- var/list/blobernauts = SSticker?.mode?.blobs["blobernauts"]
- blobernauts |= mind
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/Life(seconds, times_fired)
- if(stat != DEAD && (getBruteLoss() || getFireLoss())) // Heal on blob structures
- if(locate(/obj/structure/blob) in get_turf(src))
- heal_overall_damage(0.25, 0.25)
- if(on_fire)
- adjust_fire_stacks(-1) // Slowly extinguish the flames
- else
- take_overall_damage(0.2, 0.2) // If you are at full health, you won't lose health. You'll need it. However the moment anybody sneezes on you, the decaying will begin.
- ..()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/New()
- ..()
- if(name == "blobbernaut")
- name = text("blobbernaut ([rand(1, 1000)])")
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/death(gibbed)
- mind.name = name
- // Only execute the below if we successfully died
- . = ..()
- if(!.)
- return FALSE
- flick("blobbernaut_death", src)
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/verb/communicate_overmind()
- set category = "Blobbernaut"
- set name = "Blob Telepathy"
- set desc = "Send a message to the Overmind"
-
- if(stat != DEAD)
- blob_talk()
-
-/mob/living/simple_animal/hostile/blob/blobbernaut/proc/blob_talk()
- var/message = tgui_input_text(usr, "Announce to the overmind", "Blob Telepathy")
- var/rendered = "Blob Telepathy, [name]([overmind]) states, \"[message]\""
- if(message)
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobbernaut(M) || isblobinfected(M.mind))
- M.show_message(rendered, 2)
- else if(isobserver(M) && !isnewplayer(M))
- var/rendered_ghost = "Blob Telepathy, [name]([overmind]) \
- (F) states, \"[message]\""
- M.show_message(rendered_ghost, 2)
diff --git a/code/game/gamemodes/blob/blobs/captured_nuke.dm b/code/game/gamemodes/blob/blobs/captured_nuke.dm
deleted file mode 100644
index 7256332d366..00000000000
--- a/code/game/gamemodes/blob/blobs/captured_nuke.dm
+++ /dev/null
@@ -1,27 +0,0 @@
-/obj/structure/blob/captured_nuke //alternative to blob just straight up destroying nukes
- name = "blob captured nuke"
- icon_state = "blob"
- desc = "A Nuclear Warhead tangled in blob tendrils pulsating with a horrific green glow."
- max_integrity = 100
- point_return = 0
-
-/obj/structure/blob/captured_nuke/Initialize(mapload, obj/machinery/nuclearbomb/N)
- . = ..()
- START_PROCESSING(SSobj, src)
- N?.forceMove(src)
- update_icon(UPDATE_OVERLAYS)
-
-
-/obj/structure/blob/captured_nuke/update_overlays()
- . = ..()
- . += mutable_appearance('icons/mob/blob.dmi', "blob_nuke_overlay", appearance_flags = RESET_COLOR)
-
-
-/obj/structure/blob/captured_nuke/Destroy()
- for(var/obj/machinery/nuclearbomb/O in contents)
- O.forceMove(loc)
- STOP_PROCESSING(SSobj, src)
- return ..()
-
-/obj/structure/blob/captured_nuke/Life(seconds, times_fired)
- obj_integrity = min(max_integrity, obj_integrity + 1)
diff --git a/code/game/gamemodes/blob/blobs/core.dm b/code/game/gamemodes/blob/blobs/core.dm
deleted file mode 100644
index 6b94aa23d1a..00000000000
--- a/code/game/gamemodes/blob/blobs/core.dm
+++ /dev/null
@@ -1,154 +0,0 @@
-/obj/structure/blob/core
- name = "blob core"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blank_blob"
- max_integrity = 400
- armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 75, "acid" = 90)
- fire_resist = 2
- point_return = -1
- var/overmind_get_delay = 0 // we don't want to constantly try to find an overmind, do it every 5 minutes
- var/resource_delay = 0
- var/point_rate = 2
- var/is_offspring = null
- var/selecting = 0
-
-/obj/structure/blob/core/New(loc, var/h = 200, var/client/new_overmind = null, var/new_rate = 2, offspring)
- GLOB.blob_cores += src
- START_PROCESSING(SSobj, src)
- GLOB.poi_list |= src
- adjustcolors(color) //so it atleast appears
- if(!overmind)
- create_overmind(new_overmind)
- if(offspring)
- is_offspring = TRUE
- point_rate = new_rate
- ..(loc, h)
-
-
-/obj/structure/blob/core/adjustcolors(a_color)
- cut_overlays()
- color = null
- var/image/I = new('icons/mob/blob.dmi', "blob")
- I.color = a_color
- add_overlay(I)
- var/image/C = new('icons/mob/blob.dmi', "blob_core_overlay")
- add_overlay(C)
-
- if(blocks_emissive)
- add_overlay(get_emissive_block())
-
-
-/obj/structure/blob/core/Destroy()
- GLOB.blob_cores -= src
- if(overmind)
- overmind.blob_core = null
- overmind = null
- SSticker?.mode?.blob_died()
- STOP_PROCESSING(SSobj, src)
- GLOB.poi_list.Remove(src)
- return ..()
-
-/obj/structure/blob/core/ex_act(severity)
- var/damage = 50 - 10 * severity //remember, the core takes half brute damage, so this is 20/15/10 damage based on severity
- take_damage(damage, BRUTE, "bomb", 0)
-
-/obj/structure/blob/core/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, overmind_reagent_trigger = 1)
- . = ..()
- if(obj_integrity > 0)
- if(overmind) //we should have an overmind, but...
- overmind.update_health_hud()
-
-/obj/structure/blob/core/RegenHealth()
- return // Don't regen, we handle it in Life()
-
-/obj/structure/blob/core/Life(seconds, times_fired)
- if(!overmind)
- create_overmind()
- else
- if(resource_delay <= world.time)
- resource_delay = world.time + 10 // 1 second
- overmind.add_points(point_rate)
- obj_integrity = min(max_integrity, obj_integrity + 1)
- if(overmind)
- overmind.update_health_hud()
- if(overmind?.blob_reagent_datum?.color)
- for(var/i = 1; i < 8; i += i)
- Pulse(0, i, overmind.blob_reagent_datum.color)
- else
- for(var/i = 1; i < 8; i += i)
- Pulse(0, i, color)
- for(var/b_dir in GLOB.alldirs)
- if(!prob(5))
- continue
- var/obj/structure/blob/normal/B = locate() in get_step(src, b_dir)
- if(B)
- B.change_to(/obj/structure/blob/shield/core)
- if(B && overmind?.blob_reagent_datum?.color)
- B.color = overmind.blob_reagent_datum.color
- else
- B.color = color
- color = null
- ..()
-
-
-/obj/structure/blob/core/proc/create_overmind(client/new_overmind, override_delay)
- if(overmind_get_delay > world.time && !override_delay)
- return
-
- overmind_get_delay = world.time + 5 MINUTES
-
- if(overmind)
- qdel(overmind)
-
- INVOKE_ASYNC(src, PROC_REF(get_new_overmind), new_overmind)
-
-/obj/structure/blob/core/proc/get_new_overmind(client/new_overmind)
- var/mob/C = null
- var/list/candidates = list()
- if(!new_overmind)
- // sendit
- if(is_offspring)
- candidates = SSghost_spawns.poll_candidates("Do you want to play as a blob offspring?", ROLE_BLOB, TRUE, source = src)
- else
- candidates = SSghost_spawns.poll_candidates("Do you want to play as a blob?", ROLE_BLOB, TRUE, source = src)
-
- if(length(candidates))
- C = pick(candidates)
- else
- C = new_overmind
-
- if(C && !QDELETED(src))
- var/mob/camera/blob/B = new(loc)
- B.key = C.key
- B.blob_core = src
- overmind = B
- B.is_offspring = is_offspring
- addtimer(CALLBACK(src, PROC_REF(add_datum_if_not_exist)), TIME_TO_ADD_OM_DATUM)
- log_game("[B.key] has become Blob [is_offspring ? "offspring" : ""]")
-
-/obj/structure/blob/core/proc/lateblobtimer()
- addtimer(CALLBACK(src, PROC_REF(lateblobcheck)), 50)
-
-/obj/structure/blob/core/proc/lateblobcheck()
- if(overmind)
- overmind.add_points(60)
- if(!overmind.mind)
- log_debug("/obj/structure/blob/core/proc/lateblobcheck: Blob core lacks a overmind.mind.")
- else
- log_debug("/obj/structure/blob/core/proc/lateblobcheck: Blob core lacks an overmind.")
-
-/obj/structure/blob/core/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer)
- overmind?.forceMove(get_turf(src))
- return ..()
-
-/obj/structure/blob/core/proc/add_datum_if_not_exist()
- overmind.select_reagent()
- if(!overmind.mind.has_antag_datum(/datum/antagonist/blob_overmind))
- var/datum/antagonist/blob_overmind/overmind_datum = new
- overmind_datum.add_to_mode = TRUE
- overmind_datum.is_offspring = is_offspring
- if(overmind.blob_reagent_datum)
- overmind_datum.reagent = overmind.blob_reagent_datum
- overmind.mind.add_antag_datum(overmind_datum)
- color = overmind.blob_reagent_datum?.color
-
diff --git a/code/game/gamemodes/blob/blobs/factory.dm b/code/game/gamemodes/blob/blobs/factory.dm
deleted file mode 100644
index 517d6ece7fd..00000000000
--- a/code/game/gamemodes/blob/blobs/factory.dm
+++ /dev/null
@@ -1,30 +0,0 @@
-/obj/structure/blob/factory
- name = "factory blob"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blob_factory"
- max_integrity = 200
- point_return = 18
- var/list/spores = list()
- var/max_spores = 3
- var/spore_delay = 0
- var/is_waiting_spawn = FALSE
-
-/obj/structure/blob/factory/Destroy()
- for(var/mob/living/simple_animal/hostile/blob/blobspore/spore in spores)
- if(spore.factory == src)
- spore.factory = null
- spores = null
- return ..()
-
-/obj/structure/blob/factory/run_action()
- if(spores.len >= max_spores)
- return
- if(spore_delay > world.time)
- return
- flick("blob_factory_glow", src)
- spore_delay = world.time + 100 // 10 seconds
- var/mob/living/simple_animal/hostile/blob/blobspore/BS = new/mob/living/simple_animal/hostile/blob/blobspore(src.loc, src)
- if(overmind)
- BS.color = overmind?.blob_reagent_datum?.complementary_color
- BS.overmind = overmind
- overmind.blob_mobs.Add(BS)
diff --git a/code/game/gamemodes/blob/blobs/node.dm b/code/game/gamemodes/blob/blobs/node.dm
deleted file mode 100644
index ca570996aec..00000000000
--- a/code/game/gamemodes/blob/blobs/node.dm
+++ /dev/null
@@ -1,39 +0,0 @@
-/obj/structure/blob/node
- name = "blob node"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blank_blob"
- max_integrity = 200
- armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 65, "acid" = 90)
- point_return = 18
-
-/obj/structure/blob/node/New(loc, h = 100)
- GLOB.blob_nodes += src
- START_PROCESSING(SSobj, src)
- ..(loc, h)
-
-/obj/structure/blob/node/adjustcolors(a_color)
- cut_overlays()
- color = null
- var/image/I = new('icons/mob/blob.dmi', "blob")
- I.color = a_color
- add_overlay(I)
- var/image/C = new('icons/mob/blob.dmi', "blob_node_overlay")
- add_overlay(C)
-
- if(blocks_emissive)
- add_overlay(get_emissive_block())
-
-/obj/structure/blob/node/Destroy()
- GLOB.blob_nodes -= src
- STOP_PROCESSING(SSobj, src)
- return ..()
-
-/obj/structure/blob/node/Life(seconds, times_fired)
- if(overmind)
- for(var/i = 1; i < 8; i += i)
- Pulse(5, i, overmind.blob_reagent_datum?.color)
- else
- for(var/i = 1; i < 8; i += i)
- Pulse(5, i, color)
- obj_integrity = min(max_integrity, obj_integrity + 1)
- color = null
diff --git a/code/game/gamemodes/blob/blobs/resource.dm b/code/game/gamemodes/blob/blobs/resource.dm
deleted file mode 100644
index 4ef14ed4963..00000000000
--- a/code/game/gamemodes/blob/blobs/resource.dm
+++ /dev/null
@@ -1,15 +0,0 @@
-/obj/structure/blob/resource
- name = "resource blob"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blob_resource"
- max_integrity = 60
- point_return = 12
- var/resource_delay = 0
-
-/obj/structure/blob/resource/run_action()
- if(resource_delay > world.time)
- return
- flick("blob_resource_glow", src)
- resource_delay = world.time + 40 // 4 seconds
- if(overmind)
- overmind.add_points(1)
diff --git a/code/game/gamemodes/blob/blobs/shield.dm b/code/game/gamemodes/blob/blobs/shield.dm
deleted file mode 100644
index f95fc50b910..00000000000
--- a/code/game/gamemodes/blob/blobs/shield.dm
+++ /dev/null
@@ -1,80 +0,0 @@
-/obj/structure/blob/shield
- name = "strong blob"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blob_shield"
- desc = "Some blob creature thingy"
- max_integrity = 150
- brute_resist = 0.25
- explosion_block = 3
- explosion_vertical_block = 2
- atmosblock = TRUE
- armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
-
-/obj/structure/blob/shield/core
- point_return = 0
-
-
-/obj/structure/blob/shield/check_integrity()
- var/old_compromised_integrity = compromised_integrity
- if(obj_integrity < max_integrity * 0.5)
- compromised_integrity = TRUE
- else
- compromised_integrity = FALSE
- if(old_compromised_integrity != compromised_integrity)
- update_state()
- update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_ICON_STATE)
-
-
-/obj/structure/blob/shield/update_state()
- if(compromised_integrity)
- atmosblock = FALSE
- else
- atmosblock = TRUE
- air_update_turf(1)
-
-
-/obj/structure/blob/shield/update_name(updates = ALL)
- . = ..()
- if(compromised_integrity)
- name = "weakened [initial(name)]"
- else
- name = initial(name)
-
-
-/obj/structure/blob/shield/update_desc(updates = ALL)
- . = ..()
- if(compromised_integrity)
- desc = "A wall of twitching tendrils."
- else
- desc = initial(desc)
-
-
-/obj/structure/blob/shield/update_icon_state()
- if(compromised_integrity)
- icon_state = "[initial(icon_state)]_damaged"
- else
- icon_state = initial(icon_state)
-
-
-/obj/structure/blob/shield/reflective
- name = "reflective blob"
- desc = "A solid wall of slightly twitching tendrils with a reflective glow."
- icon_state = "blob_glow"
- max_integrity = 100
- brute_resist = 0.5
- explosion_block = 2
- explosion_vertical_block = 1
- point_return = 9
- flags = CHECK_RICOCHET
-
-/obj/structure/blob/shield/reflective/handle_ricochet(obj/item/projectile/P)
- var/turf/p_turf = get_turf(P)
- var/face_direction = get_dir(src, p_turf)
- var/face_angle = dir2angle(face_direction)
- var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180))
- if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
- return FALSE
- var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
- P.set_angle(new_angle_s)
- visible_message("[P] reflects off [src]!")
- return TRUE
diff --git a/code/game/gamemodes/blob/blobs/storage.dm b/code/game/gamemodes/blob/blobs/storage.dm
deleted file mode 100644
index b9052b3601a..00000000000
--- a/code/game/gamemodes/blob/blobs/storage.dm
+++ /dev/null
@@ -1,16 +0,0 @@
-/obj/structure/blob/storage
- name = "storage blob"
- icon = 'icons/mob/blob.dmi'
- icon_state = "blob_resource"
- max_integrity = 30
- fire_resist = 2
- point_return = 12
-
-/obj/structure/blob/storage/obj_destruction(damage_flag)
- if(overmind)
- overmind.max_blob_points -= 50
- ..()
-
-/obj/structure/blob/storage/proc/update_max_blob_points(var/new_point_increase)
- if(overmind)
- overmind.max_blob_points += new_point_increase
diff --git a/code/game/gamemodes/blob/overmind.dm b/code/game/gamemodes/blob/overmind.dm
deleted file mode 100644
index d060d2c7890..00000000000
--- a/code/game/gamemodes/blob/overmind.dm
+++ /dev/null
@@ -1,134 +0,0 @@
-/mob/camera/blob
- name = "Blob Overmind"
- real_name = "Blob Overmind"
- icon = 'icons/mob/blob.dmi'
- icon_state = "marker"
- nightvision = 8
- sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
- invisibility = INVISIBILITY_OBSERVER
- lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
- mouse_opacity = MOUSE_OPACITY_OPAQUE
- see_invisible = SEE_INVISIBLE_LIVING
- pass_flags = PASSBLOB
- faction = list(ROLE_BLOB)
-
- var/obj/structure/blob/core/blob_core = null // The blob overmind's core
- var/blob_points = 0
- var/max_blob_points = 100
- var/last_attack = 0
- var/nodes_required = TRUE //if the blob needs nodes to place resource and factory blobs
- var/split_used = FALSE
- var/is_offspring = FALSE
- var/datum/reagent/blob/blob_reagent_datum = new/datum/reagent/blob()
- var/list/blob_mobs = list()
-
-/mob/camera/blob/New()
- var/new_name = "[initial(name)] ([rand(1, 999)])"
- name = new_name
- real_name = new_name
- last_attack = world.time
- ..()
- START_PROCESSING(SSobj, src)
-
-/mob/camera/blob/Destroy()
- STOP_PROCESSING(SSobj, src)
- return ..()
-
-/mob/camera/blob/process()
- if(!blob_core)
- qdel(src)
-
-/mob/camera/blob/Login()
- ..()
- sync_mind()
- update_health_hud()
- sync_lighting_plane_alpha()
-
-/mob/camera/blob/update_health_hud()
- if(blob_core && hud_used)
- hud_used.blobhealthdisplay.maptext = "
[round(blob_core.obj_integrity)]
"
-
-/mob/camera/blob/proc/add_points(var/points)
- if(points != 0)
- blob_points = clamp(blob_points + points, 0, max_blob_points)
- if(hud_used)
- hud_used.blobpwrdisplay.maptext = "[round(src.blob_points)]
"
-
-
-/mob/camera/blob/memory()
- SSticker.mode.update_blob_objective()
- ..()
-
-/mob/camera/blob/say(message)
- if(!message)
- return
-
- if(client)
- if(check_mute(client.ckey, MUTE_IC))
- to_chat(src, "You cannot send IC messages (muted).")
- return
- if(client.handle_spam_prevention(message, MUTE_IC))
- return
-
- if(stat)
- return
-
- blob_talk(message)
-
-
-/mob/camera/blob/proc/blob_talk(message)
- add_say_logs(src, message, language = "BLOB")
-
- message = trim(copytext(sanitize(message), 1, MAX_MESSAGE_LEN))
-
- if(!message)
- return
-
- var/rendered = "Blob Telepathy, [name]([blob_reagent_datum.name]) states, \"[message]\""
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobbernaut(M) || isblobinfected(M.mind))
- M.show_message(rendered, 2)
- else if(isobserver(M) && !isnewplayer(M))
- var/rendered_ghost = "Blob Telepathy, \
- [name]([blob_reagent_datum.name]) \
- (F) states, \"[message]\""
- M.show_message(rendered_ghost, 2)
-
-
-/mob/camera/blob/blob_act(obj/structure/blob/B)
- return
-
-/mob/camera/blob/get_status_tab_items()
- var/list/status_tab_data = ..()
- . = status_tab_data
- if(blob_core)
- status_tab_data[++status_tab_data.len] = list("Core Health:", "[blob_core.obj_integrity]")
- status_tab_data[++status_tab_data.len] = list("Power Stored:", "[blob_points]/[max_blob_points]")
-
-/mob/camera/blob/Move(atom/newloc, direct = NONE, glide_size_override = 0, update_dir = TRUE)
- if(world.time < last_movement)
- return
- last_movement = world.time + 0.5 // cap to 20fps
-
- var/obj/structure/blob/B = locate() in range("3x3", newloc)
- if(B)
- loc = newloc
- else
- return 0
-
-/mob/camera/blob/proc/can_attack()
- return (world.time > (last_attack + CLICK_CD_RANGE))
-
-/mob/camera/blob/proc/select_reagent()
- var/list/possible_reagents = list()
- var/datum/antagonist/blob_overmind/overmind_datum = mind?.has_antag_datum(/datum/antagonist/blob_overmind)
- if(!overmind_datum)
- for(var/type in subtypesof(/datum/reagent/blob))
- possible_reagents.Add(new type)
- blob_reagent_datum = pick(possible_reagents)
- else
- blob_reagent_datum = overmind_datum.reagent
- if(blob_core)
- blob_core.adjustcolors(blob_reagent_datum.color)
-
- color = blob_reagent_datum.complementary_color
diff --git a/code/game/gamemodes/blob/powers.dm b/code/game/gamemodes/blob/powers.dm
deleted file mode 100644
index f2ec8f7c9ac..00000000000
--- a/code/game/gamemodes/blob/powers.dm
+++ /dev/null
@@ -1,497 +0,0 @@
-// Point controlling procs
-
-/mob/camera/blob/proc/can_buy(var/cost = 15)
- if(blob_points < cost)
- to_chat(src, "Вы не можете себе это позволить!")
- return 0
- add_points(-cost)
- return 1
-
-// Power verbs
-
-/mob/camera/blob/verb/transport_core()
- set category = "Blob"
- set name = "Jump to Core"
- set desc = "Возвращает вас к вашему ядру."
-
- if(blob_core)
- src.loc = blob_core.loc
-
-/mob/camera/blob/verb/jump_to_node()
- set category = "Blob"
- set name = "Jump to Node"
- set desc = "Перемещает вас к выбранному узлу."
-
- if(GLOB.blob_nodes.len)
- var/list/nodes = list()
- for(var/i = 1; i <= GLOB.blob_nodes.len; i++)
- var/obj/structure/blob/node/B = GLOB.blob_nodes[i]
- nodes["Blob Node #[i] ([get_location_name(B)])"] = B
- var/node_name = input(src, "Выберете.", "Перемещение к узлу") in nodes
- var/obj/structure/blob/node/chosen_node = nodes[node_name]
- if(chosen_node)
- src.loc = chosen_node.loc
-
-/mob/camera/blob/verb/toggle_node_req()
- set category = "Blob"
- set name = "Toggle Node Requirement"
- set desc = "Переключить требование узла для размещения ресурсной плитки и фабрики."
- nodes_required = !nodes_required
- if(nodes_required)
- to_chat(src, "Теперь вам необходимо иметь узел или ядро рядом для размещения фабрики и ресурсной плитки.")
- else
- to_chat(src, "Теперь вам не нужно иметь узел или ядро рядом для размещения фабрики и ресурсной плитки.")
-
-/mob/camera/blob/verb/create_shield_power()
- set category = "Blob"
- set name = "Create/Upgrade Shield Blob (15)"
- set desc = "Создайте/улучшите крепкую плитку. Использование на существующей крепкой плитке превращает её в отражающую плитку, способную отражать большинство энергетических снарядов, но делая её намного слабее для остальных атак."
-
- var/turf/T = get_turf(src)
- create_shield(T)
-
-/mob/camera/blob/proc/create_shield(var/turf/T)
-
- var/obj/structure/blob/B = locate(/obj/structure/blob) in T
- var/obj/structure/blob/shield/S = locate(/obj/structure/blob/shield) in T
-
- if(!S)
- if(!B)//We are on a blob
- to_chat(src, "Тут нет плитки!")
- return
-
- else if(!istype(B, /obj/structure/blob/normal))
- to_chat(src, "Невозможно использовать на этой плитке. Найдите обычную плитку.")
- return
-
- else if(!can_buy(15))
- return
-
- B.color = blob_reagent_datum.color
- B.change_to(/obj/structure/blob/shield)
- else
-
- if(istype(S, /obj/structure/blob/shield/reflective))
- to_chat(src, "Здесь уже отражающая плитка!")
- return
-
-
- else if(S.obj_integrity < S.max_integrity * 0.5)
- to_chat(src, "Эта крепкая плитка слишком повреждена, чтобы ее можно было модифицировать!")
- return
-
- else if (!can_buy(15))
- return
-
- to_chat(src, "Вы выделяете отражающую слизь на крепкую плитку, позволяя ей отражать энергетические снаряды ценой снижения прочности.")
-
- S.change_to(/obj/structure/blob/shield/reflective)
- S.color = blob_reagent_datum.color
- return
-
-/mob/camera/blob/verb/create_resource()
- set category = "Blob"
- set name = "Create Resource Blob (40)"
- set desc = "Создайте ресурсную плитку, которая будет приносить вам ресурсы."
-
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/B = (locate(/obj/structure/blob) in T)
-
- if(!B)//We are on a blob
- to_chat(src, "Тут нет плитки!")
- return
-
- if(!istype(B, /obj/structure/blob/normal))
- to_chat(src, "Невозможно использовать на этой плитке. Найдите обычную плитку.")
- return
- for(var/obj/structure/blob/resource/blob in orange(4, T))
- to_chat(src, "Поблизости находится ресурсная плитка, отойдите на расстояние более 4 плиток от неё!")
- return
-
- if(nodes_required)
- if(!(locate(/obj/structure/blob/node) in orange(3, T)) && !(locate(/obj/structure/blob/core) in orange(4, T)))
- to_chat(src, "Вам нужно разместить этот объект ближе к узлу или ядру!")
- return //handholdotron 2000
-
- if(!can_buy(40))
- return
-
- B.color = blob_reagent_datum.color
- B.change_to(/obj/structure/blob/resource)
- var/obj/structure/blob/resource/R = locate() in T
- if(R)
- R.overmind = src
-
- return
-
-/mob/camera/blob/verb/create_node()
- set category = "Blob"
- set name = "Create Node Blob (60)"
- set desc = "Создает узел."
-
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/B = (locate(/obj/structure/blob) in T)
-
- if(!B)//We are on a blob
- to_chat(src, "Тут нет плитки блоба!")
- return
-
- if(!istype(B, /obj/structure/blob/normal))
- to_chat(src, "Невозможно использовать на этой плитке. Найдите обычную плитку.")
- return
-
- for(var/obj/structure/blob/node/blob in orange(5, T))
- to_chat(src, "Поблизости находится узел, отойдите на расстояние более 5 плиток от него!")
- return
-
- if(!can_buy(60))
- return
-
- B.change_to(/obj/structure/blob/node)
- var/obj/structure/blob/node/R = locate() in T
- if(R)
- R.adjustcolors(blob_reagent_datum.color)
- R.overmind = src
- return
-
-
-/mob/camera/blob/verb/create_factory()
- set category = "Blob"
- set name = "Create Factory Blob (60)"
- set desc = "Создает плитку, производящую споры."
-
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/B = locate(/obj/structure/blob) in T
- if(!B)
- to_chat(src, "Тут нет плитки!")
- return
-
- if(!istype(B, /obj/structure/blob/normal))
- to_chat(src, "Невозможно использовать на этой плитке. Найдите обычную плитку.")
- return
-
- for(var/obj/structure/blob/factory/blob in orange(7, T))
- to_chat(src, "Поблизости находится фабрика, отойдите на расстояние более 7 плиток от неё!")
- return
-
- if(nodes_required)
- if(!(locate(/obj/structure/blob/node) in orange(3, T)) && !(locate(/obj/structure/blob/core) in orange(4, T)))
- to_chat(src, "Вам нужно разместить этот объект ближе к узлу или ядру!")
- return //handholdotron 2000
-
- if(!can_buy(60))
- return
-
- B.change_to(/obj/structure/blob/factory)
- B.color = blob_reagent_datum.color
- var/obj/structure/blob/factory/R = locate() in T
- if(R)
- R.overmind = src
- return
-
-
-/mob/camera/blob/verb/create_blobbernaut()
- set category = "Blob"
- set name = "Create Blobbernaut (60)"
- set desc = "Создает сильное порождение блоба. Блобернаута!"
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/B = locate(/obj/structure/blob) in T
- if(!B)
- to_chat(src, "Вы должны быть на плитке блоба!")
- return FALSE
-
- if(!istype(B, /obj/structure/blob/factory))
- to_chat(src, "Невозможно использовать эту плитку, найдите фабрику.")
- return FALSE
- var/obj/structure/blob/factory/b_fac = B
-
- if(b_fac.is_waiting_spawn)
- return FALSE
-
- if(!can_buy(60))
- return FALSE
-
- spawn()
- var/mob/C
- b_fac.is_waiting_spawn = TRUE
-
- var/list/candidates = SSghost_spawns.poll_candidates("Вы хотите сыграть за блобернаута?", ROLE_BLOB, TRUE, 10 SECONDS, source = /mob/living/simple_animal/hostile/blob/blobbernaut)
- if(length(candidates))
- C = pick(candidates)
-
- if(!C)
- add_points(60)
- b_fac.is_waiting_spawn = FALSE
-
- if(b_fac && b_fac.is_waiting_spawn) //Если фабрика цела и её не разрушили во время голосования
- var/mob/living/simple_animal/hostile/blob/blobbernaut/blobber = new (get_turf(b_fac))
- qdel(b_fac)
- blobber.key = C.key
- log_game("[blobber.key] has spawned as Blobbernaut")
- to_chat(blobber, "Вы блобернаут! Вы должны помочь всем формам блоба в их миссии по уничтожению всего!")
- to_chat(blobber, "Вы исцеляетесь, стоя на плитках блоба, однако вы будете медленно разлагаться, если получите урон за пределами блоба.")
-
- blobber.color = blob_reagent_datum.complementary_color
- blobber.overmind = src
- blob_mobs.Add(blobber)
- blobber.AIStatus = AI_OFF
- blobber.LoseTarget()
- addtimer(CALLBACK(blobber, TYPE_PROC_REF(/mob/living/simple_animal/hostile/blob/blobbernaut/, add_to_gamemode)), TIME_TO_ADD_OM_DATUM)
- return TRUE
-
-
-/mob/camera/blob/verb/relocate_core()
- set category = "Blob"
- set name = "Relocate Core (80)"
- set desc = "Перемещает ваше ядро на узел, на котором вы находитесь, ваше старое ядро будет превращено в узел."
-
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/node/B = locate(/obj/structure/blob/node) in T
- if(!B)
- to_chat(src, "Вы должны быть на узле!")
- return
-
- if(!can_buy(80))
- return
-
- // The old switcharoo.
- var/turf/old_turf = blob_core.loc
- blob_core.loc = T
- B.loc = old_turf
- return
-
-
-/mob/camera/blob/verb/revert()
- set category = "Blob"
- set name = "Remove Blob"
- set desc = "Удаляет плитку. Вы получите 30 % возмещение стоимости специальных структур блоба."
-
- var/turf/T = get_turf(src)
- remove_blob(T)
-
-/mob/camera/blob/proc/remove_blob(var/turf/T)
-
- var/obj/structure/blob/B = locate(/obj/structure/blob) in T
- if(!T)
- return
- if(!B)
- to_chat(src, "Тут нет плитки блоба!")
- return
- if(B.point_return < 0)
- to_chat(src, "Невозможно удалить эту плитку!")
- return
- if(max_blob_points < B.point_return + blob_points)
- to_chat(src, "У вас слишком много ресурсов для удаления этой плитки!")
- return
- if(B.point_return)
- add_points(B.point_return)
- to_chat(src, "Получено [B.point_return] ресурса после удаления \the [B].")
- qdel(B)
- return
-
-
-/mob/camera/blob/verb/expand_blob_power()
- set category = "Blob"
- set name = "Expand/Attack Blob (5)"
- set desc = "Пытается создать новую плитку блоба в этом тайле. Если тайл не чист, мы наносим урон объекту, находящемуся в нем, что может его очистить."
-
- var/turf/T = get_turf(src)
- expand_blob(T)
-
-/mob/camera/blob/proc/expand_blob(var/turf/T)
- if(!T)
- return
-
- if(!can_attack())
- return
-
- if(!is_location_within_transition_boundaries(T))
- to_chat(src, "Вы не можете расширяться сюда...")
- return
-
- var/obj/structure/blob/B = locate() in T
- if(B)
- to_chat(src, "Здесь уже есть плитка!")
- return
-
- var/obj/structure/blob/OB = locate() in circlerange(T, 1)
- if(!OB)
- to_chat(src, "Рядом с вами нет ни одной плитки.")
- return
-
- if(!((locate(/mob/living) in T) || can_buy(5)))
- return
- last_attack = world.time
- OB.expand(T, 0, blob_reagent_datum.color)
- for(var/mob/living/L in T)
- if(ROLE_BLOB in L.faction) //no friendly/dead fire
- continue
- var/mob_protection = L.get_permeability_protection()
- blob_reagent_datum.reaction_mob(L, REAGENT_TOUCH, 25, 1, mob_protection)
- blob_reagent_datum.send_message(L)
- OB.color = blob_reagent_datum.color
- return
-
-
-/mob/camera/blob/verb/rally_spores_power()
- set category = "Blob"
- set name = "Rally Spores"
- set desc = "Направьте споры, чтоб они переместились в выбранное место."
-
- var/turf/T = get_turf(src)
- rally_spores(T)
-
-/mob/camera/blob/proc/rally_spores(var/turf/T)
- to_chat(src, "Вы направляете свои споры.")
-
- var/list/surrounding_turfs = block(T.x - 1, T.y - 1, T.z, T.x + 1, T.y + 1, T.z)
- if(!surrounding_turfs.len)
- return
-
- for(var/mob/living/simple_animal/hostile/blob/blobspore/BS in GLOB.alive_mob_list)
- if(isturf(BS.loc) && get_dist(BS, T) <= 35)
- BS.LoseTarget()
- BS.Goto(pick(surrounding_turfs), BS.move_to_delay)
- return
-
-/mob/camera/blob/verb/split_consciousness()
- set category = "Blob"
- set name = "Split consciousness (100) (One use)"
- set desc = "Тратьте ресурсы, чтобы попытаться создать еще одного блоба."
-
- var/turf/T = get_turf(src)
- if(!T)
- return
- if(split_used)
- to_chat(src, "Вы уже произвели потомка.")
- return
- if(is_offspring)
- to_chat(src, "Потомки блоба не могут производить потомков.")
- return
-
- var/obj/structure/blob/N = (locate(/obj/structure/blob) in T)
- if(!N)
- to_chat(src, "Для создания вашего потомка необходим узел.")
- return
- if(!istype(N, /obj/structure/blob/node))
- to_chat(src, "Для создания вашего потомка необходим узел.")
- return
- if(!can_buy(100))
- return
-
- split_used = TRUE
-
- new /obj/structure/blob/core/ (get_turf(N), 200, null, blob_core.point_rate, offspring = TRUE)
- qdel(N)
-
-
-/mob/camera/blob/verb/blob_broadcast()
- set category = "Blob"
- set name = "Blob Broadcast"
- set desc = "Говорите, используя споры и блобернаутов в качестве рупоров. Это действие бесплатно."
-
- var/speak_text = clean_input("Что вы хотите сказать от лица ваших созданий?", "Blob Broadcast", null)
-
- if(!speak_text)
- return
- else
- to_chat(usr, "Вы говорите от лица ваших созданий, [speak_text]")
- for(var/mob/living/simple_animal/hostile/blob_minion in blob_mobs)
- if(blob_minion.stat == CONSCIOUS)
- blob_minion.say(speak_text)
- return
-
-/mob/camera/blob/verb/create_storage()
- set category = "Blob"
- set name = "Create Storage Blob (40)"
- set desc = "Создаёт хранилище, которая будет накапливать дополнительные ресурсы для вас. Это увеличивает ваш максимальный предел ресурсов на 50."
-
-
- var/turf/T = get_turf(src)
-
- if(!T)
- return
-
- var/obj/structure/blob/B = (locate(/obj/structure/blob) in T)
-
- if(!B)//We are on a blob
- to_chat(src, "Тут нет плитки блоба!")
- return
-
- if(!istype(B, /obj/structure/blob/normal))
- to_chat(src, "Невозможно использовать эту плитку, найдите обычную.")
- return
-
- for(var/obj/structure/blob/storage/blob in orange(3, T))
- to_chat(src, "Поблизости находится хранилище, отойдите на расстояние более 4 плиток от него!")
- return
-
- if(!can_buy(40))
- return
-
- B.color = blob_reagent_datum.color
- B.change_to(/obj/structure/blob/storage)
- var/obj/structure/blob/storage/R = locate() in T
- if(R)
- R.overmind = src
- R.update_max_blob_points(50)
-
- return
-
-
-/mob/camera/blob/verb/chemical_reroll()
- set category = "Blob"
- set name = "Reactive Chemical Adaptation (50)"
- set desc = "Заменяет ваш химикат на другой случайным образом."
-
- if(!can_buy(50))
- return
-
- var/datum/reagent/blob/B = pick((subtypesof(/datum/reagent/blob) - blob_reagent_datum.type))
- blob_reagent_datum = new B
- var/datum/antagonist/blob_overmind/overmind_datum = mind.has_antag_datum(/datum/antagonist/blob_overmind)
- if(overmind_datum)
- overmind_datum.reagent = blob_reagent_datum
- color = blob_reagent_datum.complementary_color
-
- for(var/obj/structure/blob/BL in GLOB.blobs)
- BL.adjustcolors(blob_reagent_datum.color)
-
- for(var/mob/living/simple_animal/hostile/blob/BLO)
- BLO.adjustcolors(blob_reagent_datum.complementary_color)
-
- to_chat(src, "Ваш новый реагент: [blob_reagent_datum.name] - [blob_reagent_datum.description]")
-
-/mob/camera/blob/verb/blob_help()
- set category = "Blob"
- set name = "*Blob Help*"
- set desc = "Help on how to blob."
- for (var/message in get_blob_help_messages(blob_reagent_datum))
- to_chat(src, message)
-
-
diff --git a/code/game/gamemodes/blob/theblob.dm b/code/game/gamemodes/blob/theblob.dm
deleted file mode 100644
index 349298f8b1f..00000000000
--- a/code/game/gamemodes/blob/theblob.dm
+++ /dev/null
@@ -1,300 +0,0 @@
-//I will need to recode parts of this but I am way too tired atm
-/obj/structure/blob
- name = "blob"
- icon = 'icons/mob/blob.dmi'
- light_range = 3
- desc = "Some blob creature thingy"
- density = FALSE
- opacity = TRUE
- anchored = TRUE
- pass_flags_self = PASSBLOB
- can_astar_pass = CANASTARPASS_ALWAYS_PROC
- max_integrity = 30
- armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 70)
- var/point_return = 0 //How many points the blob gets back when it removes a blob of that type. If less than 0, blob cannot be removed.
- var/health_timestamp = 0
- var/brute_resist = 0.5 //multiplies brute damage by this
- var/fire_resist = 1 //multiplies burn damage by this
- var/atmosblock = FALSE //if the blob blocks atmos and heat spread
- /// If a threshold is reached, resulting in shifting variables
- var/compromised_integrity = FALSE
- var/mob/camera/blob/overmind
- creates_cover = TRUE
- obj_flags = BLOCK_Z_OUT_DOWN | BLOCK_Z_IN_UP // stops blob mobs from falling on multiz.
-
-
-/obj/structure/blob/Initialize(mapload)
- . = ..()
- GLOB.blobs += src
- setDir(pick(GLOB.cardinal))
- check_integrity()
- if(atmosblock)
- air_update_turf(1)
- ConsumeTile()
- var/static/list/loc_connections = list(
- COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
- )
- AddElement(/datum/element/connect_loc, loc_connections)
-
-
-/obj/structure/blob/Destroy()
- if(atmosblock)
- atmosblock = FALSE
- air_update_turf(1)
- GLOB.blobs -= src
- if(isturf(loc)) //Necessary because Expand() is screwed up and spawns a blob and then deletes it
- playsound(src.loc, 'sound/effects/splat.ogg', 50, 1)
- return ..()
-
-/obj/structure/blob/has_prints()
- return FALSE
-
-/obj/structure/blob/BlockSuperconductivity()
- return atmosblock
-
-/obj/structure/blob/proc/check_integrity()
- return
-
-/obj/structure/blob/proc/update_state()
- return
-
-/obj/structure/blob/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- return checkpass(mover, PASSBLOB)
-
-/obj/structure/blob/CanAtmosPass(turf/T, vertical)
- return !atmosblock
-
-
-/obj/structure/blob/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
- return pass_info.pass_flags == PASSEVERYTHING || (pass_info.pass_flags & PASSBLOB)
-
-
-/obj/structure/blob/process()
- Life()
- return
-
-/obj/structure/blob/blob_act(obj/structure/blob/B)
- return
-
-/obj/structure/blob/proc/Life()
- return
-
-/obj/structure/blob/proc/RegenHealth()
- // All blobs heal over time when pulsed, but it has a cool down
- if(health_timestamp > world.time)
- return 0
- if(obj_integrity < max_integrity)
- obj_integrity = min(max_integrity, obj_integrity + 1)
- check_integrity()
- health_timestamp = world.time + 10 // 1 seconds
-
-
-/obj/structure/blob/proc/Pulse(var/pulse = 0, var/origin_dir = 0, var/a_color)//Todo: Fix spaceblob expand
- RegenHealth()
-
- if(run_action())//If we can do something here then we dont need to pulse more
- return
-
- if(pulse > 30)
- return//Inf loop check
-
- //Looking for another blob to pulse
- var/list/dirs = list(1,2,4,8)
- dirs.Remove(origin_dir)//Dont pulse the guy who pulsed us
- for(var/i = 1 to 4)
- if(!dirs.len) break
- var/dirn = pick(dirs)
- dirs.Remove(dirn)
- var/turf/T = get_step(src, dirn)
- if(!is_location_within_transition_boundaries(T))
- continue
- var/obj/structure/blob/B = (locate(/obj/structure/blob) in T)
- if(!B)
- expand(T,1,a_color)//No blob here so try and expand
- return
- B.adjustcolors(a_color)
-
- B.Pulse((pulse+1),get_dir(src.loc,T), a_color)
- return
- return
-
-
-/obj/structure/blob/proc/run_action()
- return 0
-
-/obj/structure/blob/proc/ConsumeTile()
- for(var/atom/A in loc)
- A.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
-
-/obj/structure/blob/proc/expand(var/turf/T = null, var/prob = 1, var/a_color)
- if(prob && !prob(obj_integrity))
- return
- if(isspaceturf(T) && prob(75)) return
- if(!T)
- var/list/dirs = list(1,2,4,8)
- for(var/i = 1 to 4)
- var/dirn = pick(dirs)
- dirs.Remove(dirn)
- T = get_step(src, dirn)
- if(!(locate(/obj/structure/blob) in T)) break
- else T = null
-
- if(!T) return 0
- if(!is_location_within_transition_boundaries(T))
- return
- var/obj/structure/blob/normal/B = new /obj/structure/blob/normal(src.loc, min(obj_integrity, 30))
- B.color = a_color
- B.set_density(TRUE)
- if(T.Enter(B,src))//Attempt to move into the tile
- B.set_density(initial(B.density))
- B.loc = T
- else
- T.blob_act()//If we cant move in hit the turf
- B.loc = null //So we don't play the splat sound, see Destroy()
- qdel(B)
-
- for(var/atom/A in T)//Hit everything in the turf
- A.blob_act(src)
- return 1
-
-
-/obj/structure/blob/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
- SIGNAL_HANDLER
-
- arrived.blob_act(src)
-
-
-/obj/structure/blob/tesla_act(power)
- ..()
- take_damage(power / 400, BURN, "energy")
-
-
-/obj/structure/blob/attack_animal(mob/living/simple_animal/M)
- if(ROLE_BLOB in M.faction) //sorry, but you can't kill the blob as a blobbernaut
- return
- ..()
-
-/obj/structure/blob/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
- switch(damage_type)
- if(BRUTE)
- if(damage_amount)
- playsound(src.loc, 'sound/effects/attackblob.ogg', 50, TRUE)
- else
- playsound(src, 'sound/weapons/tap.ogg', 50, TRUE)
- if(BURN)
- playsound(src.loc, 'sound/items/welder.ogg', 100, TRUE)
-
-/obj/structure/blob/run_obj_armor(damage_amount, damage_type, damage_flag = 0, attack_dir)
- switch(damage_type)
- if(BRUTE)
- damage_amount *= brute_resist
- if(BURN)
- damage_amount *= fire_resist
- if(CLONE)
- else
- return 0
- var/armor_protection = 0
- if(damage_flag)
- armor_protection = armor.getRating(damage_flag)
- damage_amount = round(damage_amount * (100 - armor_protection)*0.01, 0.1)
- if(overmind?.blob_reagent_datum && damage_flag)
- damage_amount = overmind.blob_reagent_datum.damage_reaction(src, damage_amount, damage_type, damage_flag)
- return damage_amount
-
-/obj/structure/blob/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
- if(QDELETED(src))
- return
- . = ..()
- if(. && obj_integrity > 0)
- check_integrity()
-
-/obj/structure/blob/proc/change_to(var/type)
- if(!ispath(type))
- error("[type] is an invalid type for the blob.")
- var/obj/structure/blob/B = new type(src.loc)
- if(!istype(type, /obj/structure/blob/core) || !istype(type, /obj/structure/blob/node))
- B.color = color
- else
- B.adjustcolors(color)
- qdel(src)
-
-/obj/structure/blob/proc/adjustcolors(var/a_color)
- if(a_color)
- color = a_color
-
-
-/obj/structure/blob/examine(mob/user)
- . = ..()
- . += "It looks like it's made of [get_chem_name()]."
- . += "It looks like this chemical does: [get_chem_desc()]."
-
-/obj/structure/blob/proc/get_chem_name()
- for(var/mob/camera/blob/B in GLOB.mob_list)
- if(!QDELETED(B) && lowertext(B.blob_reagent_datum.color) == lowertext(src.color)) // Goddamit why we use strings for these
- return B.blob_reagent_datum.name
- return "unknown"
-
-/obj/structure/blob/proc/get_chem_desc()
- for(var/mob/camera/blob/B in GLOB.mob_list)
- if(!QDELETED(B) && lowertext(B.blob_reagent_datum.color) == lowertext(src.color)) // Goddamit why we use strings for these
- return B.blob_reagent_datum.description
- return "something unknown"
-
-
-/obj/structure/blob/hit_by_thrown_carbon(mob/living/carbon/human/C, datum/thrownthing/throwingdatum, damage, mob_hurt, self_hurt)
- damage *= 0.25 // Lets not have sorium be too much of a blender / rapidly kill itself
- return ..()
-
-
-/obj/structure/blob/normal
- icon_state = "blob"
- light_range = 0
- obj_integrity = 21 //doesn't start at full health
- max_integrity = 25
- brute_resist = 0.25
-
-
-/obj/structure/blob/normal/check_integrity()
- var/old_compromised_integrity = compromised_integrity
- if(obj_integrity <= 15)
- compromised_integrity = TRUE
- else
- compromised_integrity = FALSE
- if(old_compromised_integrity != compromised_integrity)
- update_state()
- update_appearance(UPDATE_NAME|UPDATE_DESC|UPDATE_ICON_STATE)
-
-
-/obj/structure/blob/normal/update_state()
- if(compromised_integrity)
- brute_resist = 0.5
- else
- brute_resist = 0.25
-
-
-/obj/structure/blob/normal/update_name(updates = ALL)
- . = ..()
- if(compromised_integrity)
- name = "fragile blob"
- else
- name = "[overmind ? "blob" : "dead blob"]"
-
-
-/obj/structure/blob/normal/update_desc(updates = ALL)
- . = ..()
- if(compromised_integrity)
- desc = "A thin lattice of slightly twitching tendrils."
- else
- desc = "A thick wall of [overmind ? "writhing" : "lifeless"] tendrils."
-
-
-/obj/structure/blob/normal/update_icon_state()
- if(compromised_integrity)
- icon_state = "blob_damaged"
- else
- icon_state = "blob"
-
-
diff --git a/code/game/gamemodes/devil/true_devil/_true_devil.dm b/code/game/gamemodes/devil/true_devil/_true_devil.dm
index 9ec8575a5cc..35bebd86ea2 100644
--- a/code/game/gamemodes/devil/true_devil/_true_devil.dm
+++ b/code/game/gamemodes/devil/true_devil/_true_devil.dm
@@ -12,6 +12,7 @@
status_flags = CANPUSH
universal_understand = TRUE
universal_speak = TRUE //The devil speaks all languages meme
+ hud_type = /datum/hud/devil
var/ascended = FALSE
var/mob/living/oldform
@@ -36,7 +37,7 @@
health = maxHealth
icon_state = "arch_devil"
-/mob/living/carbon/true_devil/proc/set_name()
+/mob/living/carbon/true_devil/set_name()
name = mind.devilinfo.truename
real_name = name
diff --git a/code/game/gamemodes/miniantags/bot_swarm/swarmer.dm b/code/game/gamemodes/miniantags/bot_swarm/swarmer.dm
index ce9d67f7946..d8c11afc2fb 100644
--- a/code/game/gamemodes/miniantags/bot_swarm/swarmer.dm
+++ b/code/game/gamemodes/miniantags/bot_swarm/swarmer.dm
@@ -102,6 +102,8 @@
light_on = FALSE
universal_speak = 0
universal_understand = 0
+ wet_immunity = TRUE
+ hud_type = /datum/hud/swarmer
var/resources = 0 //Resource points, generated by consuming metal/glass
var/max_resources = 200
diff --git a/code/game/gamemodes/miniantags/demons/demon.dm b/code/game/gamemodes/miniantags/demons/demon.dm
index 60f8b2bac62..ca8ee147704 100644
--- a/code/game/gamemodes/miniantags/demons/demon.dm
+++ b/code/game/gamemodes/miniantags/demons/demon.dm
@@ -31,6 +31,7 @@
del_on_death = TRUE
dirslash_enabled = TRUE
slowed_by_pull_and_push = FALSE
+ wet_immunity = TRUE
var/vialspawned = FALSE
var/playstyle_string
var/datum/action/innate/demon/whisper/whisper_action
diff --git a/code/game/gamemodes/miniantags/guardian/guardian.dm b/code/game/gamemodes/miniantags/guardian/guardian.dm
index 5fab14d1b2f..df58b9822b3 100644
--- a/code/game/gamemodes/miniantags/guardian/guardian.dm
+++ b/code/game/gamemodes/miniantags/guardian/guardian.dm
@@ -29,6 +29,7 @@
move_resist = MOVE_FORCE_STRONG
AIStatus = AI_OFF
butcher_results = list(/obj/item/reagent_containers/food/snacks/ectoplasm = 1)
+ hud_type = /datum/hud/guardian
var/summoned = FALSE
var/cooldown = 0
var/damage_transfer = 1 //how much damage from each attack we transfer to the owner
diff --git a/code/game/gamemodes/miniantags/revenant/revenant.dm b/code/game/gamemodes/miniantags/revenant/revenant.dm
index a9c47c8f336..9b615961223 100644
--- a/code/game/gamemodes/miniantags/revenant/revenant.dm
+++ b/code/game/gamemodes/miniantags/revenant/revenant.dm
@@ -36,6 +36,7 @@
mob_size = MOB_SIZE_TINY
pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
speed = 1
+ wet_immunity = TRUE
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
tts_seed = "Sylvanas"
diff --git a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
index 4685377069f..32dd628f828 100644
--- a/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
+++ b/code/game/gamemodes/miniantags/revenant/revenant_abilities.dm
@@ -439,7 +439,7 @@
var/list/potential_victims = list()
for(var/mob/living/carbon/potential_victim in range(aoe_range, get_turf(possessed_object)))
- if(!can_see(possessed_object, potential_victim, aoe_range)) // You can't see me
+ if(!possessed_object.can_see(potential_victim, aoe_range)) // You can't see me
continue
if(potential_victim.stat != CONSCIOUS) // Don't kill our precious essence-filled sleepy mobs
diff --git a/code/game/gamemodes/nuclear/nuclearbomb.dm b/code/game/gamemodes/nuclear/nuclearbomb.dm
index b55f4f98e5e..900b94fd7d1 100644
--- a/code/game/gamemodes/nuclear/nuclearbomb.dm
+++ b/code/game/gamemodes/nuclear/nuclearbomb.dm
@@ -513,9 +513,9 @@ GLOBAL_VAR(bomb_set)
return
if(locate(/obj/structure/blob) in T)
return
- var/obj/structure/blob/captured_nuke/N = new(T, src)
+ var/obj/structure/blob/special/captured_nuke/N = new(T, src)
N.overmind = B.overmind
- N.adjustcolors(B.color)
+ N.update_blob()
/obj/machinery/nuclearbomb/tesla_act(power, explosive)
..()
diff --git a/code/game/machinery/camera/motion.dm b/code/game/machinery/camera/motion.dm
index f937c2d75e2..d8097fc37c9 100644
--- a/code/game/machinery/camera/motion.dm
+++ b/code/game/machinery/camera/motion.dm
@@ -58,7 +58,7 @@
/// Returns TRUE if the camera can see the target.
-/obj/machinery/camera/proc/can_see(atom/target, length = 7) // I stole this from global and modified it to work with Xray cameras.
+/obj/machinery/camera/can_see(atom/target, length = 7) // I stole this from global and modified it to work with Xray cameras.
if(!target || target.invisibility > SEE_INVISIBLE_LIVING || target.alpha == NINJA_ALPHA_INVISIBILITY)
return FALSE
var/turf/current_turf = get_turf(src)
diff --git a/code/game/machinery/portable_turret.dm b/code/game/machinery/portable_turret.dm
index e0efa1afea8..ec2da9ef99d 100644
--- a/code/game/machinery/portable_turret.dm
+++ b/code/game/machinery/portable_turret.dm
@@ -520,7 +520,7 @@ GLOBAL_LIST_EMPTY(turret_icons)
//Verify that targeted atoms are in our sight. Otherwise, just remove them from processing.
for(var/atom/movable/atom as anything in processing_targets)
- if(!can_see(src, atom, scan_range))
+ if(!can_see(atom, scan_range))
processing_targets -= atom
var/list/primary_candidates
diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm
index 716c499ae69..ea14348a030 100644
--- a/code/game/mecha/mecha.dm
+++ b/code/game/mecha/mecha.dm
@@ -731,7 +731,7 @@
/obj/mecha/blob_act(obj/structure/blob/B)
log_message("Attack by blob. Attacker - [B].")
- take_damage(30, BRUTE, "melee", 0, get_dir(src, B))
+ take_damage(30, BRUTE, MELEE, 0, get_dir(src, B))
/obj/mecha/attack_tk()
return
diff --git a/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm b/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm
new file mode 100644
index 00000000000..317b56ce6a8
--- /dev/null
+++ b/code/game/objects/effects/effect_system/fluid_spread/_fluid_spread.dm
@@ -0,0 +1,156 @@
+/////////////////////////////////////////////
+//// SMOKE SYSTEMS
+/////////////////////////////////////////////
+
+/**
+ * A group of fluid objects.
+ */
+/datum/fluid_group
+ /// The set of fluid objects currently in this group.
+ var/list/nodes
+ /// The number of fluid object that this group wants to have contained.
+ var/target_size
+ /// The total number of fluid objects that have ever been in this group.
+ var/total_size = 0
+
+/datum/fluid_group/New(target_size = 0)
+ . = ..()
+ src.nodes = list()
+ src.target_size = target_size
+
+/datum/fluid_group/Destroy(force)
+ QDEL_LAZYLIST(nodes)
+ return ..()
+
+/**
+ * Adds a fluid node to this fluid group.
+ *
+ * Is a noop if the node is already in the group.
+ * Removes the node from any other fluid groups it is in.
+ * Syncs the group of the node with the group it is being added to (this one).
+ * Increments the total size of the fluid group.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be added to this group.
+ *
+ * Returns:
+ * - [TRUE]: If the node to be added is in this group by the end of the proc.
+ * - [FALSE]: Otherwise.
+ */
+/datum/fluid_group/proc/add_node(obj/effect/particle_effect/fluid/node)
+ if(!istype(node))
+ CRASH("Attempted to add non-fluid node [isnull(node) ? "NULL" : node] to a fluid group.")
+ if(QDELING(node))
+ CRASH("Attempted to add qdeling node to a fluid group")
+
+ if(node.group)
+ if(node.group == src)
+ return TRUE
+ if(!node.group.remove_node(node))
+ return FALSE
+
+ nodes += node
+ node.group = src
+ total_size++
+ return TRUE
+
+
+/**
+ * Removes a fluid node from this fluid group.
+ *
+ * Is a noop if the node is not in this group.
+ * Nulls the nodes fluid group ref to sync it with its new state.
+ * DOES NOT decrement the total size of the fluid group.
+ *
+ * Arguments:
+ * - [node][/obj/effect/particle_effect/fluid]: The fluid node that is going to be removed from this group.
+ *
+ * Returns:
+ * - [TRUE]: If the node to be removed is not in the group by the end of the proc.
+ */
+/datum/fluid_group/proc/remove_node(obj/effect/particle_effect/fluid/node)
+ if(node.group != src)
+ return TRUE
+
+ nodes -= node
+ node.group = null
+ return TRUE // Note: does not decrement total size since we don't want the group to expand again when it begins to dissipate or it will never stop.
+
+
+/**
+ * A particle effect that belongs to a fluid group.
+ */
+/obj/effect/particle_effect/fluid
+ name = "fluid"
+ /// The fluid group that this particle effect belongs to.
+ var/datum/fluid_group/group
+ /// What SSfluid bucket this particle effect is currently in.
+ var/tmp/effect_bucket
+ /// The index of the fluid spread bucket this is being spread in.
+ var/tmp/spread_bucket
+
+/obj/effect/particle_effect/fluid/Initialize(mapload, datum/fluid_group/group, obj/effect/particle_effect/fluid/source)
+ . = ..()
+ if(!group)
+ group = source?.group || new
+ group.add_node(src)
+ source?.transfer_fingerprints_to(src)
+
+/obj/effect/particle_effect/fluid/Destroy()
+ group.remove_node(src)
+ return ..()
+
+/**
+ * Attempts to spread this fluid node to wherever it can spread.
+ *
+ * Exact results vary by subtype implementation.
+ */
+/obj/effect/particle_effect/fluid/proc/spread()
+ CRASH("The base fluid spread proc is not implemented and should not be called. You called it.")
+
+
+/**
+ * A factory which produces fluid groups.
+ */
+/datum/effect_system/fluid_spread
+ effect_type = /obj/effect/particle_effect/fluid
+ /// The amount of smoke to produce.
+ var/amount = 10
+
+/datum/effect_system/fluid_spread/set_up(range = 1, amount = DIAMOND_AREA(range), atom/holder, atom/location, ...)
+ src.holder = holder
+ src.location = location
+ src.amount = amount
+
+/datum/effect_system/fluid_spread/start(log = FALSE)
+ var/location = src.location || get_turf(holder)
+ var/obj/effect/particle_effect/fluid/flood = new effect_type(location, new /datum/fluid_group(amount))
+ if(log) // Smoke is used as an aesthetic effect in a tonne of places and we don't want, say, a broken secway spamming admin chat.
+ help_out_the_admins(flood, holder, location)
+ flood.spread()
+
+/**
+ * Handles logging the beginning of a fluid flood.
+ *
+ * Arguments:
+ * - [flood][/obj/effect/particle_effect/fluid]: The first cell of the fluid flood.
+ * - [holder][/atom]: What the flood originated from.
+ * - [location][/atom]: Where the flood originated.
+ */
+/datum/effect_system/fluid_spread/proc/help_out_the_admins(obj/effect/particle_effect/fluid/flood, atom/holder, atom/location)
+ var/source_msg
+ var/blame_msg
+ if(holder)
+ holder.transfer_fingerprints_to(flood) // This is important. If this doesn't exist thermobarics are annoying to adjudicate.
+
+ source_msg = "from inside of [ismob(holder) ? ADMIN_LOOKUPFLW(holder) : ADMIN_VERBOSEJMP(holder)]"
+ var/lastkey = holder.fingerprintslast
+ if(lastkey)
+ var/mob/scapegoat = get_mob_by_key(lastkey)
+ blame_msg = " last touched by [ADMIN_LOOKUPFLW(scapegoat)]"
+ else
+ blame_msg = " with no known fingerprints"
+ else
+ source_msg = "with no known source"
+ message_admins("\A [flood] flood started at [ADMIN_VERBOSEJMP(location)] [source_msg][blame_msg].")
+ log_game("\A [flood] flood started at [location || "nonexistant location"] [holder ? "from [holder] last touched by [holder || "N/A"]" : "with no known source"].")
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
new file mode 100644
index 00000000000..fe5681a9437
--- /dev/null
+++ b/code/game/objects/effects/effect_system/fluid_spread/effects_smoke.dm
@@ -0,0 +1,462 @@
+/**
+ * A fluid which spreads through the air affecting every mob it engulfs.
+ */
+/obj/effect/particle_effect/fluid/smoke
+ name = "smoke"
+ icon = 'icons/effects/96x96.dmi'
+ icon_state = "smoke"
+ pixel_x = -32
+ pixel_y = -32
+ opacity = TRUE
+ plane = ABOVE_GAME_PLANE
+ layer = FLY_LAYER
+ anchored = TRUE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ animate_movement = FALSE
+ /// How long the smoke sticks around before it dissipates.
+ var/lifetime = 10 SECONDS
+ /// Makes the smoke react to changes on/of its turf.
+ var/static/loc_connections = list(
+ COMSIG_TURF_CALCULATED_ADJACENT_ATMOS = PROC_REF(react_to_atmos_adjacency_changes)
+ )
+
+/obj/effect/particle_effect/fluid/smoke/Initialize(mapload, datum/fluid_group/group, ...)
+ . = ..()
+ create_reagents(1000)
+ setDir(pick(GLOB.cardinal))
+ AddElement(/datum/element/connect_loc, loc_connections)
+ SSsmoke.start_processing(src)
+
+/obj/effect/particle_effect/fluid/smoke/Destroy()
+ SSsmoke.stop_processing(src)
+ if(spread_bucket)
+ SSsmoke.cancel_spread(src)
+ return ..()
+
+
+/**
+ * Makes the smoke fade out and then deletes it.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/kill_smoke()
+ SSsmoke.stop_processing(src)
+ if(spread_bucket)
+ SSsmoke.cancel_spread(src)
+ INVOKE_ASYNC(src, PROC_REF(fade_out))
+ QDEL_IN(src, 1 SECONDS)
+
+/**
+ * Animates the smoke gradually fading out of visibility.
+ * Also makes the smoke turf transparent as it passes 160 alpha.
+ *
+ * Arguments:
+ * - frames = 0.8 [SECONDS]: The amount of time the smoke should fade out over.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/fade_out(frames = 0.8 SECONDS)
+ if(alpha == 0) //Handle already transparent case
+ if(opacity)
+ set_opacity(FALSE)
+ return
+
+ if(frames == 0)
+ set_opacity(FALSE)
+ alpha = 0
+ return
+
+ var/time_to_transparency = round(((alpha - 160) / alpha) * frames)
+ if(time_to_transparency >= 1)
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, set_opacity), FALSE), time_to_transparency)
+ else
+ set_opacity(FALSE)
+ animate(src, time = frames, alpha = 0)
+
+
+/obj/effect/particle_effect/fluid/smoke/spread(seconds_per_tick = 0.1 SECONDS)
+ if(group.total_size > group.target_size)
+ return
+ var/turf/t_loc = get_turf(src)
+ if(!t_loc)
+ return
+
+ for(var/turf/spread_turf in t_loc.GetAtmosAdjacentTurfs())
+ if(group.total_size > group.target_size)
+ break
+ if(locate(type) in spread_turf)
+ continue // Don't spread smoke where there's already smoke!
+ for(var/mob/living/smoker in spread_turf)
+ smoke_mob(smoker, seconds_per_tick)
+
+ var/obj/effect/particle_effect/fluid/smoke/spread_smoke = new type(spread_turf, group, src)
+ reagents.copy_to(spread_smoke, reagents.total_volume)
+ spread_smoke.add_atom_colour(color, FIXED_COLOUR_PRIORITY)
+ spread_smoke.lifetime = lifetime
+
+ SSsmoke.queue_spread(spread_smoke)
+
+
+/obj/effect/particle_effect/fluid/smoke/process(seconds_per_tick)
+ lifetime -= seconds_per_tick SECONDS
+ if(lifetime <= 0)
+ kill_smoke()
+ return FALSE
+ for(var/mob/living/smoker in loc) // In case smoke somehow winds up in a locker or something this should still behave sanely.
+ smoke_mob(smoker, seconds_per_tick)
+ return TRUE
+
+/**
+ * Handles the effects of this smoke on any mobs it comes into contact with.
+ *
+ * Arguments:
+ * - [smoker][/mob/living/carbon]: The mob that is being exposed to this smoke.
+ * - seconds_per_tick: A scaling factor for the effects this has. Primarily based off of tick rate to normalize effects to units of rate/sec.
+ *
+ * Returns whether the smoke effect was applied to the mob.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/smoke_mob(mob/living/carbon/smoker, seconds_per_tick)
+ if(!istype(smoker))
+ return FALSE
+ if(lifetime < 1)
+ return FALSE
+ if(smoker.internal != null || smoker.can_breathe_gas())
+ return FALSE
+ if(smoker.smoke_delay)
+ return FALSE
+
+ smoker.smoke_delay = TRUE
+ addtimer(VARSET_CALLBACK(smoker, smoke_delay, FALSE), 1 SECONDS)
+ return TRUE
+
+/**
+ * Makes the smoke react to nearby opening/closing airlocks and the like.
+ * Makes it possible for smoke to spread through airlocks that open after the edge of the smoke cloud has already spread past them.
+ *
+ * Arguments:
+ * - [source][/turf]: The turf that has been touched by an atmos adjacency change.
+ */
+/obj/effect/particle_effect/fluid/smoke/proc/react_to_atmos_adjacency_changes(turf/source)
+ SIGNAL_HANDLER
+ if(!group)
+ return NONE
+ if(spread_bucket)
+ return NONE
+ SSsmoke.queue_spread(src)
+
+/// A factory which produces clouds of smoke.
+/datum/effect_system/fluid_spread/smoke
+ effect_type = /obj/effect/particle_effect/fluid/smoke
+
+/////////////////////////////////////////////
+// Transparent smoke
+/////////////////////////////////////////////
+
+/// Same as the base type, but the smoke produced is not opaque
+/datum/effect_system/fluid_spread/smoke/transparent
+ effect_type = /obj/effect/particle_effect/fluid/smoke/transparent
+
+/// Same as the base type, but is not opaque.
+/obj/effect/particle_effect/fluid/smoke/transparent
+ opacity = FALSE
+
+/**
+ * A helper proc used to spawn small puffs of smoke.
+ *
+ * Arguments:
+ * - range: The amount of smoke to produce as number of steps from origin covered.
+ * - amount: The amount of smoke to produce as the total desired coverage area. Autofilled from the range arg if not set.
+ * - location: Where to produce the smoke cloud.
+ * - smoke_type: The smoke typepath to spawn.
+ */
+/proc/do_smoke(range = 0, amount = DIAMOND_AREA(range), atom/holder = null, location = null, smoke_type = /obj/effect/particle_effect/fluid/smoke, log = FALSE)
+ var/datum/effect_system/fluid_spread/smoke/smoke = new
+ smoke.effect_type = smoke_type
+ smoke.set_up(amount = amount, holder = holder, location = location)
+ smoke.start(log = log)
+
+/////////////////////////////////////////////
+// Quick smoke
+/////////////////////////////////////////////
+
+/// Smoke that dissipates as quickly as possible.
+/obj/effect/particle_effect/fluid/smoke/quick
+ lifetime = 1 SECONDS
+ opacity = FALSE
+
+/// A factory which produces smoke that dissipates as quickly as possible.
+/datum/effect_system/fluid_spread/smoke/quick
+ effect_type = /obj/effect/particle_effect/fluid/smoke/quick
+
+/////////////////////////////////////////////
+// Bad smoke
+/////////////////////////////////////////////
+
+/// Smoke that makes you cough and reduces the power of lasers.
+/obj/effect/particle_effect/fluid/smoke/bad
+ lifetime = 16 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/bad/Initialize(mapload)
+ . = ..()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/effect/particle_effect/fluid/smoke/bad/smoke_mob(mob/living/carbon/smoker)
+ . = ..()
+ if(!.)
+ return
+
+ smoker.drop_from_hands()
+ smoker.adjustOxyLoss(1)
+ smoker.emote("cough")
+
+/**
+ * Reduces the power of any beam projectile that passes through the smoke.
+ *
+ * Arguments:
+ * - [source][/datum]: The location that has just been entered. If [/datum/element/connect_loc] is working this is [src.loc][/atom/var/loc].
+ * - [arrived][/atom/movable]: The atom that has just entered the source location.
+ * - [old_loc][/atom]: The location the entering atom just was in.
+ * - [old_locs][/list/atom]: The set of locations the entering atom was just in.
+ */
+/obj/effect/particle_effect/fluid/smoke/bad/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+ if(istype(arrived, /obj/item/projectile/beam))
+ var/obj/item/projectile/beam/beam = arrived
+ beam.damage *= 0.5
+
+/// A factory which produces smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad
+
+/////////////////////////////////////////////
+// Bad Smoke (But Green (and Black))
+/////////////////////////////////////////////
+
+/// Green smoke that makes you cough.
+/obj/effect/particle_effect/fluid/smoke/bad/green
+ name = "green smoke"
+ color = COLOR_LIME
+ opacity = FALSE
+
+/// A factory which produces green smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad/green
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad/green
+
+/// Black smoke that makes you cough. (Actually dark grey)
+/obj/effect/particle_effect/fluid/smoke/bad/black
+ name = "black smoke"
+ color = "#383838"
+ opacity = FALSE
+
+/// A factory which produces black smoke that makes you cough.
+/datum/effect_system/fluid_spread/smoke/bad/black
+ effect_type = /obj/effect/particle_effect/fluid/smoke/bad/black
+
+/////////////////////////////////////////////
+// Nanofrost smoke
+/////////////////////////////////////////////
+
+/// Light blue, transparent smoke which is usually paired with a blast that chills every turf in the area.
+/obj/effect/particle_effect/fluid/smoke/freezing
+ name = "nanofrost smoke"
+ color = "#B2FFFF"
+ opacity = FALSE
+
+/// A factory which produces light blue, transparent smoke and a blast that chills every turf in the area.
+/datum/effect_system/fluid_spread/smoke/freezing
+ effect_type = /obj/effect/particle_effect/fluid/smoke/freezing
+ /// The radius in which to chill every open turf.
+ var/blast = 0
+ /// The temperature to set the turfs air temperature to.
+ var/temperature = 2
+ /// Whether to weld every vent and air scrubber in the affected area shut.
+ var/weldvents = TRUE
+ /// Whether to make sure each affected turf is actually within range before cooling it.
+ var/distcheck = TRUE
+
+/**
+ * Chills an open turf.
+ *
+ * Forces the air temperature to a specific value.
+ * Transmutes all of the plasma in the air into nitrogen.
+ * Extinguishes all fires and burning objects/mobs in the turf.
+ * May freeze all vents and vent scrubbers shut.
+ *
+ * Arguments:
+ * - [chilly][/turf/open]: The open turf to chill
+ */
+/datum/effect_system/fluid_spread/smoke/freezing/proc/Chilled(turf/simulated/chilly)
+ if(!istype(chilly))
+ return
+
+ if(chilly.air)
+ var/datum/gas_mixture/air = chilly.air
+ if(!distcheck || get_dist(location, chilly) < blast) // Otherwise we'll get silliness like people using Nanofrost to kill people through walls with cold air
+ air.temperature = temperature
+
+ if(air.toxins)
+ air.nitrogen += air.toxins
+ air.toxins = 0
+
+ for(var/obj/effect/hotspot/fire in chilly)
+ qdel(fire)
+ chilly.air_update_turf(FALSE, FALSE)
+
+ if(weldvents)
+ for(var/obj/machinery/atmospherics/unary/comp in chilly)
+ if(!isnull(comp.welded) && !comp.welded) //must be an unwelded vent pump or vent scrubber.
+ comp.welded = TRUE
+ comp.update_appearance()
+ comp.visible_message(span_danger("[comp] is frozen shut!"))
+
+ // Extinguishes everything in the turf
+ for(var/mob/living/potential_tinder in chilly)
+ potential_tinder.ExtinguishMob()
+ for(var/obj/item/potential_tinder in chilly)
+ potential_tinder.extinguish()
+
+/datum/effect_system/fluid_spread/smoke/freezing/set_up(range = 5, amount = DIAMOND_AREA(range), atom/holder, atom/location, blast_radius = 0)
+ . = ..()
+ blast = blast_radius
+
+/datum/effect_system/fluid_spread/smoke/freezing/start(log = FALSE)
+ if(blast)
+ for(var/turf/T in RANGE_TURFS(blast, location))
+ Chilled(T)
+ return ..()
+
+/// A variant of the base freezing smoke formerly used by the vent decontamination event.
+/datum/effect_system/fluid_spread/smoke/freezing/decon
+ temperature = T20C
+ distcheck = FALSE
+ weldvents = FALSE
+
+
+/////////////////////////////////////////////
+// Sleep smoke
+/////////////////////////////////////////////
+
+/// Smoke which knocks you out if you breathe it in.
+/obj/effect/particle_effect/fluid/smoke/sleeping
+ color = "#9C3636"
+ lifetime = 20 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/sleeping/smoke_mob(mob/living/carbon/smoker, seconds_per_tick)
+ if(..())
+ smoker.Sleeping(20 SECONDS)
+ smoker.emote("cough")
+ return TRUE
+
+/// A factory which produces sleeping smoke.
+/datum/effect_system/fluid_spread/smoke/sleeping
+ effect_type = /obj/effect/particle_effect/fluid/smoke/sleeping
+
+/////////////////////////////////////////////
+// Chem smoke
+/////////////////////////////////////////////
+
+/**
+ * Smoke which contains reagents which it applies to everything it comes into contact with.
+ */
+/obj/effect/particle_effect/fluid/smoke/chem
+ lifetime = 20 SECONDS
+
+/obj/effect/particle_effect/fluid/smoke/chem/process(seconds_per_tick)
+ . = ..()
+ if(!.)
+ return
+
+ var/turf/location = get_turf(src)
+ var/fraction = (seconds_per_tick SECONDS) / initial(lifetime)
+ for(var/atom/movable/thing as anything in location)
+ if(thing == src)
+ continue
+ reagents.reaction(thing, REAGENT_TOUCH, fraction)
+
+ reagents.reaction(location, REAGENT_TOUCH, fraction)
+ return TRUE
+
+/obj/effect/particle_effect/fluid/smoke/chem/smoke_mob(mob/living/carbon/smoker, seconds_per_tick)
+ if(lifetime < 1)
+ return FALSE
+ if(!istype(smoker))
+ return FALSE
+ if(smoker.internal != null || !smoker.can_breathe_gas())
+ return FALSE
+
+ var/fraction = (seconds_per_tick SECONDS) / initial(lifetime)
+ reagents.copy_to(smoker, reagents.total_volume, fraction)
+ reagents.reaction(smoker, REAGENT_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, smoke_type = /datum/effect_system/fluid_spread/smoke/chem, 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_type
+ 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
+ /// Evil evil hack so we have something to "hold" our reagents
+ var/datum/reagents/chemholder
+ effect_type = /obj/effect/particle_effect/fluid/smoke/chem
+
+/datum/effect_system/fluid_spread/smoke/chem/New()
+ ..()
+ chemholder = new(1000)
+
+/datum/effect_system/fluid_spread/smoke/chem/Destroy()
+ QDEL_NULL(chemholder)
+ return ..()
+
+
+/datum/effect_system/fluid_spread/smoke/chem/set_up(range = 1, amount = DIAMOND_AREA(range), atom/holder, atom/location = null, datum/reagents/carry = null, silent = FALSE)
+ . = ..()
+ carry?.copy_to(chemholder, carry.total_volume)
+
+ if(silent)
+ return
+
+ var/list/contained_reagents = list()
+ for(var/datum/reagent/reagent as anything in chemholder.reagent_list)
+ contained_reagents += "[reagent.volume]u [reagent]"
+
+ var/where = "[AREACOORD(location)]"
+ var/contained = length(contained_reagents) ? "\[[contained_reagents.Join(", ")]\] @ [chemholder.chem_temp]K" : null
+ if(carry.my_atom?.fingerprintslast) //Some reagents don't have a my_atom in some cases
+ var/mob/M = get_mob_by_key(carry.my_atom.fingerprintslast)
+ var/more = ""
+ if(M)
+ more = "[ADMIN_LOOKUPFLW(M)] "
+ message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. Key: [more ? more : carry.my_atom.fingerprintslast].")
+ log_game("A chemical smoke reaction has taken place in ([where])[contained]. Last touched by [carry.my_atom.fingerprintslast].")
+ else
+ message_admins("Smoke: ([ADMIN_VERBOSEJMP(location)])[contained]. No associated key.")
+ log_game("A chemical smoke reaction has taken place in ([where])[contained]. No associated key.")
+
+/datum/effect_system/fluid_spread/smoke/chem/start(log = FALSE)
+ var/start_loc = holder ? get_turf(holder) : src.location
+ var/mixcolor = mix_color_from_reagents(chemholder.reagent_list)
+ var/obj/effect/particle_effect/fluid/smoke/chem/smoke = new effect_type(start_loc, new /datum/fluid_group(amount))
+ chemholder.copy_to(smoke, chemholder.total_volume)
+
+ if(mixcolor)
+ smoke.add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY) // give the smoke color, if it has any to begin with
+ if(log)
+ help_out_the_admins(smoke, holder, location)
+ smoke.spread() // Making the smoke spread immediately.
+
+/**
+ * A version of chemical smoke with a very short lifespan.
+ */
+/obj/effect/particle_effect/fluid/smoke/chem/quick
+ lifetime = 6 SECONDS
+ opacity = FALSE
+ alpha = 150
+
+/datum/effect_system/fluid_spread/smoke/chem/quick
+ effect_type = /obj/effect/particle_effect/fluid/smoke/chem/quick
diff --git a/code/game/objects/effects/particle_holder.dm b/code/game/objects/effects/particle_holder.dm
new file mode 100644
index 00000000000..3e91d2e605a
--- /dev/null
+++ b/code/game/objects/effects/particle_holder.dm
@@ -0,0 +1,70 @@
+///objects can only have one particle on them at a time, so we use these abstract effects to hold and display the effects. You know, so multiple particle effects can exist at once.
+///also because some objects do not display particles due to how their visuals are built
+/obj/effect/abstract/particle_holder
+ name = "particle holder"
+ desc = "How are you reading this? Please make a bug report :)"
+ appearance_flags = KEEP_APART|KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE //movable appearance_flags plus KEEP_APART and KEEP_TOGETHER
+ vis_flags = VIS_INHERIT_PLANE
+ layer = ABOVE_ALL_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ invisibility = 0
+ anchored = TRUE
+ /// Holds info about how this particle emitter works
+ /// See \code\__DEFINES\particles.dm
+ var/particle_flags = NONE
+
+ var/atom/parent
+
+/obj/effect/abstract/particle_holder/Initialize(mapload, particle_path = /particles/droplets, particle_flags = NONE)
+ . = ..()
+ if(!loc)
+ stack_trace("particle holder was created with no loc!")
+ return INITIALIZE_HINT_QDEL
+ // We nullspace ourselves because some objects use their contents (e.g. storage) and some items may drop everything in their contents on deconstruct.
+ parent = loc
+ loc = null
+
+ // Mouse opacity can get set to opaque by some objects when placed into the object's contents (storage containers).
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ src.particle_flags = particle_flags
+ particles = new particle_path()
+ // /atom doesn't have vis_contents, /turf and /atom/movable do
+ var/atom/movable/lie_about_areas = parent
+ lie_about_areas.vis_contents += src
+ RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(parent_deleted))
+
+ if(particle_flags & PARTICLE_ATTACH_MOB)
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ on_move(parent, null, NORTH)
+
+/obj/effect/abstract/particle_holder/Destroy(force)
+ QDEL_NULL(particles)
+ parent = null
+ return ..()
+
+/// Non movables don't delete contents on destroy, so we gotta do this
+/obj/effect/abstract/particle_holder/proc/parent_deleted(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/// signal called when a parent that's been hooked into this moves
+/// does a variety of checks to ensure overrides work out properly
+/obj/effect/abstract/particle_holder/proc/on_move(atom/movable/attached, atom/oldloc, direction)
+ SIGNAL_HANDLER
+
+ if(!(particle_flags & PARTICLE_ATTACH_MOB))
+ return
+
+ //remove old
+ if(ismob(oldloc))
+ var/mob/particle_mob = oldloc
+ particle_mob.vis_contents -= src
+
+ // If we're sitting in a mob, we want to emit from it too, for vibes and shit
+ if(ismob(attached.loc))
+ var/mob/particle_mob = attached.loc
+ particle_mob.vis_contents += src
+
+/// Sets the particles position to the passed coordinates
+/obj/effect/abstract/particle_holder/proc/set_particle_position(x = 0, y = 0, z = 0)
+ particles.position = list(x, y, z)
diff --git a/code/game/objects/effects/particles/water.dm b/code/game/objects/effects/particles/water.dm
new file mode 100644
index 00000000000..88e0ef542e3
--- /dev/null
+++ b/code/game/objects/effects/particles/water.dm
@@ -0,0 +1,14 @@
+// Water related particles.
+/particles/droplets
+ icon = 'icons/effects/particles/generic.dmi'
+ icon_state = list("dot"=2,"drop"=1)
+ width = 32
+ height = 36
+ count = 5
+ spawning = 0.2
+ lifespan = 1 SECONDS
+ fade = 0.5 SECONDS
+ color = "#549EFF"
+ position = generator(GEN_BOX, list(-9,-9,0), list(9,18,0), NORMAL_RAND)
+ scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND)
+ gravity = list(0, -0.9)
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index 71a661b567e..e5cc272d6ca 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -235,6 +235,13 @@
icon_state = "explosionfast"
duration = 4
+/obj/effect/temp_visual/blob
+ name = "blob"
+ icon_state = "blob_attack"
+ alpha = 140
+ randomdir = 0
+ duration = 6
+
/obj/effect/temp_visual/explosion/florawave
icon_state = "florawave"
duration = 4
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 25b478b26a2..de76cbef570 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -269,10 +269,16 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/g
else
return TRUE
+
/obj/item/blob_act(obj/structure/blob/B)
- if(B && B.loc == loc && !QDELETED(src))
- qdel(src)
+ if(B && B.loc == loc && !QDELETED(src) && !(obj_flags & IGNORE_BLOB_ACT))
+ obj_destruction(MELEE)
+/obj/item/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ . = ..()
+ if(QDELETED(src))
+ return FALSE
+ forceMove(voring_core)
/obj/item/examine(mob/user)
var/size
diff --git a/code/game/objects/items/devices/scanners/gas_analyzer.dm b/code/game/objects/items/devices/scanners/gas_analyzer.dm
index 475a68404ff..689c457e535 100644
--- a/code/game/objects/items/devices/scanners/gas_analyzer.dm
+++ b/code/game/objects/items/devices/scanners/gas_analyzer.dm
@@ -18,6 +18,7 @@
throw_range = 7
materials = list(MAT_METAL=30, MAT_GLASS=20)
origin_tech = "magnets=1;engineering=1"
+ tool_behaviour = TOOL_ANALYZER
var/cooldown = FALSE
var/cooldown_time = 250
var/accuracy // 0 is the best accuracy.
@@ -152,7 +153,7 @@
scan_target = get_turf(src)
if(ANALYZER_MODE_TARGET)
scan_target = target
- if(!can_see(src, target, scan_range))
+ if(!can_see(target, scan_range))
target_mode = ANALYZER_MODE_SURROUNDINGS
scan_target = get_turf(src)
if(!scan_target)
@@ -189,7 +190,7 @@
/obj/item/analyzer/afterattack(atom/target, mob/user, proximity, params)
. = ..()
- if(!can_see(user, target, scan_range))
+ if(!user.can_see(target, scan_range))
return
target_mode = ANALYZER_MODE_TARGET
if(target == user || target == user.loc)
diff --git a/code/game/objects/items/devices/transfer_valve.dm b/code/game/objects/items/devices/transfer_valve.dm
index 0fd811edcde..004741b1225 100644
--- a/code/game/objects/items/devices/transfer_valve.dm
+++ b/code/game/objects/items/devices/transfer_valve.dm
@@ -226,3 +226,7 @@
sleep(1 SECONDS)
update_icon()
+
+/obj/item/transfer_valve/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ obj_destruction(MELEE)
+
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index 747d07928ab..724ff60f546 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -127,8 +127,8 @@
robot.key = ghost.key
robot.set_stat(CONSCIOUS)
- GLOB.dead_mob_list -= robot //please never forget this ever kthx
- GLOB.alive_mob_list += robot
+ robot.remove_from_dead_mob_list() //please never forget this ever kthx
+ robot.add_to_alive_mob_list()
robot.notify_ai(ROBOT_NOTIFY_AI_CONNECTED)
return TRUE
diff --git a/code/game/objects/items/weapons/grenades/clusterbuster.dm b/code/game/objects/items/weapons/grenades/clusterbuster.dm
index fd4195a4093..a7650c2fb22 100644
--- a/code/game/objects/items/weapons/grenades/clusterbuster.dm
+++ b/code/game/objects/items/weapons/grenades/clusterbuster.dm
@@ -1,3 +1,5 @@
+#define CLUSTERBUSTER_PAYLOAD_POWER 0.8
+#define SEGMENTATION_PAYLOAD_DECREASE 1.8
////////////////////
//Clusterbang
////////////////////
@@ -7,6 +9,7 @@
icon = 'icons/obj/weapons/grenade.dmi'
icon_state = "clusterbang"
var/payload = /obj/item/grenade/flashbang/cluster
+ var/payload_power = CLUSTERBUSTER_PAYLOAD_POWER
/obj/item/grenade/clusterbuster/prime()
update_mob()
@@ -21,7 +24,7 @@
for(var/loop = again ,loop > 0, loop--)
new /obj/item/grenade/clusterbuster/segment(loc, payload)//Creates 'segments' that launches a few more payloads
- new /obj/effect/payload_spawner(loc, payload, numspawned)//Launches payload
+ new /obj/effect/payload_spawner(loc, payload, numspawned, payload_power)//Launches payload
playsound(loc, 'sound/weapons/armbomb.ogg', 75, 1, -3)
@@ -37,19 +40,20 @@
icon = 'icons/obj/weapons/grenade.dmi'
icon_state = "clusterbang_segment"
-/obj/item/grenade/clusterbuster/segment/New(var/loc, var/payload_type = /obj/item/grenade/flashbang/cluster)
+/obj/item/grenade/clusterbuster/segment/New(loc, payload_type = /obj/item/grenade/flashbang/cluster)
..()
icon_state = "clusterbang_segment_active"
payload = payload_type
active = 1
SSmove_manager.move_away(src, loc, rand(1,4), 1)
+ payload_power /= SEGMENTATION_PAYLOAD_DECREASE
spawn(rand(15,60))
prime()
/obj/item/grenade/clusterbuster/segment/prime()
- new /obj/effect/payload_spawner(loc, payload, rand(4,8))
+ new /obj/effect/payload_spawner(loc, payload, rand(4,8), payload_power)
playsound(loc, 'sound/weapons/armbomb.ogg', 75, 1, -3)
@@ -58,7 +62,7 @@
//////////////////////////////////
//The payload spawner effect
/////////////////////////////////
-/obj/effect/payload_spawner/New(var/turf/newloc,var/type, var/numspawned as num)
+/obj/effect/payload_spawner/New(turf/newloc,type, numspawned as num, power)
. = ..()
for(var/loop = numspawned ,loop > 0, loop--)
var/obj/item/grenade/P = new type(loc)
@@ -69,7 +73,7 @@
spawn(rand(15,60))
if(!QDELETED(P))
if(istype(P, /obj/item/grenade))
- P.prime()
+ P.prime(power)
qdel(src)
diff --git a/code/game/objects/items/weapons/grenades/flashbang.dm b/code/game/objects/items/weapons/grenades/flashbang.dm
index b6e3db3eefa..511f7cc5fa4 100644
--- a/code/game/objects/items/weapons/grenades/flashbang.dm
+++ b/code/game/objects/items/weapons/grenades/flashbang.dm
@@ -10,7 +10,7 @@
var/light_time = 0.2 SECONDS // The duration the area is illuminated
var/range = 7 // The range in tiles of the flashbang
-/obj/item/grenade/flashbang/prime()
+/obj/item/grenade/flashbang/prime(power = 1)
update_mob()
var/turf/T = get_turf(src)
if(T)
@@ -21,7 +21,7 @@
// Blob damage
for(var/obj/structure/blob/B in hear(range + 1, T))
var/damage = round(30 / (get_dist(B, T) + 1))
- B.take_damage(damage, BURN, "melee", FALSE)
+ B.take_damage(damage * power, BURN, MELEE, FALSE)
// Stunning & damaging mechanic
bang(T, src, range)
diff --git a/code/game/objects/items/weapons/grenades/grenade.dm b/code/game/objects/items/weapons/grenades/grenade.dm
index 96ebb79d2f1..1fa18ff923d 100644
--- a/code/game/objects/items/weapons/grenades/grenade.dm
+++ b/code/game/objects/items/weapons/grenades/grenade.dm
@@ -102,3 +102,6 @@
SSmove_manager.stop_looping(src)
. = ..()
+
+/obj/item/grenade/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ obj_destruction(MELEE)
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index a122e3cb4f5..094dc3a3d10 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -35,6 +35,44 @@
armor_protection = clamp(armor_protection - armour_penetration, min(armor_protection, 0), 100)
return round(damage_amount * (100 - armor_protection)*0.01, DAMAGE_PRECISION)
+
+/// Proc for recovering atom_integrity. Returns the amount repaired by
+/obj/proc/repair_damage(amount)
+ if(amount <= 0) // We only recover here
+ return
+ var/new_integrity = min(max_integrity, obj_integrity + amount)
+ . = new_integrity - obj_integrity
+
+ update_integrity(new_integrity)
+
+
+/// Handles the integrity of an obj changing. This must be called instead of changing integrity directly.
+/obj/proc/update_integrity(new_value)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ var/old_value = obj_integrity
+ new_value = max(0, new_value)
+ if(obj_integrity == new_value)
+ return
+ obj_integrity = new_value
+ on_update_integrity(old_value, new_value)
+ return new_value
+
+/// Handle updates to your obj's integrity
+/obj/proc/on_update_integrity(old_value, new_value)
+ SHOULD_NOT_SLEEP(TRUE)
+ SHOULD_CALL_PARENT(TRUE)
+ SEND_SIGNAL(src, COMSIG_OBJ_INTEGRITY_CHANGED, old_value, new_value)
+
+/// This mostly exists to keep obj_integrity private. Might be useful in the future.
+/obj/proc/get_integrity()
+ SHOULD_BE_PURE(TRUE)
+ return obj_integrity
+
+/// Similar to get_integrity, but returns the percentage as [0-1] instead.
+/obj/proc/get_integrity_percentage()
+ SHOULD_BE_PURE(TRUE)
+ return round(obj_integrity / max_integrity, 0.01)
+
///the sound played when the obj is damaged.
/obj/proc/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
@@ -70,12 +108,16 @@
if(!QDELETED(src)) //Bullet on_hit effect might have already destroyed this object
take_damage(P.damage, P.damage_type, P.flag, 0, turn(P.dir, 180), P.armour_penetration)
+
/obj/blob_act(obj/structure/blob/B)
+ if(!..() || (obj_flags & IGNORE_BLOB_ACT))
+ return
if(isturf(loc))
var/turf/T = loc
if((T.intact && level == 1) || T.transparent_floor == TURF_TRANSPARENT) //the blob doesn't destroy thing below the floor
return
- take_damage(400, BRUTE, "melee", 0, get_dir(src, B))
+ take_damage(400, BRUTE, MELEE, 0, get_dir(src, B))
+
/obj/proc/attack_generic(mob/user, damage_amount = 0, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, armor_penetration = 0) //used by attack_alien, attack_animal, and attack_slime
user.do_attack_animation(src)
diff --git a/code/game/objects/structures/crates_lockers/closets/fireaxe.dm b/code/game/objects/structures/crates_lockers/closets/fireaxe.dm
index 7d989bf5e9a..4dff2175d22 100644
--- a/code/game/objects/structures/crates_lockers/closets/fireaxe.dm
+++ b/code/game/objects/structures/crates_lockers/closets/fireaxe.dm
@@ -146,6 +146,11 @@
operate_panel()
+/obj/structure/closet/fireaxecabinet/blob_act(obj/structure/blob/B)
+ if(fireaxe)
+ fireaxe.forceMove(loc)
+ qdel(src)
+
/obj/structure/closet/fireaxecabinet/attack_tk(mob/user)
if(localopened && fireaxe)
fireaxe.forceMove(loc)
@@ -251,6 +256,11 @@
return ..()
+/obj/structure/fishingrodcabinet/blob_act(obj/structure/blob/B)
+ if(olreliable)
+ olreliable.forceMove(loc)
+ qdel(src)
+
/obj/structure/fishingrodcabinet/attack_hand(mob/user)
if(!olreliable)
return ..()
diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm
index 87f06272e8a..ec23bc71d5b 100644
--- a/code/game/objects/structures/mirror.dm
+++ b/code/game/objects/structures/mirror.dm
@@ -8,7 +8,7 @@
anchored = TRUE
max_integrity = 200
integrity_failure = 100
- flags = CHECK_RICOCHET
+ flags_ricochet = RICOCHET_SHINY | RICOCHET_HARD
var/list/ui_users = list()
/obj/structure/mirror/Initialize(mapload, newdir = SOUTH, building = FALSE)
@@ -93,17 +93,9 @@
return FALSE
else if(prob(70))
return FALSE
+
+ return ..()
- var/turf/p_turf = get_turf(P)
- var/face_direction = get_dir(get_turf(src), p_turf)
- var/face_angle = dir2angle(face_direction)
- var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180))
- if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
- return FALSE
- var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
- P.set_angle(new_angle_s)
- visible_message("[P] reflects off [src]!")
- return TRUE
/obj/item/mounted/mirror
name = "mirror"
diff --git a/code/game/turfs/simulated/floor/plating.dm b/code/game/turfs/simulated/floor/plating.dm
index 7c5d81932eb..66d5172fd51 100644
--- a/code/game/turfs/simulated/floor/plating.dm
+++ b/code/game/turfs/simulated/floor/plating.dm
@@ -176,6 +176,10 @@
barefootstep = FOOTSTEP_HARD_BAREFOOT
clawfootstep = FOOTSTEP_HARD_CLAW
heavyfootstep = FOOTSTEP_GENERIC_HEAVY
+
+/turf/simulated/floor/engine/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 3)
/turf/simulated/floor/engine/break_tile()
return //unbreakable
@@ -239,9 +243,9 @@
if(prob(50))
ChangeTurf(baseturf)
-/turf/simulated/floor/engine/blob_act(obj/structure/blob/B)
- if(prob(25))
- ChangeTurf(baseturf)
+
+/turf/simulated/floor/engine/blob_consume()
+ ChangeTurf(baseturf)
/turf/simulated/floor/engine/cult
name = "engraved floor"
diff --git a/code/game/turfs/simulated/minerals.dm b/code/game/turfs/simulated/minerals.dm
index 40929c1f917..e2e4a24ad6a 100644
--- a/code/game/turfs/simulated/minerals.dm
+++ b/code/game/turfs/simulated/minerals.dm
@@ -47,6 +47,9 @@
if(istype(T, /turf/simulated/mineral/random))
Spread(T)
+/turf/simulated/mineral/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 2)
/// Generates typecache of tools allowed to dig this mineral
/turf/simulated/mineral/proc/generate_picks()
@@ -198,6 +201,9 @@
if(1)
attempt_drill(null,TRUE,3)
+/turf/simulated/mineral/blob_consume()
+ gets_drilled()
+
/turf/simulated/mineral/ancient
name = "ancient rock"
desc = "A rare asteroid rock that appears to be resistant to all mining tools except pickaxes!"
diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm
index 3dc4787b133..b6d747e76fd 100644
--- a/code/game/turfs/simulated/walls.dm
+++ b/code/game/turfs/simulated/walls.dm
@@ -64,6 +64,10 @@
underlay_appearance.icon_state = fixed_underlay["icon_state"]
fixed_underlay = string_assoc_list(fixed_underlay)
underlays += underlay_appearance
+
+/turf/simulated/wall/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 2)
//Appearance
/turf/simulated/wall/examine(mob/user) // If you change this, consider changing the examine_status proc of false walls to match
@@ -136,16 +140,6 @@
if(radiated_temperature > max_temperature)
take_damage(rand(10, 20) * (radiated_temperature / max_temperature))
-/turf/simulated/wall/handle_ricochet(obj/item/projectile/P) //A huge pile of shitcode!
- var/turf/p_turf = get_turf(P)
- var/face_direction = get_dir(src, p_turf)
- var/face_angle = dir2angle(face_direction)
- var/incidence_s = GET_ANGLE_OF_INCIDENCE(face_angle, (P.Angle + 180))
- if(abs(incidence_s) > 90 && abs(incidence_s) < 270)
- return FALSE
- var/new_angle_s = SIMPLIFY_DEGREES(face_angle + incidence_s)
- P.set_angle(new_angle_s)
- return TRUE
/turf/simulated/wall/dismantle_wall(devastated = FALSE, explode = FALSE)
if(devastated)
@@ -183,10 +177,10 @@
return
/turf/simulated/wall/blob_act(obj/structure/blob/B)
- if(prob(50))
- dismantle_wall()
- else
- add_dent(WALL_DENT_HIT)
+ add_dent(WALL_DENT_HIT)
+
+/turf/simulated/wall/blob_consume()
+ dismantle_wall()
/turf/simulated/wall/rpd_act(mob/user, obj/item/rpd/our_rpd)
if(our_rpd.mode == RPD_ATMOS_MODE)
diff --git a/code/game/turfs/simulated/walls_mineral.dm b/code/game/turfs/simulated/walls_mineral.dm
index 4a886c24c34..43c89d59964 100644
--- a/code/game/turfs/simulated/walls_mineral.dm
+++ b/code/game/turfs/simulated/walls_mineral.dm
@@ -235,7 +235,7 @@
icon_state = "shuttle-0"
base_icon_state = "shuttle"
explosion_block = 3
- flags = CHECK_RICOCHET
+ flags_ricochet = RICOCHET_SHINY | RICOCHET_HARD
sheet_type = /obj/item/stack/sheet/mineral/titanium
smooth = SMOOTH_BITMASK | SMOOTH_DIAGONAL_CORNERS
canSmoothWith = SMOOTH_GROUP_TITANIUM_WALLS + SMOOTH_GROUP_WINDOW_FULLTILE_SHUTTLE + SMOOTH_GROUP_AIRLOCK
diff --git a/code/game/turfs/simulated/walls_reinforced.dm b/code/game/turfs/simulated/walls_reinforced.dm
index c9c41156ce9..9a1f52f7400 100644
--- a/code/game/turfs/simulated/walls_reinforced.dm
+++ b/code/game/turfs/simulated/walls_reinforced.dm
@@ -21,6 +21,11 @@
var/d_state = RWALL_INTACT
var/can_be_reinforced = 1
+/turf/simulated/wall/r_wall/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 3)
+
+
/turf/simulated/wall/r_wall/examine(mob/user)
. = ..()
switch(d_state)
diff --git a/code/game/turfs/space/space.dm b/code/game/turfs/space/space.dm
index 5b588a6f492..cb8b9931e6f 100644
--- a/code/game/turfs/space/space.dm
+++ b/code/game/turfs/space/space.dm
@@ -59,9 +59,13 @@
if(opacity)
directional_opacity = ALL_CARDINALS
-
+ ComponentInitialize()
return INITIALIZE_HINT_NORMAL
+/turf/space/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 4)
+
/turf/space/BeforeChange()
..()
var/datum/space_level/S = GLOB.space_manager.get_zlev(z)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 337e11f4264..3fab930ee84 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -122,9 +122,14 @@
if(istype(loc, /area/space))
force_no_gravity = TRUE
-
+
+ ComponentInitialize()
return INITIALIZE_HINT_NORMAL
+/turf/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/blob_turf_consuming, 0)
+
/turf/Destroy(force)
. = QDEL_HINT_IWILLGC
if(!changing_turf)
@@ -175,6 +180,9 @@
/turf/ex_act(severity)
return FALSE
+/turf/proc/blob_consume()
+ return
+
/turf/rpd_act(mob/user, obj/item/rpd/our_rpd) //This is the default turf behaviour for the RPD; override it as required
if(our_rpd.mode == RPD_ATMOS_MODE)
our_rpd.create_atmos_pipe(user, src)
diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm
index f82ab795574..edb592b03cc 100644
--- a/code/modules/admin/player_panel.dm
+++ b/code/modules/admin/player_panel.dm
@@ -425,7 +425,7 @@
if(blob_infected && blob_infected.len)
var/datum/game_mode/mode = SSticker.mode
dat += "
"
dat += "
Blobs | |
"
for(var/datum/mind/blob in mode.blobs["infected"])
@@ -454,14 +455,14 @@
dat += "
"
- dat += "
Blobernauts | |
"
- for(var/datum/mind/blob in mode.blobs["blobernauts"])
+ dat += "
Minions | |
"
+ for(var/datum/mind/blob in mode.blobs["minions"])
var/mob/M = blob.current
if(M)
dat += "[ADMIN_PP(M,"[M.real_name]")][M.client ? "" : " (ghost)"][M.stat == 2 ? " (DEAD)" : ""] | "
dat += "PM | "
else
- dat += "
Blobernauts not found! |
"
+ dat += "Minions not found! |
"
dat += "
"
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 37c10d36971..d9bd05aac7a 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1963,8 +1963,7 @@
else if(href_list["edit_blob_win_count"])
if(!check_rights(R_ADMIN))
return
-
- var/blob_win_count = input(usr, "Ввидите новое число критической массы","Критическая масса:", SSticker.mode.blob_win_count) as num
+ var/blob_win_count = tgui_input_number(usr, "Ввидите новое число критической массы", "Критическая масса:" , SSticker.mode.blob_win_count)
if(!blob_win_count)
return
@@ -1979,9 +1978,9 @@
else if(href_list["send_warning"])
if(!check_rights(R_ADMIN))
return
-
- var/message = stripped_input(usr, "Введите предупреждение", "Предупреждение")
- if(alert(usr,"Вы действительно хотите отправить предупреждение всем блобам?", "", "Да", "Нет") == "Нет")
+
+ var/message = tgui_input_text(usr, "Введите предупреждение", "Предупреждение")
+ if(tgui_alert(usr,"Вы действительно хотите отправить предупреждение всем блобам?", "", list("Да", "Нет")) == "Нет")
return
if(!SSticker || !SSticker.mode)
@@ -1995,7 +1994,7 @@
if(!check_rights(R_ADMIN))
return
- if(alert(usr,"Вы действительно хотите лопнуть всех блобов?", "", "Да", "Нет") == "Нет")
+ if(tgui_alert(usr,"Вы действительно хотите лопнуть всех блобов?", "", list("Да", "Нет")) == "Нет")
return
if(!SSticker || !SSticker.mode)
@@ -2024,6 +2023,22 @@
log_admin("[key_name(usr)] has [mode.delay_blob_end? "stopped" : "returned"] stopped delayed blob win")
message_admins("[key_name_admin(usr)] has [mode.delay_blob_end? "stopped" : "returned"] delayed blob win")
+ else if(href_list["toggle_blob_infinity_points"])
+ if(!check_rights(R_ADMIN))
+ return
+
+ if(!SSticker || !SSticker.mode)
+ return
+
+ var/datum/game_mode/mode = SSticker.mode
+ if(tgui_alert(usr,"Вы действительно хотите [mode.is_blob_infinity_points? "убрать" : "вернуть"] бесконечные очки у блобов?", "", list("Да", "Нет")) == "Нет")
+ return
+
+ mode.is_blob_infinity_points = !mode.is_blob_infinity_points
+
+ log_admin("[key_name(usr)] has [mode.is_blob_infinity_points? "remove" : "returned"] blob infinity points")
+ message_admins("[key_name_admin(usr)] has [mode.is_blob_infinity_points? "remove" : "returned"] blob infinity points")
+
else if(href_list["toggle_auto_nuke_codes"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/antagonists/blob/blob_actions.dm b/code/modules/antagonists/blob/blob_actions.dm
index 5d33c989256..5fb77feea8f 100644
--- a/code/modules/antagonists/blob/blob_actions.dm
+++ b/code/modules/antagonists/blob/blob_actions.dm
@@ -9,15 +9,26 @@
check_flags = AB_CHECK_CONSCIOUS|AB_TRANSFER_MIND
/datum/action/innate/blob/comm/Activate()
- var/input = stripped_input(usr, "Выберите сообщение для отправки другому блобу.", "Blob Telepathy", "")
+ var/input = tgui_input_text(usr, "Выберите сообщение для отправки другим блобам.", "Телепатия Блоба", "")
if(!input || !IsAvailable())
return
- blob_talk(usr, input)
+ blob_talk(input)
return
+/datum/action/innate/blob/comm/proc/blob_talk(message)
+
+ message = trim(copytext(sanitize(message), 1, MAX_MESSAGE_LEN))
+
+ if(!message)
+ return
+
+ add_say_logs(usr, message, language = "BLOB")
+ var/rendered = span_blob("\[Blob Telepathy\] [usr.name] states, [message]")
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, usr)
+
/datum/action/innate/blob/self_burst
- icon_icon = 'icons/mob/blob.dmi'
- button_icon = 'icons/mob/blob.dmi'
+ icon_icon = 'icons/hud/blob.dmi'
+ button_icon = 'icons/hud/blob.dmi'
background_icon_state = "block"
button_icon_state = "ui_tocore"
name = "Self burst"
@@ -25,7 +36,7 @@
check_flags = AB_CHECK_CONSCIOUS|AB_TRANSFER_MIND
/datum/action/innate/blob/self_burst/Activate()
- var/input = alert(usr,"Вы действительно хотите лопнуть себя и превратиться в блоба досрочно? Это действие необратимо.", "", "Да", "Нет") == "Да"
+ var/input = tgui_alert(usr,"Вы действительно хотите лопнуть себя и превратиться в блоба досрочно? Это действие необратимо.", "", list("Да", "Нет")) == "Да"
if(!input || !IsAvailable())
return
var/datum/antagonist/blob_infected/blob = usr?.mind?.has_antag_datum(/datum/antagonist/blob_infected)
@@ -33,21 +44,3 @@
return
blob.burst_blob()
return
-
-/proc/blob_talk(mob/living/user, message)
- add_say_logs(user, message, language = "BLOB")
-
- message = trim(copytext(sanitize(message), 1, MAX_MESSAGE_LEN))
-
- if(!message)
- return
-
- var/rendered = "Blob Telepathy, [user.name] states, \"[message]\""
- for(var/mob/M in GLOB.mob_list)
- if(isovermind(M) || isblobbernaut(M) || isblobinfected(M.mind))
- M.show_message(rendered, 2)
- else if(isobserver(M) && !isnewplayer(M))
- var/rendered_ghost = "Blob Telepathy, [user.name] \
- (F) states, \"[message]\""
- M.show_message(rendered_ghost, 2)
-
diff --git a/code/modules/antagonists/blob/blob_infected_datum.dm b/code/modules/antagonists/blob/blob_infected_datum.dm
index 4c41c1fb572..3e4dc54104d 100644
--- a/code/modules/antagonists/blob/blob_infected_datum.dm
+++ b/code/modules/antagonists/blob/blob_infected_datum.dm
@@ -61,9 +61,14 @@
/datum/antagonist/blob_infected/Destroy(force, ...)
- add_game_logs("has been deblobized", owner.current)
+ if(!is_tranformed)
+ add_game_logs("has been deblobized", owner.current)
stop_process = TRUE
- return ..()
+ . = ..()
+ qdel(time_to_burst_display)
+ qdel(blob_talk_action)
+ qdel(blob_burst_action)
+ return .
/datum/antagonist/blob_infected/add_owner_to_gamemode()
@@ -159,6 +164,7 @@
if(!blob_talk_action)
blob_talk_action = new
blob_talk_action.Grant(antag_mob)
+ GLOB.blob_telepathy_mobs += antag_mob
if(!blob_burst_action)
blob_burst_action = new
blob_burst_action.Grant(antag_mob)
@@ -167,12 +173,9 @@
/datum/antagonist/blob_infected/proc/remove_blob_actions(mob/living/antag_mob)
if(!antag_mob)
return
- if(!blob_talk_action)
- return
- blob_talk_action.Remove(antag_mob)
- if(!blob_burst_action)
- return
- blob_burst_action.Remove(antag_mob)
+ blob_talk_action?.Remove(antag_mob)
+ GLOB.blob_telepathy_mobs -= antag_mob
+ blob_burst_action?.Remove(antag_mob)
/datum/antagonist/blob_infected/proc/add_burst_display(mob/living/antag_mob)
@@ -242,7 +245,7 @@
blob_client = GLOB.directory[ckey(owner.key)]
location = get_turf(C)
var/datum/game_mode/mode= SSticker.mode
- if (ismob(C.loc))
+ if(ismob(C.loc))
var/mob/M = C.loc
M.gib()
if(!is_station_level(location.z) || isspaceturf(location))
@@ -251,16 +254,20 @@
if(blob_client && location)
mode.bursted_blobs_count++
C.was_bursted = TRUE
-
+ kill_borer_inside()
var/datum/antagonist/blob_overmind/overmind = transform_to_overmind()
owner.remove_antag_datum(/datum/antagonist/blob_infected)
- kill_borer_inside()
C.gib()
- var/obj/structure/blob/core/core = new(location, 200, blob_client, SSticker.mode.blob_point_rate)
+ var/obj/structure/blob/special/core/core = new(location, blob_client)
if(!(core.overmind && core.overmind.mind))
return
core.overmind.mind.add_antag_datum(overmind)
core.lateblobtimer()
+ notify_ghosts(
+ "A Blob host has burst in [get_area_name(core)]",
+ source = core,
+ title = "Blob Awakening!",
+ )
SSticker?.mode?.process_blob_stages()
mode.update_blob_objective()
diff --git a/code/modules/antagonists/blob/blob_minion.dm b/code/modules/antagonists/blob/blob_minion.dm
new file mode 100644
index 00000000000..0acf9a5108d
--- /dev/null
+++ b/code/modules/antagonists/blob/blob_minion.dm
@@ -0,0 +1,88 @@
+/datum/antagonist/blob_minion
+ name = "\improper Blob Minion"
+ roundend_category = "blobs"
+ job_rank = ROLE_BLOB
+ special_role = SPECIAL_ROLE_BLOB_MINION
+ wiki_page_name = "Blob"
+ russian_wiki_name = "Блоб"
+ show_in_roundend = FALSE
+ show_in_orbit = FALSE
+ /// The blob core that this minion is attached to
+ var/datum/weakref/overmind
+
+/datum/antagonist/blob_minion/can_be_owned(datum/mind/new_owner)
+ . = ..() && isminion(new_owner?.current)
+
+/datum/antagonist/blob_minion/New(mob/camera/blob/overmind)
+ . = ..()
+ src.overmind = WEAKREF(overmind)
+
+/datum/antagonist/blob_minion/add_owner_to_gamemode()
+ var/datum/game_mode/mode = SSticker.mode
+ if(mode)
+ mode.blobs["minions"] |= owner
+
+/datum/antagonist/blob_minion/remove_owner_from_gamemode()
+ var/datum/game_mode/mode = SSticker.mode
+ if(mode)
+ mode.blobs["minions"] -= owner
+
+
+/datum/antagonist/blob_minion/roundend_report_header()
+ return
+
+
+/datum/antagonist/blob_minion/on_gain()
+ . = ..()
+ give_objectives()
+
+/datum/antagonist/blob_minion/give_objectives()
+ var/datum/objective/blob_minion/objective = new
+ objective.owner = owner
+ objective.overmind = overmind
+ objectives += objective
+
+/datum/antagonist/blob_minion/blobernaut
+ name = "\improper Blobernaut"
+
+
+/datum/antagonist/blob_minion/blobernaut/greet()
+ . = ..()
+ var/mob/camera/blob/blob = overmind
+ var/datum/blobstrain/blobstrain = blob.blobstrain
+ . += span_dangerbigger("Вы блобернаут! Вы должны помогать всем формам блоба в их миссии по уничтожению всего!")
+ . += span_info("Вы сильны, крепки, и медленно регенерируете в пределах плиток блоба, [span_cultlarge("но вы будете медленно умирать, если их рядом нету")] или если фабрика, создавшая вас, будет разрушена.")
+ . += span_info("Вы можете общаться с другими бернаутами, миньенами, зараженными и надразумами телепатически заместо обычного общения.")
+ . += span_info("Штамм вашего надразума: [blobstrain.name]!")
+ . += span_info("Штамм [blobstrain.name] [blobstrain.shortdesc ? "[blobstrain.shortdesc]" : "[blobstrain.description]"]")
+
+/datum/objective/blob_minion
+ name = "protect the blob core"
+ explanation_text = "Защищайте ядро блоба и исполняйте приказы надразумов. Любой ценой."
+ var/datum/weakref/overmind
+
+/datum/objective/blob_minion/check_completion()
+ var/mob/camera/blob/resolved_overmind = overmind.resolve()
+ if(!resolved_overmind)
+ return FALSE
+ return resolved_overmind.stat != DEAD
+
+/**
+ * Takes any datum `source` and checks it for blob_minion datum.
+ */
+/proc/isblobminion(datum/source)
+ if(!source)
+ return FALSE
+
+ if(istype(source, /datum/mind))
+ var/datum/mind/our_mind = source
+ return our_mind.has_antag_datum(/datum/antagonist/blob_minion)
+
+ if(!ismob(source))
+ return FALSE
+
+ var/mob/mind_holder = source
+ if(!mind_holder.mind)
+ return FALSE
+
+ return mind_holder.mind.has_antag_datum(/datum/antagonist/blob_minion)
diff --git a/code/modules/antagonists/blob/blob_minions/blob_mob.dm b/code/modules/antagonists/blob/blob_minions/blob_mob.dm
new file mode 100644
index 00000000000..83f48fd3288
--- /dev/null
+++ b/code/modules/antagonists/blob/blob_minions/blob_mob.dm
@@ -0,0 +1,106 @@
+/// Root of shared behaviour for mobs spawned by blobs, is abstract and should not be spawned
+/mob/living/simple_animal/hostile/blob_minion
+ name = "Blob Error"
+ desc = "Нефункциональное грибковое существо, созданное плохим кодом или небесной ошибкой. Показывайте и смейтесь."
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blob_head"
+ unique_name = TRUE
+ pass_flags = PASSBLOB
+ status_flags = NONE //No throwing blobspores into deep space to despawn, or throwing blobbernaughts, which are bigger than you.
+ faction = list(ROLE_BLOB)
+ bubble_icon = "blob"
+ speak_emote = null
+ stat_attack = UNCONSCIOUS
+ atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
+ nightvision = 8
+ lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
+ can_buckle_to = FALSE
+ universal_speak = TRUE //So mobs can understand them when a blob uses Blob Broadcast
+ sentience_type = SENTIENCE_OTHER
+ gold_core_spawnable = NO_SPAWN
+ can_be_on_fire = TRUE
+ fire_damage = 3
+ tts_seed = "Earth"
+ tts_atom_say_effect = SOUND_EFFECT_NONE
+ a_intent = INTENT_HARM
+ /// Is blob mob linked to factory
+ var/factory_linked = FALSE
+
+
+/mob/living/simple_animal/hostile/blob_minion/ComponentInitialize()
+ AddComponent( \
+ /datum/component/animal_temperature, \
+ minbodytemp = 0, \
+ maxbodytemp = INFINITY, \
+ )
+ AddComponent(/datum/component/blob_minion, on_strain_changed = CALLBACK(src, PROC_REF(on_strain_updated)))
+
+/mob/living/simple_animal/hostile/blob_minion/Initialize(mapload)
+ . = ..()
+ add_traits(list(TRAIT_BLOB_ALLY, TRAIT_MUTE), INNATE_TRAIT)
+
+/// Called when our blob overmind changes their variant, update some of our mob properties
+/mob/living/simple_animal/hostile/blob_minion/proc/on_strain_updated(mob/camera/blob/overmind, datum/blobstrain/new_strain)
+ return
+
+/mob/living/simple_animal/hostile/blob_minion/can_z_move(direction, turf/start, turf/destination, z_move_flags, mob/living/rider)
+ var/obj/structure/blob/s_blob = locate(/obj/structure/blob) in start
+ var/obj/structure/blob/d_blob = locate(/obj/structure/blob) in destination
+ var/check = !(z_move_flags & ZMOVE_FALL_CHECKS)
+ if(s_blob && d_blob)
+ return check
+ . = ..()
+
+/mob/living/simple_animal/hostile/blob_minion/move_up()
+ var/turf/current_turf = get_turf(src)
+ var/turf/above_turf = GET_TURF_ABOVE(current_turf)
+ if((locate(/obj/structure/blob) in current_turf) && (locate(/obj/structure/blob) in above_turf))
+ if(zMove(UP, above_turf, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK))
+ to_chat(src, span_notice("You move upwards."))
+ return
+ . = ..()
+
+/mob/living/simple_animal/hostile/blob_minion/move_down()
+ var/turf/current_turf = get_turf(src)
+ var/turf/below_turf = GET_TURF_BELOW(current_turf)
+ if((locate(/obj/structure/blob) in current_turf) && (locate(/obj/structure/blob) in below_turf))
+ if(zMove(DOWN, below_turf, z_move_flags = ZMOVE_FLIGHT_FLAGS|ZMOVE_FEEDBACK))
+ to_chat(src, span_notice("You move down."))
+ return
+ . = ..()
+
+
+/mob/living/simple_animal/hostile/blob_minion/
+
+/// Associates this mob with a specific blob factory node
+/mob/living/simple_animal/hostile/blob_minion/proc/link_to_factory(obj/structure/blob/special/factory/factory)
+ factory_linked = TRUE
+ RegisterSignal(factory, COMSIG_QDELETING, PROC_REF(on_factory_destroyed))
+
+/mob/living/simple_animal/hostile/blob_minion/attack_animal(mob/living/simple_animal/M)
+ if(ROLE_BLOB in M.faction)
+ to_chat(M, span_danger("Вы не можете навредить другому порождению блоба"))
+ return
+ ..()
+
+/// Called when our factory is destroyed
+/mob/living/simple_animal/hostile/blob_minion/proc/on_factory_destroyed()
+ SIGNAL_HANDLER
+ to_chat(src, span_userdanger("Your factory was destroyed! You feel yourself dying!"))
+
+
+/mob/living/simple_animal/hostile/blob_minion/can_be_blob()
+ return FALSE
+
+/mob/living/simple_animal/hostile/blob_minion/verb/say_to_nearby_mobs()
+ set name = "Сказать окружающим"
+ set desc = "Вы скажете введенный текст окружающим вас мобам"
+
+ var/speak_text = tgui_input_text(usr, "Что вы хотите сказать?", "Сказать окружающим", null)
+
+ if(!speak_text)
+ return
+ if(stat == CONSCIOUS)
+ add_say_logs(usr, speak_text, language = "BLOB mob_say")
+ atom_say(speak_text)
diff --git a/code/modules/antagonists/blob/blob_minions/blob_spore.dm b/code/modules/antagonists/blob/blob_minions/blob_spore.dm
new file mode 100644
index 00000000000..e814e9e734e
--- /dev/null
+++ b/code/modules/antagonists/blob/blob_minions/blob_spore.dm
@@ -0,0 +1,136 @@
+/**
+ * A floating fungus which turns people into zombies and explodes into reagent clouds upon death.
+ */
+/mob/living/simple_animal/hostile/blob_minion/spore
+ name = "blob spore"
+ desc = "Плавающая хрупкая спора."
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blobpod"
+ icon_living = "blobpod"
+ health_doll_icon = "blobpod"
+ health = BLOBMOB_SPORE_HEALTH
+ maxHealth = BLOBMOB_SPORE_HEALTH
+ verb_say = list("psychically pulses", "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 = BLOBMOB_SPORE_OBJ_DMG
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+ attacktext = "ударяет"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ deathmessage = "взрывается облаком газа!"
+ gold_core_spawnable = HOSTILE_SPAWN
+ del_on_death = TRUE
+ speed = BLOBMOB_SPORE_SPEED_MOD
+ /// Size of cloud produced from a dying spore
+ var/death_cloud_size = 2
+ /// Type of mob to create
+ var/mob/living/zombie_type = /mob/living/simple_animal/hostile/blob_minion/zombie
+
+
+/mob/living/simple_animal/hostile/blob_minion/spore/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
+
+/mob/living/simple_animal/hostile/blob_minion/spore/ComponentInitialize()
+ . = ..()
+ AddElement(/datum/element/simple_flying)
+
+/mob/living/simple_animal/hostile/blob_minion/spore/death(gibbed)
+ . = ..()
+ death_burst()
+
+/mob/living/simple_animal/hostile/blob_minion/spore/on_factory_destroyed()
+ death()
+
+/// Create an explosion of spores on death
+/mob/living/simple_animal/hostile/blob_minion/spore/proc/death_burst()
+ do_blob_chem_smoke(range = death_cloud_size, reagent_volume = BLOB_REAGENSPORE_VOL, holder = src, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+/mob/living/simple_animal/hostile/blob_minion/spore/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ if(istype(mover, /obj/structure/blob))
+ return TRUE
+
+/mob/living/simple_animal/hostile/blob_minion/spore/CanAttack(atom/the_target)
+ if(ishuman(the_target))
+ stat_attack = DEAD
+ . = ..()
+ stat_attack = initial(stat_attack)
+
+
+/mob/living/simple_animal/hostile/blob_minion/spore/AttackingTarget()
+ . = ..()
+ var/mob/living/carbon/human/human_target = target
+ if(!istype(human_target) || human_target.stat != DEAD)
+ return
+ zombify(human_target)
+
+/// Become a zombie
+/mob/living/simple_animal/hostile/blob_minion/spore/proc/zombify(mob/living/carbon/human/target)
+ if(HAS_TRAIT(target, TRAIT_NO_TRANSFORM))
+ return
+
+ visible_message(span_warning("Тело [target.name] внезапно поднимается!"))
+ var/mob/living/simple_animal/hostile/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/simple_animal/hostile/blob_minion/spore/minion
+ gold_core_spawnable = NO_SPAWN
+ zombie_type = /mob/living/simple_animal/hostile/blob_minion/zombie/controlled
+ /// We die if we leave the same turf as this z level
+ var/turf/z_turf
+
+/mob/living/simple_animal/hostile/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/simple_animal/hostile/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/simple_animal/hostile/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/simple_animal/hostile/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,\
+ poll_candidates = TRUE,\
+ )
+ else
+ qdel(GetComponent(/datum/component/ghost_direct_control))
+
+/mob/living/simple_animal/hostile/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/simple_animal/hostile/blob_minion/spore/minion/weak
+ name = "fragile blob spore"
+ health = 15
+ maxHealth = 15
+ melee_damage_lower = 1
+ melee_damage_upper = 2
+ death_cloud_size = 1
+
+/mob/living/simple_animal/hostile/blob_minion/spore/minion/weak/zombify()
+ return
+
+/mob/living/simple_animal/hostile/blob_minion/spore/minion/weak/on_strain_updated()
+ return
diff --git a/code/modules/antagonists/blob/blob_minions/blob_zombie.dm b/code/modules/antagonists/blob/blob_minions/blob_zombie.dm
new file mode 100644
index 00000000000..f1303f1a95b
--- /dev/null
+++ b/code/modules/antagonists/blob/blob_minions/blob_zombie.dm
@@ -0,0 +1,112 @@
+/// A shambling mob made out of a crew member
+/mob/living/simple_animal/hostile/blob_minion/zombie
+ name = "blob zombie"
+ desc = "Шаркающий труп, оживленный блобом."
+ icon_state = "zombie"
+ icon_living = "zombie"
+ health_doll_icon = "blobpod"
+ health = BLOBMOB_ZOMBIE_HEALTH
+ maxHealth = BLOBMOB_ZOMBIE_HEALTH
+ verb_say = list("gurgles", "groans")
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ melee_damage_lower = BLOBMOB_ZOMBIE_DMG_LOWER
+ melee_damage_upper = BLOBMOB_ZOMBIE_DMG_UPPER
+ obj_damage = BLOBMOB_ZOMBIE_OBJ_DMG
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+ attacktext = "ударяет"
+ attack_sound = 'sound/weapons/genhit1.ogg'
+ deathmessage = "падает на землю!"
+ gold_core_spawnable = NO_SPAWN
+ del_on_death = TRUE
+ speed = BLOBMOB_ZOMBIE_SPEED_MOD
+ /// The dead body we have inside
+ var/mob/living/carbon/human/corpse
+
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/death(gibbed)
+ REMOVE_TRAIT(corpse, TRAIT_BLOB_ZOMBIFIED, BLOB_ZOMBIE_TRAIT)
+ corpse?.forceMove(loc)
+ death_burst()
+ return ..()
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone != corpse)
+ return
+ corpse = null
+ death()
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ if(istype(mover, /obj/structure/blob))
+ return TRUE
+
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/Destroy()
+ QDEL_NULL(corpse)
+ return ..()
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/on_factory_destroyed()
+ . = ..()
+ death()
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/update_overlays()
+ . = ..()
+ set_up_zombie_appearance()
+
+//Sets up our appearance
+/mob/living/simple_animal/hostile/blob_minion/zombie/proc/set_up_zombie_appearance()
+ copy_overlays(corpse, TRUE)
+ var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/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
+ overlays += blob_head_overlay
+ if(blocks_emissive)
+ add_overlay(get_emissive_block())
+
+/// Create an explosion of spores on death
+/mob/living/simple_animal/hostile/blob_minion/zombie/proc/death_burst()
+ do_blob_chem_smoke(range = 1, holder = src, reagent_volume = BLOB_REAGENSPORE_VOL, location = get_turf(src), reagent_type = /datum/reagent/toxin/spore)
+
+/// Store a body so that we can drop it on death
+/mob/living/simple_animal/hostile/blob_minion/zombie/proc/consume_corpse(mob/living/carbon/human/new_corpse)
+ ADD_TRAIT(new_corpse, TRAIT_BLOB_ZOMBIFIED, BLOB_ZOMBIE_TRAIT)
+ if(new_corpse.wear_suit)
+ maxHealth += new_corpse.getarmor(attack_flag = MELEE)
+ health = maxHealth
+ new_corpse.change_facial_hair("Shaved")
+ new_corpse.change_hair("Bald")
+ new_corpse.forceMove(src)
+ corpse = new_corpse
+ update_appearance(UPDATE_ICON)
+ overlays += new_corpse.overlays
+ set_up_zombie_appearance()
+ RegisterSignal(corpse, COMSIG_LIVING_REVIVE, PROC_REF(on_corpse_revived))
+
+/// Dynamic changeling reentry
+/mob/living/simple_animal/hostile/blob_minion/zombie/proc/on_corpse_revived()
+ SIGNAL_HANDLER
+ visible_message(span_boldwarning("[src] разрывается изнутри!"))
+ death()
+
+/// Blob-created zombies will ping for player control when they make a zombie
+/mob/living/simple_animal/hostile/blob_minion/zombie/controlled
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/controlled/consume_corpse(mob/living/carbon/human/new_corpse)
+ . = ..()
+ if(!isnull(client) || SSticker.current_state == GAME_STATE_FINISHED)
+ return
+ AddComponent(\
+ /datum/component/ghost_direct_control,\
+ ban_type = ROLE_BLOB,\
+ poll_candidates = TRUE,\
+ )
+
+/mob/living/simple_animal/hostile/blob_minion/zombie/controlled/death_burst()
+ return
diff --git a/code/modules/antagonists/blob/blob_minions/blobbernaut.dm b/code/modules/antagonists/blob/blob_minions/blobbernaut.dm
new file mode 100644
index 00000000000..e0b742670a3
--- /dev/null
+++ b/code/modules/antagonists/blob/blob_minions/blobbernaut.dm
@@ -0,0 +1,116 @@
+/**
+ * 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/simple_animal/hostile/blob_minion/blobbernaut
+ name = "blobbernaut"
+ desc = "Огромный, подвижный кусок биомассы."
+ 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, 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
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+ attacktext = "ударяет"
+ attack_sound = 'sound/effects/blobattack.ogg'
+ verb_say = "gurgles"
+ verb_ask = "demands"
+ verb_exclaim = "roars"
+ verb_yell = "bellows"
+ pressure_resistance = 50
+ force_threshold = 10
+ mob_size = MOB_SIZE_LARGE
+ move_resist = MOVE_FORCE_OVERPOWERING
+ hud_type = /datum/hud/simple_animal/blobbernaut
+ gold_core_spawnable = HOSTILE_SPAWN
+
+/mob/living/simple_animal/hostile/blob_minion/blobbernaut/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NEGATES_GRAVITY, INNATE_TRAIT)
+ update_health_hud()
+
+
+/mob/living/simple_animal/hostile/blob_minion/blobbernaut/experience_pressure_difference(pressure_difference, direction)
+ if(!HAS_TRAIT(src, TRAIT_NEGATES_GRAVITY))
+ return ..()
+
+/mob/living/simple_animal/hostile/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/simple_animal/hostile/blob_minion/blobbernaut/minion
+ gold_core_spawnable = NO_SPAWN
+ /// Is our factory dead?
+ var/orphaned = FALSE
+
+/mob/living/simple_animal/hostile/blob_minion/blobbernaut/minion/Initialize(mapload)
+ bruteloss = maxHealth / 2 // Start out injured to encourage not beelining away from the blob
+ . = ..()
+
+/mob/living/simple_animal/hostile/blob_minion/blobbernaut/minion/Life(seconds_per_tick, times_fired)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/damage_sources = 0
+ var/list/blobs_in_area = (is_there_multiz())? urange_multiz(2, src) : 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/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/simple_animal/hostile/blob_minion/blobbernaut/minion/proc/assign_key(ckey, datum/blobstrain/blobstrain)
+ key = ckey
+ flick("blobbernaut_produce", src)
+ SEND_SOUND(src, sound('sound/effects/blobattack.ogg'))
+ SEND_SOUND(src, sound('sound/effects/attackblob.ogg'))
+ log_game("[key] has spawned as Blobbernaut")
+
+/// Set our attack damage based on blob's properties
+/mob/living/simple_animal/hostile/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)
+ attacktext = initial(attacktext)
+ return
+ melee_damage_lower = BLOBMOB_BLOBBERNAUT_DMG_LOWER
+ melee_damage_upper = BLOBMOB_BLOBBERNAUT_DMG_UPPER
+ attacktext = new_strain.blobbernaut_message
+
+/// Called by our factory to inform us that it's not going to support us financially any more
+/mob/living/simple_animal/hostile/blob_minion/blobbernaut/minion/on_factory_destroyed()
+ . = ..()
+ orphaned = TRUE
+ throw_alert("nofactory", /atom/movable/screen/alert/nofactory)
+
diff --git a/code/modules/antagonists/blob/blob_overmind_datum.dm b/code/modules/antagonists/blob/blob_overmind_datum.dm
index ecd9631f335..736252139af 100644
--- a/code/modules/antagonists/blob/blob_overmind_datum.dm
+++ b/code/modules/antagonists/blob/blob_overmind_datum.dm
@@ -13,14 +13,17 @@
var/is_offspring = FALSE
/// Was the blob with this datum bursted blob_infected.
var/is_tranformed = FALSE
- /// Link to the datum of the selected blob reagent.
- var/datum/reagent/blob/reagent
+ //Link to the datum of the selected blob reagent.
+ var/datum/blobstrain/strain
+
+/datum/antagonist/blob_overmind/can_be_owned(datum/mind/new_owner)
+ . = ..() && isovermind(new_owner?.current)
/datum/antagonist/blob_overmind/on_gain()
- if(!reagent)
- var/reagent_type = pick(subtypesof(/datum/reagent/blob))
- reagent = new reagent_type
- return ..()
+ var/mob/camera/blob/camera = owner.current
+ strain = camera.blobstrain
+ . = ..()
+
/datum/antagonist/blob_overmind/add_owner_to_gamemode()
var/datum/game_mode/mode = SSticker.mode
@@ -53,16 +56,16 @@
/datum/antagonist/blob_overmind/greet()
var/list/messages = list()
- messages.Add("Вы Блоб!")
- for(var/message in get_blob_help_messages(reagent))
+ messages.Add(span_danger("Вы Блоб!"))
+ for(var/message in get_blob_help_messages(strain))
messages.Add(message)
SEND_SOUND(owner.current, 'sound/magic/mutate.ogg')
return messages
-/proc/get_blob_help_messages(datum/reagent/blob/blob_reagent_datum)
+/proc/get_blob_help_messages(datum/blobstrain/blob_reagent_datum)
var/list/messages = list()
messages += "Как надразум, вы можете управлять блобом!"
- messages += "Ваш реагент: [blob_reagent_datum.name] - [blob_reagent_datum.description]"
+ messages += blob_reagent_datum.overmind.get_strain_info()
messages += "Вы можете расширяться, атакуя людей, повреждая объекты или размещая простую плитку, если клетка свободна."
messages += "Обычная плитка будет расширять ваше влияние и может быть улучшена до специальной плитки, выполняющей определённую функцию."
messages += "Вы можете улучшить обычные плитки до следующих типов:"
diff --git a/code/modules/antagonists/blob/blobs_attack.dm b/code/modules/antagonists/blob/blobs_attack.dm
new file mode 100644
index 00000000000..19e09eb18f0
--- /dev/null
+++ b/code/modules/antagonists/blob/blobs_attack.dm
@@ -0,0 +1,11 @@
+/atom/proc/can_blob_attack()
+ return TRUE
+
+/mob/living/can_blob_attack()
+ . = ..()
+ if(!.)
+ return
+ return !incorporeal_move
+
+/obj/effect/dummy/phased_mob/can_blob_attack()
+ return FALSE
diff --git a/code/modules/antagonists/blob/blobstrains/_blobstrain.dm b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
new file mode 100644
index 00000000000..55031106c5c
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/_blobstrain.dm
@@ -0,0 +1,172 @@
+GLOBAL_LIST_INIT(valid_blobstrains, subtypesof(/datum/blobstrain) - list(/datum/blobstrain/reagent, /datum/blobstrain/multiplex))
+
+/datum/blobstrain
+ var/name
+ var/description
+ var/color = COLOR_BLACK
+ /// The color that stuff like healing effects and the overmind camera gets
+ var/complementary_color = COLOR_BLACK
+ /// A short description of the power and its effects
+ var/shortdesc = null
+ /// Any long, blob-tile specific effects
+ var/effectdesc = null
+ /// Short descriptor of what the strain does damage-wise, generally seen in the reroll menu
+ var/analyzerdescdamage = "Неизвестный. Сообщите об этой ошибке в баг-репорты и в админтикет."
+ /// Short descriptor of what the strain does in general, generally seen in the reroll menu
+ var/analyzerdesceffect
+ /// Blobbernaut attack verb
+ var/blobbernaut_message = "slams"
+ /// Message sent to any mob hit by the blob
+ var/message = "Блоб бьет вас"
+ /// Gets added onto 'message' if the mob stuck is of type living
+ var/message_living = null
+ /// Stores world.time to figure out when to next give resources
+ var/resource_delay = 0
+ /// For blob-mobs and extinguishing-based effects
+ var/fire_based = FALSE
+ var/mob/camera/blob/overmind
+ /// The amount of health regenned on core_process
+ var/base_core_regen = BLOB_CORE_HP_REGEN
+ /// The amount of points gained on core_process
+ var/point_rate = BLOB_BASE_POINT_RATE
+
+ // Various vars that strains can buff the blob with
+ /// HP regen bonus added by strain
+ var/core_regen_bonus = 0
+ /// resource point bonus added by strain
+ var/point_rate_bonus = 0
+
+ /// Adds to claim, pulse, and expand range
+ var/core_range_bonus = 0
+ /// Extra range up to which the core reinforces blobs
+ var/core_strong_reinforcement_range_bonus = 0
+ /// Extra range up to which the core reinforces blobs into reflectors
+ var/core_reflector_reinforcement_range_bonus = 0
+
+ /// Adds to claim, pulse, and expand range
+ var/node_range_bonus = 0
+ /// Nodes can sustain this any extra spores with this strain
+ var/node_spore_bonus = 0
+ /// Extra range up to which the node reinforces blobs
+ var/node_strong_reinforcement_range_bonus = 0
+ /// Extra range up to which the node reinforces blobs into reflectors
+ var/node_reflector_reinforcement_range_bonus = 0
+
+ /// Extra spores produced by factories with this strain
+ var/factory_spore_bonus = 0
+
+ /// Multiplies the max and current health of every blob with this value upon selecting this strain.
+ var/max_structure_health_multiplier = 1
+ /// Multiplies the max and current health of every mob with this value upon selecting this strain.
+ var/max_mob_health_multiplier = 1
+
+ /// Makes blobbernauts inject a bonus amount of reagents, making their attacks more powerful
+ var/blobbernaut_reagentatk_bonus = 0
+
+/datum/blobstrain/New(mob/camera/blob/new_overmind)
+ if(!istype(new_overmind))
+ stack_trace("blobstrain created without overmind")
+ overmind = new_overmind
+
+/datum/blobstrain/Destroy()
+ overmind = null
+ return ..()
+
+/datum/blobstrain/proc/on_gain()
+ overmind.color = complementary_color
+
+ if(overmind.blob_core)
+ overmind.blob_core.claim_range += core_range_bonus
+ overmind.blob_core.pulse_range += core_range_bonus
+ overmind.blob_core.expand_range += core_range_bonus
+ overmind.blob_core.strong_reinforce_range += core_strong_reinforcement_range_bonus
+ overmind.blob_core.reflector_reinforce_range += core_reflector_reinforcement_range_bonus
+
+ for(var/obj/structure/blob/special/node/N as anything in overmind.node_blobs)
+ N.claim_range += node_range_bonus
+ N.pulse_range += node_range_bonus
+ N.expand_range += node_range_bonus
+ N.strong_reinforce_range += node_strong_reinforcement_range_bonus
+ N.reflector_reinforce_range += node_reflector_reinforcement_range_bonus
+
+ for(var/obj/structure/blob/special/factory/F as anything in overmind.factory_blobs)
+ F.max_spores += factory_spore_bonus
+
+ for(var/obj/structure/blob/B as anything in overmind.all_blobs)
+ B.modify_max_integrity(B.max_integrity * max_structure_health_multiplier)
+ B.update_blob()
+
+ 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!
+ var/list/messages = list()
+ messages += "Штамм вашего надразума: [name]!"
+ messages += "Штамм [name] [shortdesc ? "[shortdesc]" : "[description]"]"
+ to_chat(blob_mob, chat_box_red(messages.Join("
")))
+
+/datum/blobstrain/proc/on_lose()
+ if(overmind.blob_core)
+ overmind.blob_core.claim_range -= core_range_bonus
+ overmind.blob_core.expand_range -= core_range_bonus
+ overmind.blob_core.strong_reinforce_range -= core_strong_reinforcement_range_bonus
+ overmind.blob_core.reflector_reinforce_range -= core_reflector_reinforcement_range_bonus
+
+ for(var/obj/structure/blob/special/node/N as anything in overmind.node_blobs)
+ N.claim_range -= node_range_bonus
+ N.expand_range -= node_range_bonus
+ N.strong_reinforce_range -= node_strong_reinforcement_range_bonus
+ N.reflector_reinforce_range -= node_reflector_reinforcement_range_bonus
+
+ for(var/obj/structure/blob/special/factory/F as anything in overmind.factory_blobs)
+ F.max_spores -= factory_spore_bonus
+
+ 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/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)
+
+/datum/blobstrain/proc/send_message(mob/living/M)
+ var/totalmessage = message
+ if(message_living && !issilicon(M))
+ totalmessage += message_living
+ totalmessage += "!"
+ to_chat(M, span_userdanger("[totalmessage]"))
+
+/datum/blobstrain/proc/core_process()
+ if(resource_delay <= world.time)
+ resource_delay = world.time + 10 // 1 second
+ overmind.add_points(point_rate+point_rate_bonus)
+ overmind.blob_core.repair_damage(base_core_regen + core_regen_bonus)
+
+/datum/blobstrain/proc/attack_living(mob/living/L, list/nearby_blobs) // When the blob attacks people
+ send_message(L)
+/// When this blob's blobbernaut attacks any atom
+/datum/blobstrain/proc/blobbernaut_attack(atom/attacking, mob/living/simple_animal/hostile/blobbernaut)
+ return
+
+/datum/blobstrain/proc/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag, coefficient = 1) //when the blob takes damage, do this
+ return coefficient*damage
+
+/datum/blobstrain/proc/death_reaction(obj/structure/blob/B, damage_flag, coefficient = 1) //when a blob dies, do this
+ return
+
+/datum/blobstrain/proc/expand_reaction(obj/structure/blob/B, obj/structure/blob/newB, turf/T, mob/camera/blob/O, coefficient = 1) //when the blob expands, do this
+ return TRUE
+
+/datum/blobstrain/proc/tesla_reaction(obj/structure/blob/B, power, coefficient = 1) //when the blob is hit by a tesla bolt, do this
+ return TRUE //return 0 to ignore damage
+
+/datum/blobstrain/proc/extinguish_reaction(obj/structure/blob/B, coefficient = 1) //when the blob is hit with water, do this
+ return
+
+/datum/blobstrain/proc/emp_reaction(obj/structure/blob/B, severity, coefficient = 1) //when the blob is hit with an emp, do this
+ return
+
+/datum/blobstrain/proc/examine(mob/user)
+ return list("Прогресс Критической Массы: [span_notice("[TOTAL_BLOB_MASS]/[NEEDED_BLOB_MASS].")]")
diff --git a/code/modules/antagonists/blob/blobstrains/_reagent.dm b/code/modules/antagonists/blob/blobstrains/_reagent.dm
new file mode 100644
index 00000000000..3cf4101b3dd
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/_reagent.dm
@@ -0,0 +1,53 @@
+/datum/blobstrain/reagent // Blobs that mess with reagents, all "legacy" ones // what do you mean "legacy" you never added an alternative
+ var/datum/reagent/reagent
+
+/datum/blobstrain/reagent/New(mob/camera/blob/new_overmind)
+ . = ..()
+ reagent = new reagent()
+
+
+/datum/blobstrain/reagent/attack_living(mob/living/L)
+ var/mob_protection = L.getarmor(null, BIO) * 0.01
+ reagent.reaction_mob(L, REAGENT_TOUCH, BLOB_REAGENTATK_VOL, TRUE, mob_protection, overmind)
+ send_message(L)
+
+/datum/blobstrain/reagent/blobbernaut_attack(atom/attacking, mob/living/simple_animal/hostile/blobbernaut)
+ if(!isliving(attacking))
+ return
+
+ var/mob/living/living_attacking = attacking
+ var/mob_protection = living_attacking.getarmor(null, BIO) * 0.01
+ reagent.reaction_mob(living_attacking, REAGENT_TOUCH, 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/simple_animal/hostile/blob_minion/spore/spore)
+ var/burst_range = (istype(spore)) ? spore.death_cloud_size : 1
+ do_blob_chem_smoke(range = burst_range, holder = spore, reagent_volume = BLOB_REAGENSPORE_VOL, location = get_turf(spore), reagent_type = reagent.type)
+
+
+/proc/do_blob_chem_smoke(range = 0, amount = DIAMOND_AREA(range), atom/holder = null, location = null, reagent_type = /datum/reagent/water, reagent_volume = 10, log = FALSE)
+ var/smoke_type = /datum/effect_system/fluid_spread/smoke/chem/quick
+ var/lifetime = /obj/effect/particle_effect/fluid/smoke/chem/quick::lifetime
+ var/volume = reagent_volume * (lifetime /(1 SECONDS))
+ do_chem_smoke(range, amount, holder, location, reagent_type, smoke_type, reagent_volume = volume, log = log)
+
+
+// These can only be applied by blobs. They are what (reagent) blobs are made out of.
+/datum/reagent/blob
+ name = "Unknown"
+ description = "не должно существовать, и вам следует немедленно обратиться за помощью в adminhelp и напишите баг-репорт."
+ color = COLOR_WHITE
+ taste_description = "Это баг"
+ penetrates_skin = TRUE
+ clothing_penetration = 1
+ metabolization_rate = BLOB_REAGENTS_METABOLISM
+
+/// 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=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ if(exposed_mob.stat == DEAD || HAS_TRAIT(exposed_mob, TRAIT_BLOB_ALLY))
+ return FALSE //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.
+
+/// Exists to earmark the new overmind arg used by blob reagents.
+/datum/reagent/blob/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ return ..()
diff --git a/code/modules/antagonists/blob/blobstrains/blazing_oil.dm b/code/modules/antagonists/blob/blobstrains/blazing_oil.dm
new file mode 100644
index 00000000000..14369d1c245
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/blazing_oil.dm
@@ -0,0 +1,46 @@
+
+//sets you on fire, does burn damage, explodes into flame when burnt, weak to water
+/datum/blobstrain/reagent/blazing_oil
+ name = "Пылающее масло"
+ description = "наносит высокий урон от ожогов и подожигает цели."
+ effectdesc = "при горении также выпускает вспышки пламени, игнорирует урон от горения, но получает урон от воды."
+ analyzerdescdamage = "Наносит высокий урон от ожогов и поджигает цели."
+ analyzerdesceffect = "При попадании выпускает вспышки пламени, игнорирует урон от горения, но получает урон от воды и других огнетушащих жидкостей."
+ color = "#B68D00"
+ complementary_color = "#BE5532"
+ blobbernaut_message = "splashes"
+ message = "Блоб обрызгивает вас горящим маслом"
+ message_living = ", и вы чувствуете, как ваша кожа обугливается и плавится"
+ reagent = /datum/reagent/blob/blazing_oil
+ fire_based = TRUE
+
+/datum/blobstrain/reagent/blazing_oil/extinguish_reaction(obj/structure/blob/B)
+ B.take_damage(4.5, BURN, ENERGY)
+
+/datum/blobstrain/reagent/blazing_oil/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(damage_type == BURN && damage_flag != ENERGY)
+ for(var/turf/simulated/T as anything in range(1, B))
+ if(iswallturf(T) || ismineralturf(T))
+ continue
+ var/obj/structure/blob/C = locate() in T
+ if(!(C && C.overmind && C.overmind.blobstrain.type == B.overmind.blobstrain.type) && prob(80))
+ new /obj/effect/hotspot(T)
+ if(damage_flag == FIRE)
+ return FALSE
+ return ..()
+
+/datum/reagent/blob/blazing_oil
+ name = "Пылающее масло"
+ id = "blob_blazing_oil"
+ taste_description = "горящее масло"
+ color = "#B68D00"
+
+/datum/reagent/blob/blazing_oil/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.adjust_fire_stacks(round(reac_volume/10))
+ exposed_mob.IgniteMob()
+ if(exposed_mob)
+ exposed_mob.apply_damage(0.8*reac_volume, BURN, forced=TRUE)
+ if(iscarbon(exposed_mob))
+ exposed_mob.emote("scream")
diff --git a/code/modules/antagonists/blob/blobstrains/blob_sorium.dm b/code/modules/antagonists/blob/blobstrains/blob_sorium.dm
new file mode 100644
index 00000000000..a1f4375f2ff
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/blob_sorium.dm
@@ -0,0 +1,69 @@
+
+//sets you on fire, does burn damage, explodes into flame when burnt, weak to water
+/datum/blobstrain/reagent/b_sorium
+ name = "Сорий"
+ description = "наносит высокий урон травмами и отбрасывает людей в стороны."
+ effectdesc = "при попадании создает сориумный взрыв."
+ analyzerdescdamage = "Наносит высокий урон травмами и отбрасывает людей в стороны."
+ analyzerdesceffect = "При попадании создает сориумный взрыв."
+ color = "#808000"
+ complementary_color = "#a2a256"
+ blobbernaut_message = "splashes"
+ message = "Блоб врезается в вас и отбрасывает в сторону"
+ reagent = /datum/reagent/blob/b_sorium
+
+
+/datum/blobstrain/reagent/b_sorium/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(prob(damage))
+ reagent_vortex(B, TRUE, damage)
+ return ..()
+
+/datum/reagent/blob/b_sorium
+ name = "Сорий"
+ id = "blob_sorium"
+ taste_description = "толчок"
+ color = "#B68D00"
+
+/datum/reagent/blob/b_sorium/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.6*reac_volume, BRUTE)
+ if(prob(30))
+ reagent_vortex(exposed_mob, TRUE, reac_volume)
+
+/proc/reagent_vortex(mob/living/M, setting_type, volume)
+ var/turf/pull = get_turf(M)
+ if(!setting_type)
+ new /obj/effect/temp_visual/implosion(pull)
+ playsound(pull, 'sound/effects/whoosh.ogg', 25, 1) //credit to Robinhood76 of Freesound.org for this.
+ else
+ new /obj/effect/temp_visual/shockwave(pull)
+ playsound(pull, 'sound/effects/bang.ogg', 25, 1)
+ var/range_power = clamp(round(volume/5, 1), 1, 5)
+ for(var/atom/movable/X in range(range_power,pull))
+ if(iseffect(X))
+ continue
+ if(X.move_resist <= MOVE_FORCE_DEFAULT && !X.anchored)
+ var/distance = get_dist(X, pull)
+ var/moving_power = max(range_power - distance, 1)
+ spawn(0)
+ if(moving_power > 2) //if the vortex is powerful and we're close, we get thrown
+ if(setting_type)
+ var/atom/throw_target = get_edge_target_turf(X, get_dir(X, get_step_away(X, pull)))
+ var/throw_range = 5 - distance
+ X.throw_at(throw_target, throw_range, 1)
+ else
+ X.throw_at(pull, distance, 1)
+ else
+ if(setting_type)
+ for(var/i = 0, i < moving_power, i++)
+ sleep(2)
+ if(!step_away(X, pull))
+ break
+ else
+ for(var/i = 0, i < moving_power, i++)
+ sleep(2)
+ if(!step_towards(X, pull))
+ break
+
+
diff --git a/code/modules/antagonists/blob/blobstrains/cryogenic_poison.dm b/code/modules/antagonists/blob/blobstrains/cryogenic_poison.dm
new file mode 100644
index 00000000000..ac61fb804db
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/cryogenic_poison.dm
@@ -0,0 +1,36 @@
+//does brute, burn, and toxin damage, and cools targets down
+/datum/blobstrain/reagent/cryogenic_poison
+ name = "Криогенный яд"
+ description = "впрыскивает в цель замораживающий яд, нанося небольшой урон от удара, но нанося большой урон с течением времени."
+ analyzerdescdamage = "Вводит в цель замораживающий яд, который постепенно затвердевает внутренние органы цели."
+ color = "#8BA6E9"
+ complementary_color = "#7D6EB4"
+ blobbernaut_message = "injects"
+ message = "Блоб ранит вас"
+ message_living = ", и вы чувствуете, что ваши внутренности твердеют"
+ reagent = /datum/reagent/blob/cryogenic_poison
+
+/datum/reagent/blob/cryogenic_poison
+ name = "Криогенный яд"
+ id = "blob_cryogenic_poison"
+ description = "впрыскивает в цель замораживающий яд, который со временем наносит большой урон."
+ color = "#8BA6E9"
+ taste_description = "заморозка мозга"
+
+/datum/reagent/blob/cryogenic_poison/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ if(exposed_mob.reagents)
+ exposed_mob.reagents.add_reagent(/datum/reagent/consumable/frostoil, 0.3*reac_volume)
+ exposed_mob.reagents.add_reagent(/datum/reagent/consumable/drink/cold/ice, 0.3*reac_volume)
+ exposed_mob.reagents.add_reagent(/datum/reagent/blob/cryogenic_poison, 0.3*reac_volume)
+ exposed_mob.apply_damage(0.2*reac_volume, BRUTE, forced=TRUE)
+
+/datum/reagent/blob/cryogenic_poison/on_mob_life(mob/living/carbon/exposed_mob, seconds_per_tick, times_fired)
+ . = ..()
+ var/need_mob_update
+ need_mob_update = exposed_mob.adjustBruteLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += exposed_mob.adjustFireLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE)
+ need_mob_update += exposed_mob.adjustToxLoss(0.5 * REM * seconds_per_tick, updating_health = FALSE)
+ if(need_mob_update)
+ . = STATUS_UPDATE_HEALTH
diff --git a/code/modules/antagonists/blob/blobstrains/debris_devourer.dm b/code/modules/antagonists/blob/blobstrains/debris_devourer.dm
new file mode 100644
index 00000000000..ce7a9780637
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/debris_devourer.dm
@@ -0,0 +1,71 @@
+#define DEBRIS_DENSITY (length(core.contents) / (length(overmind.blobs_legit) * 0.25)) // items per blob
+
+// Accumulates junk liberally
+/datum/blobstrain/debris_devourer
+ name = "Пожиратель мусора"
+ description = "бросает поглощенные предметы и трупы в цели. Наносит очень низкий урон травмами без запуска объектов."
+ analyzerdescdamage = "Наносит очень низкий урон травмами и может метать поглощенные предметы при атаке."
+ analyzerdesceffect = "Пожирает незакрепленные предметы и трупы, и бросает их при атаке. Поглощенные объекты снижают входящий урон."
+ color = "#8B1000"
+ complementary_color = "#00558B"
+ blobbernaut_message = "blasts"
+ message = "Блоб бьет тебя"
+
+
+/datum/blobstrain/debris_devourer/attack_living(mob/living/L, list/nearby_blobs)
+ send_message(L)
+ for (var/obj/structure/blob/blob in nearby_blobs)
+ debris_attack(L, blob)
+
+/datum/blobstrain/debris_devourer/on_sporedeath(mob/living/spore)
+ var/obj/structure/blob/special/core/core = overmind.blob_core
+ for(var/i in 1 to 3)
+ var/obj/item/I = pick(core.contents)
+ if(I && !QDELETED(I))
+ I.forceMove(get_turf(spore))
+ I.throw_at(get_edge_target_turf(spore,pick(GLOB.alldirs)), 6, 5, spore, TRUE, FALSE, null, 3)
+
+/datum/blobstrain/debris_devourer/expand_reaction(obj/structure/blob/B, obj/structure/blob/newB, turf/T, mob/camera/blob/O, coefficient = 1) //when the blob expands, do this
+ if(overmind)
+ for (var/atom/A in T)
+ A.blob_vore_act(overmind.blob_core)
+ return TRUE
+
+/datum/blobstrain/debris_devourer/proc/debris_attack(atom/attacking, atom/source)
+ var/obj/structure/blob/special/core/core = overmind.blob_core
+ if(prob(40 * DEBRIS_DENSITY)) // Pretend the items are spread through the blob and its mobs and not in the core.
+ var/obj/item/I = length(core.contents) ? pick(core.contents) : null
+ if(!QDELETED(I))
+ if(isobj(I))
+ I.obj_flags |= IGNORE_BLOB_ACT
+ addtimer(CALLBACK(src, PROC_REF(remove_protection), I), BLOB_ACT_PROTECTION_TIME)
+ I.forceMove(get_turf(source))
+ I.throw_at(attacking, 6, 5, overmind, TRUE, FALSE, null, 3)
+
+/datum/blobstrain/debris_devourer/proc/remove_protection(obj/item)
+ item.obj_flags &= ~IGNORE_BLOB_ACT
+
+/datum/blobstrain/debris_devourer/blobbernaut_attack(atom/attacking, mob/living/simple_animal/hostile/blobbernaut)
+ debris_attack(attacking, blobbernaut)
+
+/datum/blobstrain/debris_devourer/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag, coefficient = 1) //when the blob takes damage, do this
+ var/obj/structure/blob/special/core/core = overmind.blob_core
+ return round(max((coefficient*damage)-min(coefficient*DEBRIS_DENSITY, 10), 0)) // reduce damage taken by items per blob, up to 10
+
+/datum/blobstrain/debris_devourer/examine(mob/user)
+ . = ..()
+ var/obj/structure/blob/special/core/core = overmind.blob_core
+ if(isobserver(user))
+ . += span_notice("Поглощенный мусор в настоящее время снижает получаемый урон на [round(max(min(DEBRIS_DENSITY, 10),0))]")
+ else
+ switch (round(max(min(DEBRIS_DENSITY, 10),0)))
+ if(0)
+ . += span_notice("В настоящее время поглощенного мусора недостаточно, чтобы уменьшить урон.")
+ if(1 to 3)
+ . += span_notice("Поглощенный мусор в настоящее время снижает получаемый урон на очень небольшую величину.") // these roughly correspond with force description strings
+ if(4 to 7)
+ . += span_notice("Поглощенный мусор в настоящее время незначительно снижает получаемый урон.")
+ if(8 to 10)
+ . += span_notice("Поглощенный мусор в настоящее время снижает получаемый урон на среднюю величину.")
+
+#undef DEBRIS_DENSITY
diff --git a/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
new file mode 100644
index 00000000000..7c593fd42d5
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/distributed_neurons.dm
@@ -0,0 +1,38 @@
+//kills unconscious targets and turns them into blob zombies, produces fragile spores when killed. Spore produced by factories are sentient.
+/datum/blobstrain/reagent/distributed_neurons
+ name = "Распределенные нейроны"
+ description = "наносит средне-низкий урон токсинами и превращает бессознательные цели в зомби блоба."
+ effectdesc = "при разрушении также производит хрупкие споры. Споры, производимые фабриками, разумны."
+ shortdesc = "наносит средне-низкий урон токсинами и убьет все цели, находящиеся без сознания, при атаке. Споры, производимые фабриками, разумны."
+ analyzerdescdamage = "Наносит средне-низкий урон токсинами и зомбирует людей, находящихся без сознания."
+ analyzerdesceffect = "При разрушении производит хрупкие споры. Споры, производимые фабриками, разумны."
+ color = "#E88D5D"
+ complementary_color = "#823ABB"
+ message_living = "и ты чувствуешь усталость"
+ reagent = /datum/reagent/blob/distributed_neurons
+
+/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) && 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("Спора вылетает из блоба!"))
+ blob_tile.overmind.create_spore(blob_tile.loc, /mob/living/simple_animal/hostile/blob_minion/spore/minion/weak)
+ return ..()
+
+/datum/reagent/blob/distributed_neurons
+ name = "Распределенные нейроны"
+ id = "blob_distributed_neurons"
+ color = "#E88D5D"
+ taste_description = "шипящий"
+
+/datum/reagent/blob/distributed_neurons/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.6*reac_volume, TOX)
+ if(overmind && ishuman(exposed_mob))
+ if(exposed_mob.stat == UNCONSCIOUS)
+ 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(BLOB_ZOMBIFICATION_COST))
+ var/mob/living/simple_animal/hostile/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("Потрачено [BLOB_ZOMBIFICATION_COST] ресурса на зомбификацию [exposed_mob]."))
diff --git a/code/modules/antagonists/blob/blobstrains/electromagnetic_web.dm b/code/modules/antagonists/blob/blobstrains/electromagnetic_web.dm
new file mode 100644
index 00000000000..34cc2429cf1
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/electromagnetic_web.dm
@@ -0,0 +1,33 @@
+//does burn damage and EMPs, slightly fragile
+/datum/blobstrain/reagent/electromagnetic_web
+ name = "Электромагнитная паутина"
+ color = "#83ECEC"
+ complementary_color = "#EC8383"
+ description = "наносит большой урон от ожогов и излучает ЭМИ."
+ effectdesc = "также получает значительно увеличенный урон и выпускает ЭМИ после разрушения."
+ analyzerdescdamage = "Наносит большой урон от ожогов и излучает ЭМИ."
+ analyzerdesceffect = "Хрупок ко всем типам урона и получает огромный урон от травм. Кроме того, при разрушении выпускает небольшой ЭМИ."
+ reagent = /datum/reagent/blob/electromagnetic_web
+
+/datum/blobstrain/reagent/electromagnetic_web/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(damage_type == BRUTE) // take full brute, divide by the multiplier to get full value
+ return damage / B.brute_resist
+ return damage * 1.25 //a laser will do 25 damage, which will kill any normal blob
+
+/datum/blobstrain/reagent/electromagnetic_web/death_reaction(obj/structure/blob/B, damage_flag)
+ if(damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER)
+ empulse(B.loc, 1, 3) //less than screen range, so you can stand out of range to avoid it
+
+/datum/reagent/blob/electromagnetic_web
+ name = "Электромагнитная паутина"
+ id = "blob_electromagnetic_web"
+ taste_description = "поп-рок"
+ color = "#83ECEC"
+
+/datum/reagent/blob/electromagnetic_web/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ if(prob(reac_volume*2))
+ exposed_mob.emp_act(EMP_LIGHT)
+ if(exposed_mob)
+ exposed_mob.apply_damage(reac_volume, BURN, forced=TRUE)
diff --git a/code/modules/antagonists/blob/blobstrains/energized_jelly.dm b/code/modules/antagonists/blob/blobstrains/energized_jelly.dm
new file mode 100644
index 00000000000..82d48db407c
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/energized_jelly.dm
@@ -0,0 +1,43 @@
+//does tons of oxygen damage and a little stamina, immune to tesla bolts, weak to EMP
+/datum/blobstrain/reagent/energized_jelly
+ name = "Энергетическое желе"
+ description = "наносит урон выносливости и средний урон гипоксией, а также лишает цели возможности дышать."
+ effectdesc = "также проводит электричество, но получает урон от ЭМИ. Вызывает электрические разряды в теле после удара."
+ analyzerdescdamage = "Наносит высокий урон выносливости, средний урон гипоксией и не дает цели дышать."
+ analyzerdesceffect = "Невосприимчив к электричеству и легко его проводит, но слаб к ЭМИ. Вызывает электрические разряды в теле после удара."
+ color = "#EFD65A"
+ complementary_color = "#00E5B1"
+ reagent = /datum/reagent/blob/energized_jelly
+
+/datum/blobstrain/reagent/energized_jelly/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && B.get_integrity() - damage <= 0 && prob(30))
+ do_sparks(rand(2, 4), FALSE, B)
+ return ..()
+
+/datum/blobstrain/reagent/energized_jelly/tesla_reaction(obj/structure/blob/B, power)
+ return FALSE
+
+/datum/blobstrain/reagent/energized_jelly/emp_reaction(obj/structure/blob/B, severity)
+ var/damage = rand(30, 50) - severity * rand(10, 15)
+ B.take_damage(damage, BURN, ENERGY)
+
+/datum/reagent/blob/energized_jelly
+ name = "Энергетическое желе"
+ id = "blob_energized_jelly"
+ taste_description = "желатин"
+ color = "#EFD65A"
+
+
+/datum/reagent/blob/energized_jelly/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.LoseBreath(round(0.2*reac_volume))
+ exposed_mob.adjustStaminaLoss(reac_volume * 1.2)
+ exposed_mob.apply_damage(0.6*reac_volume, OXY)
+ if(exposed_mob.reagents)
+ if(exposed_mob.reagents.has_reagent("teslium") && prob(0.6 * reac_volume))
+ exposed_mob.electrocute_act((0.5 * reac_volume), "разряда блоба", flags = SHOCK_NOGLOVES)
+ exposed_mob.reagents.del_reagent("teslium")
+ return //don't add more teslium after you shock it out of someone.
+ exposed_mob.reagents.add_reagent("teslium", 0.125 * reac_volume) // a little goes a long way
+
diff --git a/code/modules/antagonists/blob/blobstrains/explosive_lattice.dm b/code/modules/antagonists/blob/blobstrains/explosive_lattice.dm
new file mode 100644
index 00000000000..4d75bcac826
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/explosive_lattice.dm
@@ -0,0 +1,75 @@
+//does aoe brute damage when hitting targets, is immune to explosions
+/datum/blobstrain/reagent/explosive_lattice
+ name = "Взрывная решетка"
+ description = "атакует небольшими взрывами, нанося среднее сочетание урона ожогами и травмами всем, кто находится близко к цели. Споры взрываются при смерти."
+ effectdesc = "также имеет повышенную сопротивляемость взрывам, но получает повышенный урон от огня и других источников энергии."
+ analyzerdescdamage = "Атакует небольшими взрывами, нанося среднее сочетание урона ожогами и травмами всем, кто находится близко к цели. Споры взрываются при смерти."
+ analyzerdesceffect = "Обладает высокой устойчивостью к взрывам, но получает повышенный урон от огня и других источников энергии."
+ color = "#8B2500"
+ complementary_color = "#00668B"
+ blobbernaut_message = "blasts"
+ message = "Блоб взрывает тебя"
+ reagent = /datum/reagent/blob/explosive_lattice
+
+/datum/blobstrain/reagent/explosive_lattice/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(damage_flag == BOMB)
+ return 0
+ else if(damage_flag != MELEE && damage_flag != BULLET && damage_flag != LASER)
+ return damage * 1.5
+ return ..()
+
+/datum/blobstrain/reagent/explosive_lattice/on_sporedeath(mob/living/spore)
+ var/obj/effect/temp_visual/explosion/fast/effect = new /obj/effect/temp_visual/explosion/fast(get_turf(spore))
+ effect.alpha = 150
+ for(var/mob/living/actor in orange(get_turf(spore), 1))
+ if(ROLE_BLOB in actor.faction) // No friendly fire
+ continue
+ actor.take_overall_damage(10, 10)
+
+/datum/reagent/blob/explosive_lattice
+ name = "Взрывная решетка"
+ id = "blob_explosive_lattice"
+ taste_description = "бомба"
+ color = "#8B2500"
+
+/datum/reagent/blob/explosive_lattice/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ var/brute_loss = 0
+ var/burn_loss = 0
+ var/bomb_armor = 0
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+
+ if(reac_volume >= 10) // If it's not coming from a sporecloud, AOE 'explosion' damage
+ var/epicenter_turf = get_turf(exposed_mob)
+ var/obj/effect/temp_visual/explosion/fast/ex_effect = new /obj/effect/temp_visual/explosion/fast(get_turf(exposed_mob))
+ ex_effect.alpha = 150
+
+ // Total damage to epicenter mob of 0.7*reac_volume, like a mid-tier strain
+ brute_loss = reac_volume*0.35
+
+ bomb_armor = exposed_mob.getarmor(null, BOMB)
+ if(bomb_armor) // Same calculation and proc that ex_act uses on mobs
+ brute_loss = brute_loss*(2 - round(bomb_armor*0.01, 0.05))
+
+ burn_loss = brute_loss
+
+ exposed_mob.take_overall_damage(brute_loss, burn_loss)
+
+ for(var/mob/living/nearby_mob in orange(epicenter_turf, 1))
+ if(ROLE_BLOB in nearby_mob.faction) // No friendly fire.
+ continue
+ if(nearby_mob == exposed_mob) // We've already hit the epicenter mob
+ continue
+ // AoE damage of 0.5*reac_volume to everyone in a 1 tile range
+ brute_loss = reac_volume*0.25
+ burn_loss = brute_loss
+
+ bomb_armor = nearby_mob.getarmor(null, BOMB)
+ if(bomb_armor) // Same calculation and prod that ex_act uses on mobs
+ brute_loss = brute_loss*(2 - round(bomb_armor*0.01, 0.05))
+ burn_loss = brute_loss
+
+ nearby_mob.take_overall_damage(brute_loss, burn_loss)
+
+ else
+ exposed_mob.apply_damage(0.6*reac_volume, BRUTE, forced = TRUE)
diff --git a/code/modules/antagonists/blob/blobstrains/multiplex.dm b/code/modules/antagonists/blob/blobstrains/multiplex.dm
new file mode 100644
index 00000000000..0930da2eae0
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/multiplex.dm
@@ -0,0 +1,40 @@
+/datum/blobstrain/multiplex
+ var/list/blobstrains
+ var/typeshare
+
+/datum/blobstrain/multiplex/New(mob/camera/blob/new_overmind, list/blobstrains)
+ . = ..()
+ for (var/bt in blobstrains)
+ if(ispath(bt, /datum/blobstrain))
+ src.blobstrains += new bt(overmind)
+ else if(istype(bt, /datum/blobstrain))
+ var/datum/blobstrain/bts = bt
+ bts.overmind = overmind
+ src.blobstrains += bt
+ typeshare = (0.8 * length(src.blobstrains)) - (length(src.blobstrains)-1) // 1 is 80%, 2 are 60% etc
+
+/datum/blobstrain/multiplex/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag, coefficient = 1) //when the blob takes damage, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.damage_reaction(B, damage, damage_type, damage_flag, coefficient*typeshare)
+
+/datum/blobstrain/multiplex/death_reaction(obj/structure/blob/B, damage_flag, coefficient = 1) //when a blob dies, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.death_reaction(B, damage_flag, coefficient*typeshare)
+
+/datum/blobstrain/multiplex/expand_reaction(obj/structure/blob/B, obj/structure/blob/newB, turf/T, mob/camera/blob/O, coefficient = 1) //when the blob expands, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.expand_reaction(B, newB, T, O, coefficient*typeshare)
+
+/datum/blobstrain/multiplex/tesla_reaction(obj/structure/blob/B, power, coefficient = 1) //when the blob is hit by a tesla bolt, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.tesla_reaction(B, power, coefficient*typeshare)
+ if(prob(. / length(blobstrains) * 100))
+ return 1
+
+/datum/blobstrain/multiplex/extinguish_reaction(obj/structure/blob/B, coefficient = 1) //when the blob is hit with water, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.extinguish_reaction(B, coefficient*typeshare)
+
+/datum/blobstrain/multiplex/emp_reaction(obj/structure/blob/B, severity, coefficient = 1) //when the blob is hit with an emp, do this
+ for (var/datum/blobstrain/bt in blobstrains)
+ . += bt.emp_reaction(B, severity, coefficient*typeshare)
diff --git a/code/modules/antagonists/blob/blobstrains/networked_fibers.dm b/code/modules/antagonists/blob/blobstrains/networked_fibers.dm
new file mode 100644
index 00000000000..b1e0ec67c32
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/networked_fibers.dm
@@ -0,0 +1,46 @@
+//does massive brute and burn damage, but can only expand manually
+/datum/blobstrain/reagent/networked_fibers
+ name = "Сетевые волокна"
+ description = "наносит большое количество урона травмами и ожогами и генерирует ресурсы быстрее, но может расширяться только с помощью перемещения ядра или узлов."
+ shortdesc = "наносит сочетание урона травмами и ожогами."
+ effectdesc = "перемещает ваше ядро или узел при ручном расширении рядом с ним."
+ analyzerdescdamage = "Наносит большое количество урона травмами и ожогами."
+ analyzerdesceffect = "Мобильный и быстро генерирует ресурсы."
+ color = "#4F4441"
+ complementary_color = "#414C4F"
+ reagent = /datum/reagent/blob/networked_fibers
+ core_regen_bonus = 5
+
+/datum/blobstrain/reagent/networked_fibers/expand_reaction(obj/structure/blob/spawning_blob, obj/structure/blob/new_blob, turf/chosen_turf, mob/camera/blob/overmind, offstation)
+ if(!overmind && new_blob.overmind || offstation)
+ new_blob.overmind.add_points(1)
+ if(offstation)
+ to_chat(usr, span_warning("Двигать ядро или узел за пределы станции нельзя."))
+ qdel(new_blob)
+ return FALSE
+
+ var/list/range_contents = (is_there_multiz())? urange_multiz(1, new_blob) : range(1, new_blob)
+
+ for(var/obj/structure/blob/possible_expander in range_contents)
+ if(possible_expander.overmind == overmind && (istype(possible_expander, /obj/structure/blob/special/core) || istype(possible_expander, /obj/structure/blob/special/node)))
+ new_blob.forceMove(get_turf(possible_expander))
+ possible_expander.forceMove(chosen_turf)
+ possible_expander.setDir(get_dir(new_blob, possible_expander))
+ return TRUE
+ overmind.add_points(BLOB_EXPAND_COST)
+ qdel(new_blob)
+ return FALSE
+
+//does massive brute and burn damage, but can only expand manually
+/datum/reagent/blob/networked_fibers
+ name = "Сетевые волокна"
+ id = "blob_networked_fibers"
+ taste_description = "эффективность"
+ color = "#4F4441"
+
+/datum/reagent/blob/networked_fibers/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.6*reac_volume, BRUTE, forced = TRUE)
+ if(!QDELETED(exposed_mob))
+ exposed_mob.apply_damage(0.6*reac_volume, BURN, forced = TRUE)
diff --git a/code/modules/antagonists/blob/blobstrains/pressurized_slime.dm b/code/modules/antagonists/blob/blobstrains/pressurized_slime.dm
new file mode 100644
index 00000000000..1fd8cfd3bd6
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/pressurized_slime.dm
@@ -0,0 +1,57 @@
+//does low brute damage, oxygen damage, and stamina damage and wets tiles when damaged
+/datum/blobstrain/reagent/pressurized_slime
+ name = "Сжатая слизь"
+ description = "наносит низкий урон травмами и урон гипоксией, высокий урон выносливости и делает пол под целями очень скользкими, туша их."
+ effectdesc = "также сделает плитки скользкими рядом с атакованными плитками. Устойчив к грубым атакам."
+ analyzerdescdamage = "Наносит низкий урон травмами и урон гипоксией, высокий урон выносливости и делает пол под целями очень скользкими, туша их. Устойчив к атакам травмами."
+ analyzerdesceffect = "При нападении или убийстве смазывает близлежащие плитки пола, тушая все на них."
+ color = "#AAAABB"
+ complementary_color = "#BBBBAA"
+ blobbernaut_message = "emits slime at"
+ message = "Блоб плюхается в тебя"
+ message_living = ", и ты задыхаешься"
+ reagent = /datum/reagent/blob/pressurized_slime
+
+/datum/blobstrain/reagent/pressurized_slime/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) || damage_type != BURN)
+ extinguisharea(B, damage)
+ if(damage_type == BRUTE)
+ return damage * 0.5
+ return ..()
+
+/datum/blobstrain/reagent/pressurized_slime/death_reaction(obj/structure/blob/B, damage_flag)
+ if(damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER)
+ B.visible_message(span_boldwarning("Блоб разрывается, обрызгивая область жидкостью!"))
+ extinguisharea(B, 50)
+
+/datum/blobstrain/reagent/pressurized_slime/proc/extinguisharea(obj/structure/blob/B, probchance)
+ for(var/turf/simulated/T as anything in range(1, B))
+ if(!istype(T) || iswallturf(T) || ismineralturf(T))
+ continue
+ if(prob(probchance))
+ T.MakeSlippery(TURF_WET_LUBE, min_wet_time = 10 SECONDS, wet_time_to_add = 5 SECONDS)
+ for(var/obj/O in T)
+ O.extinguish()
+ for(var/mob/living/L in T)
+ L.adjust_wet_stacks(2.5)
+ L.WetMob()
+ L.ExtinguishMob()
+
+/datum/reagent/blob/pressurized_slime
+ name = "Сжатая слизь"
+ id = "blob_pressurized_slime"
+ taste_description = "губка"
+ color = "#AAAABB"
+
+/datum/reagent/blob/pressurized_slime/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ var/turf/simulated/location_turf = get_turf(exposed_mob)
+ if(istype(location_turf) && !(iswallturf(location_turf) || ismineralturf(location_turf)) && prob(reac_volume))
+ location_turf.MakeSlippery(TURF_WET_LUBE, min_wet_time = 10 SECONDS, wet_time_to_add = 5 SECONDS)
+ exposed_mob.adjust_wet_stacks(reac_volume / 10)
+ exposed_mob.WetMob()
+ exposed_mob.apply_damage(0.4*reac_volume, BRUTE, forced=TRUE)
+ if(exposed_mob)
+ exposed_mob.adjustStaminaLoss(reac_volume, FALSE)
+ exposed_mob.apply_damage(0.4 * reac_volume, OXY)
diff --git a/code/modules/antagonists/blob/blobstrains/radioactive_gel.dm b/code/modules/antagonists/blob/blobstrains/radioactive_gel.dm
new file mode 100644
index 00000000000..8a10420836a
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/radioactive_gel.dm
@@ -0,0 +1,34 @@
+
+//sets you on fire, does burn damage, explodes into flame when burnt, weak to water
+/datum/blobstrain/reagent/radioactive_gel
+ name = "Радиоактивный гель"
+ description = "наносит средний урон токсинами и небольшой урон травмами, но облучает тех, кого задевает."
+ effectdesc = "при получении урона облучает окружающих."
+ analyzerdescdamage = "Наносит средний урон токсинами и небольшой урон травмами, но облучает тех, кого задевает."
+ analyzerdesceffect = "При получении урона облучает окружающих."
+ color = "#2476f0"
+ complementary_color = "#24f0f0"
+ blobbernaut_message = "splashes"
+ message_living = ", и вы чувствуете странное тепло изнутри"
+ reagent = /datum/reagent/blob/radioactive_gel
+
+
+/datum/blobstrain/reagent/radioactive_gel/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if((damage_flag == ENERGY || damage_flag == LASER) && prob(damage))
+ for(var/mob/living/l in range(5, B))
+ l.apply_effect(damage, IRRADIATE)
+ return ..()
+
+/datum/reagent/blob/radioactive_gel
+ name = "Рadioactive_gel"
+ id = "blob_radioactive_gel"
+ taste_description = "радиация"
+ color = "#2476f0"
+
+/datum/reagent/blob/radioactive_gel/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.3 * reac_volume, TOX)
+ exposed_mob.apply_damage(0.2 * reac_volume, BRUTE) // lets not have IPC / plasmaman only take 7.5 damage from this
+ if(exposed_mob.reagents)
+ exposed_mob.reagents.add_reagent("uranium", 0.3 * reac_volume)
diff --git a/code/modules/antagonists/blob/blobstrains/reactive_spines.dm b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
new file mode 100644
index 00000000000..66edcea4c51
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/reactive_spines.dm
@@ -0,0 +1,46 @@
+//does brute damage through armor and bio resistance
+/datum/blobstrain/reagent/reactive_spines
+ name = "Реактивные шипы"
+ description = "наносит большой урон травмами через броню и биосопротивление."
+ effectdesc = "также будет реагировать на атаку ожогами или травмами, атакуя все в ближнем бою."
+ analyzerdescdamage = "Наносит высокий урон травмами, игнорируя броню и биосопротивление."
+ analyzerdesceffect = "При нанесении урона ожогами и травмами блоб яростно бросается в атаку, атакуя все, что находится поблизости."
+ color = "#9ACD32"
+ complementary_color = "#FFA500"
+ blobbernaut_message = "stabs"
+ message = "Блоб ранит тебя"
+ reagent = /datum/reagent/blob/reactive_spines
+ COOLDOWN_DECLARE(retaliate_cooldown)
+
+/datum/blobstrain/reagent/reactive_spines/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ 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("Блоб отвечает, набрасываясь!"))
+ for(var/atom/thing in range(1, B))
+ if(!thing.can_blob_attack())
+ continue
+ 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(thing)
+
+ 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 ..()
+
+/datum/reagent/blob/reactive_spines
+ name = "Реактивные шипы"
+ id = "blob_reactive_spines"
+ taste_description = "камень"
+ color = "#9ACD32"
+
+/datum/reagent/blob/reactive_spines/return_mob_expose_reac_volume(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ 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
+
+/datum/reagent/blob/reactive_spines/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.adjustBruteLoss(reac_volume)
diff --git a/code/modules/antagonists/blob/blobstrains/regenerative_materia.dm b/code/modules/antagonists/blob/blobstrains/regenerative_materia.dm
new file mode 100644
index 00000000000..291ba371e0a
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/regenerative_materia.dm
@@ -0,0 +1,40 @@
+//does toxin damage, hallucination, targets think they're not hurt at all
+/datum/blobstrain/reagent/regenerative_materia
+ name = "Регенеративная Материя"
+ description = "наносит средний начальный урон токсинами, впрыскивая яд, который наносит больший урон токсинами и заставляет цели верить, что они полностью здоровы. Ядро восстанавливается гораздо быстрее."
+ analyzerdescdamage = "Наносит средний начальный урон токсинами, вводя яд, который наносит больший урон токсинами и заставляет цели верить, что они полностью здоровы. Ядро восстанавливается гораздо быстрее."
+ color = "#A88FB7"
+ complementary_color = "#AF7B8D"
+ message_living = ", и ты чувствуешь себя живым"
+ reagent = /datum/reagent/blob/regenerative_materia
+ core_regen_bonus = 18
+ point_rate_bonus = 2
+
+/datum/reagent/blob/regenerative_materia
+ name = "Регенеративная Материя"
+ id = "blob_regenerative_materia"
+ taste_description = "небеса"
+ color = "#A88FB7"
+
+/datum/reagent/blob/regenerative_materia/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ if(iscarbon(exposed_mob))
+ exposed_mob.Druggy(reac_volume * 2 SECONDS)
+ if(exposed_mob.reagents)
+ exposed_mob.reagents.add_reagent(/datum/reagent/blob/regenerative_materia, 0.2*reac_volume)
+ exposed_mob.reagents.add_reagent(/datum/reagent/toxin/spore, 0.2*reac_volume)
+ exposed_mob.apply_damage(0.7*reac_volume, TOX)
+
+/datum/reagent/blob/regenerative_materia/on_mob_life(mob/living/carbon/metabolizer, seconds_per_tick, times_fired)
+ . = ..()
+ if(metabolizer.adjustToxLoss(1 * REM * seconds_per_tick, updating_health = FALSE))
+ return STATUS_UPDATE_HEALTH
+
+/datum/reagent/blob/regenerative_materia/on_mob_start_metabolize(mob/living/metabolizer)
+ . = ..()
+ metabolizer.apply_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
+
+/datum/reagent/blob/regenerative_materia/on_mob_end_metabolize(mob/living/metabolizer)
+ . = ..()
+ metabolizer.remove_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type)
diff --git a/code/modules/antagonists/blob/blobstrains/replicating_foam.dm b/code/modules/antagonists/blob/blobstrains/replicating_foam.dm
new file mode 100644
index 00000000000..ce82dafe490
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/replicating_foam.dm
@@ -0,0 +1,38 @@
+/datum/blobstrain/reagent/replicating_foam
+ name = "Репликационная пена"
+ description = "наносит средний урон травмами и иногда дополнительно расширяется при расширении."
+ shortdesc = "наносит средний урон травмами."
+ effectdesc = "также будет расширяться при атаке ожогами, но получает больше урона травмами."
+ color = "#7B5A57"
+ complementary_color = "#57787B"
+ analyzerdescdamage = "Наносит средний урон травмами."
+ analyzerdesceffect = "Расширяется при атаке ожогами, иногда дополнительно расширяется при расширении и уязвим к урону травмами."
+ reagent = /datum/reagent/blob/replicating_foam
+
+
+/datum/blobstrain/reagent/replicating_foam/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(damage_type == BRUTE)
+ damage = damage * 2
+ else if(damage_type == BURN && damage > 0 && B.get_integrity() - damage > 0 && prob(60))
+ var/obj/structure/blob/newB = B.expand(null, null, 0)
+ if(newB)
+ newB.update_integrity(B.get_integrity() - damage)
+ newB.update_appearance()
+ return ..()
+
+
+/datum/blobstrain/reagent/replicating_foam/expand_reaction(obj/structure/blob/B, obj/structure/blob/newB, turf/T, mob/camera/blob/O)
+ if(prob(30))
+ newB.expand(null, null, 0) //do it again!
+ return TRUE
+
+/datum/reagent/blob/replicating_foam
+ name = "Репликационная пена"
+ id = "blob_replicating_foam"
+ taste_description = "дублирование"
+ color = "#7B5A57"
+
+/datum/reagent/blob/replicating_foam/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.7*reac_volume, BRUTE, forced = TRUE)
diff --git a/code/modules/antagonists/blob/blobstrains/shifting_fragments.dm b/code/modules/antagonists/blob/blobstrains/shifting_fragments.dm
new file mode 100644
index 00000000000..90f62f0cd24
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/shifting_fragments.dm
@@ -0,0 +1,40 @@
+//does brute damage, shifts away when damaged
+/datum/blobstrain/reagent/shifting_fragments
+ name = "Смещающиеся фрагменты"
+ description = "наносит средний урон травмами."
+ effectdesc = "также смещает плитки при атаке, повреждении и расширении."
+ analyzerdescdamage = "Наносит средний урон травмами."
+ analyzerdesceffect = "Смещает плитки при атаке, повреждении и расширении."
+ color = "#C8963C"
+ complementary_color = "#3C6EC8"
+ reagent = /datum/reagent/blob/shifting_fragments
+
+/datum/blobstrain/reagent/shifting_fragments/expand_reaction(obj/structure/blob/B, obj/structure/blob/newB, turf/T, mob/camera/blob/O)
+ if(istype(B, /obj/structure/blob/normal) || (istype(B, /obj/structure/blob/shield)))
+ newB.forceMove(get_turf(B))
+ B.forceMove(T)
+ return TRUE
+
+/datum/blobstrain/reagent/shifting_fragments/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if((damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) && damage > 0 && B.get_integrity() - damage > 0 && prob(20 + damage))
+ var/list/blobstopick = list()
+ var/list/blob_structures = (is_there_multiz())? urange_multiz(1, B, TRUE) : orange(1, B)
+ for(var/obj/structure/blob/OB in blob_structures)
+ if((istype(OB, /obj/structure/blob/normal) || (istype(OB, /obj/structure/blob/shield) && prob(25))) && OB.overmind && OB.overmind.blobstrain.type == B.overmind.blobstrain.type)
+ blobstopick += OB //as long as the blob picked is valid; ie, a normal or shield blob that has the same chemical as we do, we can swap with it
+ if(blobstopick.len)
+ var/obj/structure/blob/targeted = pick(blobstopick) //randomize the blob chosen, because otherwise it'd tend to the lower left
+ var/turf/T = get_turf(targeted)
+ targeted.forceMove(get_turf(B))
+ B.forceMove(T) //swap the blobs
+ return ..()
+
+/datum/reagent/blob/shifting_fragments
+ name = "Смещающиеся фрагменты"
+ id = "blob_shifting_fragments"
+ color = "#C8963C"
+
+/datum/reagent/blob/shifting_fragments/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.7*reac_volume, BRUTE, forced = TRUE)
diff --git a/code/modules/antagonists/blob/blobstrains/synchronous_mesh.dm b/code/modules/antagonists/blob/blobstrains/synchronous_mesh.dm
new file mode 100644
index 00000000000..167d39f1e5e
--- /dev/null
+++ b/code/modules/antagonists/blob/blobstrains/synchronous_mesh.dm
@@ -0,0 +1,43 @@
+//does brute damage, bonus damage for each nearby blob, and spreads damage out
+/datum/blobstrain/reagent/synchronous_mesh
+ name = "Синхронная сетка"
+ description = "наносит небольшой урон травмами, но каждая плитка поблизости также атакует цель, нанося суммируемый урон."
+ effectdesc = "также распределяет урон между каждой плиткой рядом с атакованной плиткой."
+ analyzerdescdamage = "Наносит небольшой урон травмами, увеличивающийся с каждой плиткой рядом с целью."
+ analyzerdesceffect = "При атаке распределяет урон между всеми плитками рядом с атакованной плиткой."
+ color = "#65ADA2"
+ complementary_color = "#AD6570"
+ blobbernaut_message = "synchronously strikes"
+ message = "Блоб поражают тебя"
+ reagent = /datum/reagent/blob/synchronous_mesh
+
+/datum/blobstrain/reagent/synchronous_mesh/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag)
+ if(damage_flag == MELEE || damage_flag == BULLET || damage_flag == LASER) //the cause isn't fire or bombs, so split the damage
+ var/damagesplit = 1 //maximum split is 9, reducing the damage each blob takes to 11% but doing that damage to 9 blobs
+ var/list/blob_structures = (is_there_multiz())? urange_multiz(1, B, TRUE) : orange(1, B)
+ for(var/obj/structure/blob/C in blob_structures)
+ if(!C.ignore_syncmesh_share && C.overmind && C.overmind.blobstrain.type == B.overmind.blobstrain.type) //if it doesn't have the same chemical or is a core or node, don't split damage to it
+ damagesplit += 1
+ for(var/obj/structure/blob/C in blob_structures)
+ if(!C.ignore_syncmesh_share && C.overmind && C.overmind.blobstrain.type == B.overmind.blobstrain.type) //only hurt blobs that have the same overmind chemical and aren't cores or nodes
+ C.take_damage(damage/damagesplit, damage_type, 0, 0)
+ return damage / damagesplit
+ else
+ return damage * 1.25
+
+/datum/reagent/blob/synchronous_mesh
+ name = "Синхронная сетка"
+ id = "blob_synchronous_mesh"
+ taste_description = "токсичная плесень"
+ color = "#65ADA2"
+
+/datum/reagent/blob/synchronous_mesh/reaction_mob(mob/living/exposed_mob, methods=REAGENT_TOUCH, reac_volume, show_message, touch_protection, mob/camera/blob/overmind)
+ . = ..()
+ reac_volume = return_mob_expose_reac_volume(exposed_mob, methods, reac_volume, show_message, touch_protection, overmind)
+ exposed_mob.apply_damage(0.2*reac_volume, BRUTE, forced = TRUE)
+ var/list/blob_structures = (is_there_multiz())? urange_multiz(1, exposed_mob, TRUE) : range(1, exposed_mob)
+ if(exposed_mob && reac_volume)
+ for(var/obj/structure/blob/nearby_blob in blob_structures) //if the target is completely surrounded, this is 2.4*reac_volume bonus damage, total of 2.6*reac_volume
+ if(exposed_mob)
+ nearby_blob.blob_attack_animation(exposed_mob) //show them they're getting a bad time
+ exposed_mob.apply_damage(0.3*reac_volume, BRUTE, forced = TRUE)
diff --git a/code/modules/antagonists/blob/overmind.dm b/code/modules/antagonists/blob/overmind.dm
new file mode 100644
index 00000000000..75218bad02e
--- /dev/null
+++ b/code/modules/antagonists/blob/overmind.dm
@@ -0,0 +1,279 @@
+GLOBAL_LIST_EMPTY(overminds)
+
+
+/mob/camera/blob
+ name = "Blob Overmind"
+ real_name = "Blob Overmind"
+ desc = "The overmind. It controls the blob."
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "marker"
+ nightvision = 8
+ sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
+ invisibility = INVISIBILITY_OBSERVER
+ lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+ see_invisible = SEE_INVISIBLE_LIVING
+ pass_flags = PASSBLOB
+ faction = list(ROLE_BLOB)
+ mouse_opacity = MOUSE_OPACITY_ICON
+ move_on_shuttle = TRUE
+ layer = FLY_LAYER
+ plane = ABOVE_GAME_PLANE
+ pass_flags = PASSBLOB
+ verb_say = "states"
+
+ hud_type = /datum/hud/blob_overmind
+ var/obj/structure/blob/special/core/blob_core = null // The blob overmind's core
+ var/blob_points = 0
+ var/max_blob_points = OVERMIND_MAX_POINTS_DEFAULT
+ var/last_attack = 0
+ var/datum/blobstrain/reagent/blobstrain
+ var/list/blob_mobs = list()
+ /// A list of all blob structures
+ var/list/all_blobs = list()
+ var/list/resource_blobs = list()
+ var/list/factory_blobs = list()
+ var/list/node_blobs = list()
+ var/free_strain_rerolls = OVERMIND_STARTING_REROLLS
+ var/last_reroll_time = 0 //time since we last rerolled, used to give free rerolls
+ var/nodes_required = TRUE //if the blob needs nodes to place resource and factory blobs
+ var/list/blobs_legit = list()
+ var/max_count = 0 //The biggest it got before death
+ var/rerolling = FALSE
+ /// The list of strains the blob can reroll for.
+ var/list/strain_choices
+ /// Whether the blob split
+ var/split_used = FALSE
+ /// Is blob offspring of another blob
+ var/is_offspring = FALSE
+ /// Does the blob have an infinite resource?
+ var/is_infinity = FALSE
+
+
+/mob/camera/blob/Initialize(mapload, core = null, starting_points = OVERMIND_STARTING_POINTS)
+ ADD_TRAIT(src, TRAIT_BLOB_ALLY, INNATE_TRAIT)
+ blob_points = starting_points
+ blob_core = core
+ GLOB.overminds += src
+ var/new_name = "[initial(name)] ([rand(1, 999)])"
+ name = new_name
+ real_name = new_name
+ last_attack = world.time
+ select_strain(TRUE)
+ color = blobstrain.complementary_color
+ if(blob_core)
+ blob_core.update_blob()
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs |= src
+
+
+/mob/camera/blob/Destroy()
+ QDEL_NULL(blobstrain)
+ for(var/obj/structure/blob/blob_structure as anything in all_blobs)
+ blob_structure.overmind = null
+ blob_structure.update_blob()
+ all_blobs = null
+ resource_blobs = null
+ factory_blobs = null
+ node_blobs = null
+ for(var/mob/living/simple_animal/hostile/blob_minion/mob as anything in blob_mobs)
+ if(istype(mob) && !mob.factory_linked)
+ mob.death()
+ blob_mobs = null
+ GLOB.overminds -= src
+ QDEL_LIST_ASSOC_VAL(strain_choices)
+
+ STOP_PROCESSING(SSobj, src)
+ GLOB.blob_telepathy_mobs -= src
+
+ return ..()
+
+
+/mob/camera/blob/process()
+ if(!blob_core)
+ qdel(src)
+ return
+ if(!free_strain_rerolls && (last_reroll_time + BLOB_POWER_REROLL_FREE_TIME < world.time))
+ to_chat(src, span_boldnotice("Вы получили еще одну бесплатную смену штамма."))
+ free_strain_rerolls = TRUE
+ track_z()
+
+
+/mob/camera/blob/Login()
+ . = ..()
+ if(!. || !client)
+ return FALSE
+ sync_mind()
+ update_health_hud()
+ sync_lighting_plane_alpha()
+ add_points(0)
+ var/turf/T = get_turf(src)
+ if(isturf(T))
+ update_z(T.z)
+
+
+/mob/camera/blob/Logout()
+ update_z(null)
+ . = ..()
+
+
+/mob/camera/blob/proc/can_attack()
+ return (world.time > (last_attack + CLICK_CD_RANGE))
+
+/mob/camera/blob/Move(atom/newloc, direct = NONE, glide_size_override = 0, update_dir = TRUE)
+ if(world.time < last_movement)
+ return
+ last_movement = world.time + 0.5 // cap to 20fps
+
+ var/obj/structure/blob/B = locate() in range(OVERMIND_MAX_CAMERA_STRAY, newloc)
+ if(B)
+ loc = newloc
+ else
+ return FALSE
+
+
+/mob/camera/blob/can_z_move(direction, turf/start, turf/destination, z_move_flags = NONE, mob/living/rider)
+ . = ..()
+ if(!.)
+ return
+ var/turf/target_turf = .
+ if(!is_valid_turf(target_turf)) // Allows unplaced blobs to travel through station z-levels
+ if(z_move_flags & ZMOVE_FEEDBACK)
+ to_chat(src, span_warning("Ваш пункт назначения недействителен. Перейдите в другое место и попробуйте еще раз."))
+ return null
+
+/mob/camera/blob/proc/is_valid_turf(turf/tile)
+ var/area/area = get_area(tile)
+ if((area && !(area.area_flags & BLOBS_ALLOWED)) || !tile || !is_station_level(tile.z))
+ return FALSE
+ return TRUE
+
+
+/mob/camera/blob/get_status_tab_items()
+ . = ..()
+ if(blob_core)
+ . += list(list("Здоровье ядра:", "[blob_core.obj_integrity]"))
+ . += list(list("Ресурсы:", "[(is_infinity || SSticker?.mode?.is_blob_infinity_points)? "INF" : "[blob_points]/[max_blob_points]"]"))
+ . += list(list("Критическая Масса:", "[TOTAL_BLOB_MASS]/[NEEDED_BLOB_MASS]"))
+ if(free_strain_rerolls)
+ . += list(list("Осталось бесплатных смен штамма:", "[free_strain_rerolls]"))
+
+/mob/camera/blob/update_health_hud()
+ if(!blob_core)
+ return FALSE
+ var/current_health = round((blob_core.obj_integrity / blob_core.max_integrity) * 100)
+ hud_used.blobhealthdisplay.maptext = MAPTEXT("[current_health]%
")
+ for(var/mob/living/simple_animal/hostile/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/say(
+ message,
+ bubble_type,
+ sanitize = TRUE,
+)
+ if(!message)
+ return
+
+ if(client)
+ if(GLOB.admin_mutes_assoc[ckey] & MUTE_IC)
+ to_chat(src, span_boldwarning("Вы не можете писать IC сообщения (мут)."))
+ return
+ if(client.handle_spam_prevention(message, MUTE_IC))
+ return
+
+ if(stat)
+ return
+
+ blob_talk(message)
+
+/mob/camera/blob/proc/blob_talk(message)
+
+ message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
+
+ if(!message)
+ return
+
+ add_say_logs(src, message, language = "BLOB")
+
+ var/message_a = say_quote(message)
+ var/rendered = span_big(span_blob("\[Blob Telepathy\] [name]([blobstrain.name]) [message_a], [message]"))
+ relay_to_list_and_observers(rendered, GLOB.blob_telepathy_mobs, src)
+
+/mob/camera/blob/proc/add_points(points)
+ blob_points = clamp(blob_points + points, 0, max_blob_points)
+ hud_used.blobpwrdisplay.maptext = MAPTEXT("[(is_infinity || SSticker?.mode?.is_blob_infinity_points)? "INF" : round(blob_points)]
")
+
+
+/mob/camera/blob/proc/select_strain(first_select = FALSE)
+ var/reagent_type = pick(GLOB.valid_blobstrains)
+ set_strain(reagent_type, first_select)
+
+
+
+/mob/camera/blob/proc/set_strain(datum/blobstrain/new_strain, first_select = FALSE)
+ if(!ispath(new_strain))
+ return FALSE
+
+ var/had_strain = FALSE
+ if(istype(blobstrain))
+ blobstrain.on_lose()
+ qdel(blobstrain)
+ had_strain = TRUE
+
+ blobstrain = new new_strain(src)
+ var/datum/antagonist/blob_overmind/overmind_datum = mind?.has_antag_datum(/datum/antagonist/blob_overmind)
+ if(overmind_datum)
+ overmind_datum.strain = blobstrain
+ blobstrain.on_gain()
+
+ if(had_strain && !first_select)
+ var/list/messages = get_strain_info()
+ to_chat(src, chat_box_red(messages.Join("
")))
+ SEND_SIGNAL(src, COMSIG_BLOB_SELECTED_STRAIN, blobstrain)
+
+
+/mob/camera/blob/proc/get_strain_info()
+ . = list()
+ . += span_notice("Ваш штамм: [blobstrain.name]!")
+ . += span_notice("Штамм [blobstrain.name] [blobstrain.description]")
+ if(blobstrain.effectdesc)
+ . += span_notice("Штамм [blobstrain.name] [blobstrain.effectdesc]")
+ return .
+
+/mob/camera/blob/examine(mob/user)
+ . = ..()
+ if(blobstrain)
+ . += "Штамм блоба — [blobstrain.name]."
+
+/mob/camera/blob/blob_act(obj/structure/blob/B)
+ return
+
+/// Create a blob spore and link it to us
+/mob/camera/blob/proc/create_spore(turf/spore_turf, spore_type = /mob/living/simple_animal/hostile/blob_minion/spore/minion)
+ var/mob/living/simple_animal/hostile/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/simple_animal/hostile/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/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents = TRUE)
+ ..()
+ update_z(new_turf?.z)
diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm
new file mode 100644
index 00000000000..77f445b5467
--- /dev/null
+++ b/code/modules/antagonists/blob/powers.dm
@@ -0,0 +1,413 @@
+#define BLOB_REROLL_RADIUS 60
+
+/** Simple price check */
+/mob/camera/blob/proc/can_buy(cost = 15)
+ if(is_infinity || SSticker?.mode?.is_blob_infinity_points)
+ return TRUE
+ if(blob_points < cost)
+ to_chat(src, span_warning("Вам не хватает рескрсов, вам нужно как минимум [cost]!"))
+ balloon_alert(src, "нужно еще [cost-blob_points]!")
+ return FALSE
+ add_points(-cost)
+ return TRUE
+
+
+/** Moves the core elsewhere. */
+/mob/camera/blob/proc/transport_core()
+ if(blob_core)
+ forceMove(blob_core.drop_location())
+
+/** Jumps to a node */
+/mob/camera/blob/proc/jump_to_node()
+ if(!length(GLOB.blob_nodes))
+ return FALSE
+
+ var/list/nodes = list()
+ for(var/index in 1 to length(GLOB.blob_nodes))
+ var/obj/structure/blob/special/node/blob = GLOB.blob_nodes[index]
+ nodes["Узел #[index] ([get_area_name(blob)])"] = blob
+
+ var/node_name = tgui_input_list(src, "Выберите узел для перемещения", "Выбор узла", nodes)
+ if(isnull(node_name) || isnull(nodes[node_name]))
+ return FALSE
+
+ var/obj/structure/blob/special/node/chosen_node = nodes[node_name]
+ if(chosen_node)
+ forceMove(chosen_node.loc)
+
+
+/** Places important blob structures */
+/mob/camera/blob/proc/create_special(price, blobstrain, min_separation, needs_node, turf/tile)
+ if(!tile)
+ tile = get_turf(src)
+
+ var/obj/structure/blob/blob = (locate(/obj/structure/blob) in tile)
+ if(!blob)
+ to_chat(src, span_warning("Тут нет плитки!"))
+ balloon_alert(src, "тут нет плитки!")
+ return FALSE
+
+ if(!istype(blob, /obj/structure/blob/normal))
+ to_chat(src, span_warning("Невозможно использовать на этой плитке. Найдите обычную плитку."))
+ balloon_alert(src, "нужна обычная плитка!")
+ return FALSE
+
+ var/area/area = get_area(src)
+ if(!(area.area_flags & BLOBS_ALLOWED)) //factory and resource blobs must be legit
+ to_chat(src, span_warning("Эта плитка должна быть размещена на станции!"))
+ balloon_alert(src, "нельзя поставить вне станции!")
+ return FALSE
+
+ if(needs_node)
+ if(nodes_required && node_check(tile))
+ to_chat(src, span_warning("Вам нужно разместить эту плитку ближе к узлу или ядру!"))
+ balloon_alert(src, "слишком далеко от узла или ядра!")
+ return FALSE //handholdotron 2000
+
+ if(min_separation)
+ for(var/obj/structure/blob/other_blob in get_sep_tile(tile, min_separation))
+ if(other_blob.type == blobstrain)
+ to_chat(src, span_warning("Поблизости находится ресурсная плитка, отойдите на расстояние более [min_separation] плиток от неё!"))
+ other_blob.balloon_alert(src, "слишком близко!")
+ return FALSE
+
+ if(!can_buy(price))
+ return FALSE
+
+ var/obj/structure/blob/node = blob.change_to(blobstrain, src)
+ return node
+
+
+/mob/camera/blob/proc/node_check(turf/tile)
+ if(is_there_multiz())
+ return !(locate(/obj/structure/blob/special/node) in urange_multiz(BLOB_NODE_PULSE_RANGE, tile, TRUE)) && !(locate(/obj/structure/blob/special/core) in urange_multiz(BLOB_CORE_PULSE_RANGE, tile, TRUE))
+ return !(locate(/obj/structure/blob/special/node) in orange(BLOB_NODE_PULSE_RANGE, tile)) && !(locate(/obj/structure/blob/special/core) in orange(BLOB_CORE_PULSE_RANGE, tile))
+
+/mob/camera/blob/proc/get_sep_tile(turf/tile, min_separation)
+ if(is_there_multiz())
+ return urange_multiz(min_separation, tile, TRUE)
+ return orange(min_separation, tile)
+
+/** Creates a shield to reflect projectiles */
+/mob/camera/blob/proc/create_shield(turf/tile)
+ var/obj/structure/blob/blob = (locate(/obj/structure/blob) in tile)
+ if(!blob)
+ to_chat(src, span_warning("Тут нет плитки!"))
+ balloon_alert(src, "тут нет плитки!")
+ return FALSE
+
+ if(istype(blob, /obj/structure/blob/special))
+ to_chat(src, span_warning("Невозможно использовать на этой плитке. Найдите обычную плитку."))
+ balloon_alert(src, "нужна обычная плитка!")
+ return FALSE
+
+ var/obj/structure/blob/shield/shield = blob
+ if(!istype(shield) && can_buy(BLOB_UPGRADE_STRONG_COST))
+ shield = shield.change_to(/obj/structure/blob/shield, src)
+ shield?.balloon_alert(src, "улучшено в [shield.name]!")
+ return FALSE
+
+ if(istype(shield, /obj/structure/blob/shield/reflective))
+ to_chat(src, span_warning("Невозможно использовать на этой плитке. Ее больше некуда улучшать."))
+ balloon_alert(src, "улучшено на максимум!")
+ return FALSE
+
+ if(!can_buy(BLOB_UPGRADE_REFLECTOR_COST))
+ return FALSE
+
+ if(shield.get_integrity() < shield.max_integrity * 0.5)
+ add_points(BLOB_UPGRADE_REFLECTOR_COST)
+ to_chat(src, span_warning("Эта крепкая плитка слишком повреждена, чтобы ее можно было улучшить!"))
+ return FALSE
+
+ to_chat(src, span_warning("Вы выделяете отражающую слизь на крепкую плитку, позволяя ей отражать энергетические снаряды ценой снижения прочности."))
+ shield = shield.change_to(/obj/structure/blob/shield/reflective, src, shield.point_return)
+ shield.balloon_alert(src, "улучшено в [shield.name]!")
+
+/** Preliminary check before polling ghosts. */
+/mob/camera/blob/proc/create_blobbernaut()
+ var/turf/current_turf = get_turf(src)
+ var/obj/structure/blob/special/factory/factory = locate(/obj/structure/blob/special/factory) in current_turf
+ if(!factory)
+ to_chat(src, span_warning("Вы должны быть на фабрике блоба!"))
+ balloon_alert(src, "нужна фабрика!")
+ return FALSE
+ if(factory.blobbernaut || factory.is_creating_blobbernaut) //if it already made or making a blobbernaut, it can't do it again
+ to_chat(src, span_warning("Эта фабрика уже создает блобернаута."))
+ return FALSE
+ if(factory.get_integrity() < factory.max_integrity * 0.5)
+ to_chat(src, span_warning("Эта фабрика уже создала и поддерживает блобернаута."))
+ return FALSE
+ if(!can_buy(BLOBMOB_BLOBBERNAUT_RESOURCE_COST))
+ return FALSE
+
+ factory.is_creating_blobbernaut = TRUE
+ to_chat(src, span_notice("Вы пытаетесь создать блоббернаута."))
+ pick_blobbernaut_candidate(factory)
+
+/// Polls ghosts to get a blobbernaut candidate.
+/mob/camera/blob/proc/pick_blobbernaut_candidate(obj/structure/blob/special/factory/factory)
+ if(isnull(factory))
+ return
+ var/icon/blobbernaut_icon = icon(icon, "blobbernaut")
+ blobbernaut_icon.Blend(blobstrain.color, ICON_MULTIPLY)
+ var/image/blobbernaut_image = image(blobbernaut_icon)
+ var/list/candidates = SSghost_spawns.poll_candidates(
+ question = "Вы хотите сыграть за блобернаута?",
+ role = ROLE_BLOB,
+ poll_time = 20 SECONDS,
+ antag_age_check = TRUE,
+ check_antaghud = TRUE,
+ source = blobbernaut_image,
+ role_cleanname = "blobbernaut",
+ )
+ if(candidates.len)
+ var/mob/chosen_one = pick(candidates)
+ on_poll_concluded(factory, chosen_one)
+ else
+ to_chat(src, span_warning("Вы не смогли создать блобернаута. Ваши ресурсы были возвращены. Повторите попытку позже."))
+ add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST)
+ factory.assign_blobbernaut(null)
+
+/// Called when the ghost poll concludes
+/mob/camera/blob/proc/on_poll_concluded(obj/structure/blob/special/factory/factory, mob/dead/observer/ghost)
+ var/mob/living/simple_animal/hostile/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory))
+ assume_direct_control(blobber)
+ factory.assign_blobbernaut(blobber)
+ blobber.assign_key(ghost.key, blobstrain)
+ RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked))
+
+/// When one of our boys attacked something, we sometimes want to perform extra effects
+/mob/camera/blob/proc/on_blobbernaut_attacked(mob/living/simple_animal/hostile/blobbynaut, atom/target, success)
+ SIGNAL_HANDLER
+ if(!success)
+ return
+ if(!QDELETED(src))
+ blobstrain.blobbernaut_attack(target, blobbynaut)
+
+/** Moves the core */
+/mob/camera/blob/proc/relocate_core()
+ var/turf/tile = get_turf(src)
+ var/obj/structure/blob/special/node/blob = locate(/obj/structure/blob/special/node) in tile
+
+ if(!blob)
+ to_chat(src, span_warning("Вы должны быть на узле!"))
+ balloon_alert(src, "нужно быть на узле!")
+ return FALSE
+
+ if(!blob_core)
+ to_chat(src, span_userdanger("У вас нет ядра и вы на пороге смерти. Покойтесь с миром!"))
+ balloon_alert(src, "у вас нет ядра!")
+ return FALSE
+
+ var/area/area = get_area(tile)
+ if(isspaceturf(tile) || area && !(area.area_flags & BLOBS_ALLOWED))
+ to_chat(src, span_warning("Вы не можете переместить свое ядро сюда!"))
+ balloon_alert(src, "нельзя переместить сюда!")
+ return FALSE
+
+ if(!can_buy(BLOB_POWER_RELOCATE_COST))
+ return FALSE
+
+ var/turf/old_turf = get_turf(blob_core)
+ var/old_dir = blob_core.dir
+ blob_core.forceMove(tile)
+ blob_core.setDir(blob.dir)
+ blob.forceMove(old_turf)
+ blob.setDir(old_dir)
+
+/** Searches the tile for a blob and removes it. */
+/mob/camera/blob/proc/remove_blob(turf/tile, atom/location)
+ var/obj/structure/blob/blob = locate() in tile
+
+ if(!blob)
+ to_chat(src, span_warning("Тут нет плитки блоба!"))
+ return FALSE
+
+ if(blob.point_return < 0)
+ to_chat(src, span_warning("Невозможно удалить эту плитку."))
+ return FALSE
+
+ if(max_blob_points < blob.point_return + blob_points)
+ to_chat(src, span_warning("У вас слишком много ресурсов для удаления этой плитки!"))
+ return FALSE
+
+ if(blob.point_return)
+ add_points(blob.point_return)
+ to_chat(src, span_notice("Получено [blob.point_return] за удаление [blob]."))
+ blob.balloon_alert(src, "+[blob.point_return]")
+
+ qdel(blob)
+
+ return TRUE
+
+/** Expands to nearby tiles */
+/mob/camera/blob/proc/expand_blob(turf/tile, atom/location)
+ if(world.time < last_attack)
+ return FALSE
+ var/list/possible_blobs = list()
+ var/turf/T
+
+ if(is_there_multiz())
+ T = get_turf(location)
+ for(var/obj/structure/blob/blob in urange_multiz(1, T))
+ possible_blobs += blob
+ else
+ T = tile
+ for(var/obj/structure/blob/blob in range(1, T))
+ possible_blobs += blob
+
+ if(!length(possible_blobs))
+ to_chat(src, span_warning("Рядом с целью нету плиток блоба!"))
+ return FALSE
+
+ if(!can_buy(BLOB_EXPAND_COST))
+ return FALSE
+
+ var/attack_success
+ for(var/mob/living/player in T)
+ if(!player.can_blob_attack())
+ continue
+ if(ROLE_BLOB in player.faction) //no friendly/dead fire
+ continue
+ if(player.stat != DEAD)
+ attack_success = TRUE
+ blobstrain.attack_living(player, possible_blobs)
+
+ var/obj/structure/blob/blob = locate() in T
+
+ if(blob)
+ if(attack_success) //if we successfully attacked a turf with a blob on it, only give an attack refund
+ blob.blob_attack_animation(T, src)
+ add_points(BLOB_ATTACK_REFUND)
+ else
+ to_chat(src, span_warning("Здесь уже есть плитка!"))
+ add_points(BLOB_EXPAND_COST) //otherwise, refund all of the cost
+ else
+ directional_attack(T, possible_blobs, attack_success)
+
+ if(attack_success)
+ last_attack = world.time + CLICK_CD_MELEE
+ else
+ last_attack = world.time + CLICK_CD_RAPID
+
+
+/** Finds cardinal and diagonal attack directions */
+/mob/camera/blob/proc/directional_attack(turf/tile, list/possible_blobs, attack_success = FALSE)
+ var/list/cardinal_blobs = list()
+ var/list/diagonal_blobs = list()
+
+ for(var/obj/structure/blob/blob in possible_blobs)
+ if(get_dir_multiz(blob, tile) in GLOB.cardinals_multiz)
+ cardinal_blobs += blob
+ else
+ diagonal_blobs += blob
+
+ var/obj/structure/blob/attacker
+ if(length(cardinal_blobs))
+ attacker = pick(cardinal_blobs)
+ if(!attacker.expand(tile, src))
+ add_points(BLOB_ATTACK_REFUND) //assume it's attacked SOMETHING, possibly a structure
+ else
+ attacker = pick(diagonal_blobs)
+ if(attack_success)
+ attacker.blob_attack_animation(tile, src)
+ playsound(attacker, 'sound/effects/splat.ogg', 50, TRUE)
+ add_points(BLOB_ATTACK_REFUND)
+ else
+ add_points(BLOB_EXPAND_COST) //if we're attacking diagonally and didn't hit anything, refund
+ return TRUE
+
+/** Rally spores to a location */
+/mob/camera/blob/proc/rally_spores(turf/tile)
+ to_chat(src, "Вы направляете свои споры.")
+ var/list/surrounding_turfs = TURF_NEIGHBORS(tile)
+ if(!length(surrounding_turfs))
+ return FALSE
+ for(var/mob/living/simple_animal/hostile/blob_mob as anything in blob_mobs)
+ if(!isturf(blob_mob.loc) || get_dist(blob_mob, tile) > 35 || blob_mob.key)
+ continue
+ blob_mob.LoseTarget()
+ blob_mob.Goto(pick(surrounding_turfs), blob_mob.move_to_delay)
+
+
+/mob/camera/blob/proc/split_consciousness()
+ var/turf/T = get_turf(src)
+ if(!T)
+ return
+ var/area/Ablob = get_area(T)
+ if(isspaceturf(T) || Ablob && !(Ablob.area_flags & BLOBS_ALLOWED))
+ to_chat(src, span_warning("Вы не можете поделиться вне станции!"))
+ balloon_alert(src, "нельзя поделиться вне станции!")
+ return FALSE
+ if(split_used)
+ to_chat(src, span_warning("Вы уже произвели потомка."))
+ balloon_alert(src, "вы уже поделились!")
+ return
+ if(is_offspring)
+ to_chat(src, span_warning("Потомки блоба не могут производить потомков."))
+ balloon_alert(src, "вы сами потомок блоба!")
+ return
+
+ var/obj/structure/blob/N = (locate(/obj/structure/blob) in T)
+ if(N && !istype(N, /obj/structure/blob/special/node))
+ to_chat(src, span_warning("Для создания вашего потомка необходим узел."))
+ balloon_alert(src, "необходим узел!")
+ return
+
+ if(!can_buy(BLOB_CORE_SPLIT_COST))
+ return
+
+ split_used = TRUE
+ new /obj/structure/blob/special/core/ (get_turf(N), null, TRUE)
+ qdel(N)
+
+
+/** Opens the reroll menu to change strains */
+/mob/camera/blob/proc/strain_reroll()
+ if(!free_strain_rerolls && blob_points < BLOB_POWER_REROLL_COST)
+ to_chat(src, span_warning("Вам нужно как минимум [BLOB_POWER_REROLL_COST], чтобы снова изменить свой штамм!"))
+ return FALSE
+
+ open_reroll_menu()
+
+/** Controls changing strains */
+/mob/camera/blob/proc/open_reroll_menu()
+ if(!strain_choices)
+ strain_choices = list()
+
+ var/list/new_strains = GLOB.valid_blobstrains.Copy() - blobstrain.type
+ for (var/unused in 1 to BLOB_POWER_REROLL_CHOICES)
+ var/datum/blobstrain/strain = pick_n_take(new_strains)
+
+ var/image/strain_icon = image('icons/mob/blob.dmi', "blob_core")
+ strain_icon.color = initial(strain.color)
+
+ //var/info_text = span_boldnotice("[initial(strain.name)]")
+ //info_text += "
[span_notice("[initial(strain.analyzerdescdamage)]")]"
+ //if(!isnull(initial(strain.analyzerdesceffect)))
+ //info_text += "
[span_notice("[initial(strain.analyzerdesceffect)]")]"
+
+ strain_choices[initial(strain.name)] = strain_icon
+
+ var/strain_result = show_radial_menu(src, src, strain_choices, radius = BLOB_REROLL_RADIUS)
+ if(isnull(strain_result))
+ return
+
+ if(!free_strain_rerolls && !can_buy(BLOB_POWER_REROLL_COST))
+ return
+
+ for (var/_other_strain in GLOB.valid_blobstrains)
+ var/datum/blobstrain/other_strain = _other_strain
+ if(initial(other_strain.name) == strain_result)
+ set_strain(other_strain)
+
+ if(free_strain_rerolls)
+ free_strain_rerolls -= 1
+
+ last_reroll_time = world.time
+ strain_choices = null
+
+ return
+
+#undef BLOB_REROLL_RADIUS
diff --git a/code/modules/antagonists/blob/powers_verbs.dm b/code/modules/antagonists/blob/powers_verbs.dm
new file mode 100644
index 00000000000..6a5afd1d8d0
--- /dev/null
+++ b/code/modules/antagonists/blob/powers_verbs.dm
@@ -0,0 +1,35 @@
+/** Toggles requiring nodes */
+/mob/camera/blob/verb/toggle_node_req()
+ set category = "Blob"
+ set name = "Переключить требование узла"
+ set desc = "Переключить требование узла для размещения ресурсной плитки и фабрики."
+
+ nodes_required = !nodes_required
+ if(nodes_required)
+ to_chat(src, span_warning("Теперь вам необходимо иметь узел или ядро рядом для размещения фабрики и ресурсной плитки."))
+ else
+ to_chat(src, span_warning("Теперь вам не нужно иметь узел или ядро рядом для размещения фабрики и ресурсной плитки."))
+
+/mob/camera/blob/verb/blob_help()
+ set category = "Blob"
+ set name = "*Помощь*"
+ set desc = "Подсказывает, как быть блобом."
+ var/list/messages = get_blob_help_messages(blobstrain)
+ to_chat(src, chat_box_regular(messages.Join("
")))
+
+/mob/camera/blob/verb/blob_broadcast()
+ set category = "Blob"
+ set name = "Ретрянсляция блоба"
+ set desc = "Говорите, используя споры и блобернаутов в качестве рупоров. Это действие бесплатно."
+
+ var/speak_text = tgui_input_text(usr, "Что вы хотите сказать от лица ваших созданий?", "Ретрянсляция блоба", null)
+
+ if(!speak_text)
+ return
+ else
+ to_chat(usr, "Вы говорите от лица ваших созданий, [speak_text]")
+ for(var/mob/living/simple_animal/hostile/blob_minion in blob_mobs)
+ if(blob_minion.stat == CONSCIOUS)
+ add_say_logs(usr, speak_text, language = "BLOB Broadcast")
+ blob_minion.atom_say(speak_text)
+ return
diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm
new file mode 100644
index 00000000000..2568444e4c6
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/_blob.dm
@@ -0,0 +1,424 @@
+//I will need to recode parts of this but I am way too tired atm
+/obj/structure/blob
+ name = "blob"
+ icon = 'icons/mob/blob.dmi'
+ light_range = 3
+ desc = "Толстая стена извивающихся щупалец."
+ density = FALSE
+ opacity = TRUE
+ anchored = TRUE
+ pass_flags_self = PASSBLOB
+ layer = BELOW_MOB_LAYER
+ can_astar_pass = CANASTARPASS_ALWAYS_PROC
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 70)
+ creates_cover = TRUE
+ obj_flags = BLOCK_Z_OUT_DOWN | BLOCK_Z_IN_UP // stops blob mobs from falling on multiz.
+ max_integrity = BLOB_REGULAR_MAX_HP
+ /// Multiplies brute damage by this
+ var/brute_resist = BLOB_BRUTE_RESIST
+ /// Multiplies burn damage by this
+ var/fire_resist = BLOB_FIRE_RESIST
+ /// how much health this blob regens when pulsed
+ var/health_regen = BLOB_REGULAR_HP_REGEN
+ /// How many points the blob gets back when it removes a blob of that type. If less than 0, blob cannot be removed.
+ var/point_return = 0
+ /// If a threshold is reached, resulting in shifting variables
+ var/compromised_integrity = FALSE
+ /// Blob overmind
+ var/mob/camera/blob/overmind
+ /// We got pulsed when?
+ COOLDOWN_DECLARE(pulse_timestamp)
+ /// we got healed when?
+ COOLDOWN_DECLARE(heal_timestamp)
+ /// Only used by the synchronous mesh strain. If set to true, these blobs won't share or receive damage taken with others.
+ var/ignore_syncmesh_share = FALSE
+ /// If the blob blocks atmos and heat spread
+ var/atmosblock = FALSE
+
+/obj/structure/blob/ComponentInitialize()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+
+/obj/structure/blob/Initialize(mapload, owner_overmind)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_CHASM_DESTROYED, INNATE_TRAIT)
+ GLOB.blobs += src
+ if(owner_overmind && isovermind(owner_overmind))
+ link_to_overmind(owner_overmind)
+ setDir(pick(GLOB.cardinal))
+ if(atmosblock)
+ air_update_turf(TRUE)
+ ConsumeTile()
+ update_blob()
+
+
+/obj/structure/blob/proc/link_to_overmind(mob/camera/blob/owner_overmind)
+ overmind = owner_overmind
+ overmind.all_blobs += src
+
+
+/obj/structure/blob/Destroy()
+ if(atmosblock)
+ atmosblock = FALSE
+ air_update_turf(1)
+ GLOB.blobs -= src
+ SSticker?.mode?.legit_blobs -= src
+ if(overmind)
+ overmind.all_blobs -= src
+ overmind.blobs_legit -= src //if it was in the legit blobs list, it isn't now
+ overmind = null
+ if(isturf(loc)) //Necessary because Expand() is screwed up and spawns a blob and then deletes it
+ playsound(src.loc, 'sound/effects/splat.ogg', 50, 1)
+ return ..()
+
+/obj/structure/blob/obj_destruction(damage_flag)
+ if(overmind)
+ overmind.blobstrain.death_reaction(src, damage_flag)
+ . = ..()
+
+/obj/structure/blob/Adjacent(atom/neighbour)
+ . = ..()
+ if(.)
+ var/result = 0
+ var/direction = get_dir(src, neighbour)
+ var/list/dirs = list("[NORTHWEST]" = list(NORTH, WEST), "[NORTHEAST]" = list(NORTH, EAST), "[SOUTHEAST]" = list(SOUTH, EAST), "[SOUTHWEST]" = list(SOUTH, WEST))
+ for(var/A in dirs)
+ if(direction == text2num(A))
+ for(var/B in dirs[A])
+ var/C = locate(/obj/structure/blob) in get_step(src, B)
+ if(C)
+ result++
+ . -= result - 1
+
+
+/obj/structure/blob/BlockSuperconductivity()
+ return atmosblock
+
+
+/obj/structure/blob/CanAtmosPass(turf/T, vertical)
+ return !atmosblock
+
+
+/obj/structure/blob/update_icon() //Updates color based on overmind color if we have an overmind.
+ . = ..()
+ if(overmind)
+ add_atom_colour(overmind.blobstrain.color, FIXED_COLOUR_PRIORITY)
+ var/area/A = get_area(src)
+ if(!(A.area_flags & BLOBS_ALLOWED))
+ add_atom_colour(BlendRGB(overmind.blobstrain.color, COLOR_WHITE, 0.5), FIXED_COLOUR_PRIORITY) //lighten it to indicate an off-station blob
+ else
+ remove_atom_colour(FIXED_COLOUR_PRIORITY)
+
+
+/obj/structure/blob/proc/Be_Pulsed()
+ if(COOLDOWN_FINISHED(src, pulse_timestamp))
+ ConsumeTile()
+ if(COOLDOWN_FINISHED(src, heal_timestamp))
+ RegenHealth()
+ COOLDOWN_START(src, heal_timestamp, 20)
+ update_blob()
+ COOLDOWN_START(src, pulse_timestamp, 10)
+ return TRUE//we did it, we were pulsed!
+ return FALSE //oh no we failed
+
+
+/obj/structure/blob/proc/RegenHealth()
+ obj_integrity = min(max_integrity, obj_integrity + health_regen)
+ update_blob()
+
+
+/obj/structure/blob/proc/ConsumeTile()
+ for(var/atom/thing in loc)
+ if(!thing.can_blob_attack())
+ continue
+ 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
+ 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
+
+
+/obj/structure/blob/proc/blob_attack_animation(atom/A = null, controller) //visually attacks an atom
+ var/obj/effect/temp_visual/blob/O = new /obj/effect/temp_visual/blob(src.loc)
+ O.setDir(dir)
+ var/area/my_area = get_area(src)
+ if(controller)
+ var/mob/camera/blob/BO = controller
+ O.color = BO.blobstrain.color
+ if(!(my_area.area_flags & BLOBS_ALLOWED))
+ O.color = BlendRGB(O.color, COLOR_WHITE, 0.5) //lighten it to indicate an off-station blob
+ O.alpha = 200
+ else if(overmind)
+ O.color = overmind.blobstrain.color
+ if(!(my_area.area_flags & BLOBS_ALLOWED))
+ O.color = BlendRGB(O.color, COLOR_WHITE, 0.5) //lighten it to indicate an off-station blob
+ if(A)
+ O.do_attack_animation(A) //visually attack the whatever
+ return O //just in case you want to do something to the animation.
+
+
+/obj/structure/blob/proc/expand(turf/T = null, controller = null, expand_reaction = 1)
+ if(!T)
+ var/list/dirs = (is_there_multiz())? GLOB.cardinals_multiz.Copy() : GLOB.cardinal.Copy()
+ for(var/i = 1 to dirs.len)
+ var/dirn = pick(dirs)
+ dirs.Remove(dirn)
+ T = get_step_multiz(src, dirn)
+ if(!(locate(/obj/structure/blob) in T))
+ break
+ else
+ T = null
+ if(!T)
+ return
+
+ if(!is_location_within_transition_boundaries(T))
+ return
+ var/make_blob = TRUE //can we make a blob?
+
+ if(isspaceturf(T) && !(locate(/obj/structure/lattice) in T))
+ if(SEND_SIGNAL(T, COMSIG_TRY_CONSUME_TURF) & COMPONENT_CANT_CONSUME)
+ make_blob = FALSE
+ playsound(src.loc, 'sound/effects/splat.ogg', 50, TRUE) //Let's give some feedback that we DID try to spawn in space, since players are used to it
+
+ ConsumeTile() //hit the tile we're in, making sure there are no border objects blocking us
+ if(!T.CanPass(src, get_dir(T, src))) //is the target turf impassable
+ if(SEND_SIGNAL(T, COMSIG_TRY_CONSUME_TURF) & COMPONENT_CANT_CONSUME)
+ make_blob = FALSE
+ T.blob_act(src) //hit the turf if it is
+ for(var/atom/A in T)
+ if(!A.CanPass(src, get_dir(T, src))) //is anything in the turf impassable
+ make_blob = FALSE
+ if(!A.can_blob_attack())
+ continue
+ if(isliving(A) && overmind && !controller) // Make sure to inject strain-reagents with automatic attacks when needed.
+ var/mob/living/mob = A
+ if(ROLE_BLOB in mob.faction) //no friendly fire
+ continue
+ overmind.blobstrain.attack_living(mob)
+ continue // Don't smack them twice though
+ A.blob_act(src) //also hit everything in the turf
+
+ if(make_blob) //well, can we?
+ var/obj/structure/blob/B = new /obj/structure/blob/normal(src.loc, (controller || overmind))
+ B.set_density(TRUE)
+ if(T.Enter(B)) //NOW we can attempt to move into the tile
+ B.set_density(initial(B.density))
+ B.forceMove(T)
+ var/offstation = FALSE
+ var/area/Ablob = get_area(B)
+ if(Ablob.area_flags & BLOBS_ALLOWED) //Is this area allowed for winning as blob?
+ overmind.blobs_legit |= B
+ SSticker?.mode?.legit_blobs |= B
+ else if(controller)
+ B.balloon_alert(overmind, "вне станции, не считается!")
+ offstation = TRUE
+ B.update_blob()
+ var/reaction_result = TRUE
+ if(B.overmind && expand_reaction)
+ reaction_result = B.overmind.blobstrain.expand_reaction(src, B, T, controller, offstation)
+ if(reaction_result && is_there_multiz() && check_level_trait(T.z, ZTRAIT_DOWN) && !isopenspaceturf(T))
+ T.ChangeTurf(/turf/simulated/openspace)
+ return B
+ else
+ blob_attack_animation(T, controller)
+ T.blob_act(src) //if we can't move in hit the turf again
+ qdel(B) //we should never get to this point, since we checked before moving in. destroy the blob so we don't have two blobs on one tile
+ return
+ else
+ blob_attack_animation(T, controller) //if we can't, animate that we attacked
+ return
+
+
+/obj/structure/blob/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ var/mob/mover_mob = mover
+ return checkpass(mover, PASSBLOB) || (istype(mover_mob) && mover_mob.stat == DEAD)
+
+
+/obj/structure/blob/CanAStarPass(to_dir, datum/can_pass_info/pass_info)
+ return pass_info.pass_flags == PASSEVERYTHING || (pass_info.pass_flags & PASSBLOB)
+
+
+/obj/structure/blob/emp_act(severity)
+ . = ..()
+ // tgstation emp protection
+ //if(. & EMP_PROTECT_SELF)
+ //return
+ if(severity > 0)
+ if(overmind)
+ overmind.blobstrain.emp_reaction(src, severity)
+ if(prob(100 - severity * 30))
+ new /obj/effect/temp_visual/emp(get_turf(src))
+
+
+/obj/structure/blob/tesla_act(power)
+ if(overmind)
+ if(overmind.blobstrain.tesla_reaction(src, power))
+ take_damage(power * 1.25e-3, BURN, ENERGY)
+ else
+ take_damage(power * 1.25e-3, BURN, ENERGY)
+ power -= power * 2.5e-3 //You don't get to do it for free
+ return ..() //You don't get to do it for free
+
+
+/obj/structure/blob/blob_act(obj/structure/blob/B)
+ return
+
+
+/obj/structure/blob/extinguish()
+ . = ..()
+ if(overmind)
+ overmind.blobstrain.extinguish_reaction(src)
+
+
+/obj/structure/blob/hit_by_thrown_carbon(mob/living/carbon/human/C, datum/thrownthing/throwingdatum, damage, mob_hurt, self_hurt)
+ damage *= 0.25 // Lets not have sorium be too much of a blender / rapidly kill itself
+ return ..()
+
+
+/obj/structure/blob/attack_animal(mob/living/simple_animal/M)
+ if(ROLE_BLOB in M.faction) //sorry, but you can't kill the blob as a blobbernaut
+ to_chat(M, span_danger("Вы не можете навредить структурам блоба"))
+ return
+ ..()
+
+
+/obj/structure/blob/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = NONE)
+ switch(damage_type)
+ if(BRUTE)
+ if(damage_amount)
+ playsound(src.loc, 'sound/effects/attackblob.ogg', 50, TRUE)
+ else
+ playsound(src, 'sound/weapons/tap.ogg', 50, TRUE)
+ if(BURN)
+ playsound(src.loc, 'sound/items/welder.ogg', 100, TRUE)
+
+
+/obj/structure/blob/run_obj_armor(damage_amount, damage_type, damage_flag = 0, attack_dir)
+ switch(damage_type)
+ if(BRUTE)
+ damage_amount *= brute_resist
+ if(BURN)
+ damage_amount *= fire_resist
+ else
+ return 0
+ var/armor_protection = 0
+ if(damage_flag)
+ armor_protection = armor.getRating(damage_flag)
+ damage_amount = round(damage_amount * (100 - armor_protection)*0.01, 0.1)
+ if(overmind && damage_flag)
+ damage_amount = overmind.blobstrain.damage_reaction(src, damage_amount, damage_type, damage_flag)
+ return damage_amount
+
+
+/obj/structure/blob/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
+ if(QDELETED(src))
+ return
+ . = ..()
+ if(. && obj_integrity > 0)
+ update_blob()
+
+
+/obj/structure/blob/has_prints()
+ return FALSE
+
+/obj/structure/blob/proc/update_state()
+ return
+
+/obj/structure/blob/proc/update_blob()
+ update_state()
+ update_appearance()
+
+/obj/structure/blob/proc/Life()
+ return
+
+/obj/structure/blob/proc/run_action()
+ return FALSE
+
+/obj/structure/blob/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ arrived.blob_act(src)
+
+
+/obj/structure/blob/proc/change_to(type, controller, point_return = 0)
+ if(!ispath(type))
+ CRASH("change_to(): invalid type for blob")
+ var/obj/structure/blob/B = new type(src.loc, controller)
+ B.update_blob()
+ B.setDir(dir)
+ B.point_return += point_return
+ qdel(src)
+ return B
+
+
+/obj/structure/blob/attackby(obj/item/I, mob/user, params)
+ if(I.tool_behaviour == TOOL_ANALYZER)
+ user.changeNext_move(CLICK_CD_MELEE)
+ to_chat(user, "Анализатор подает один звуковой сигнал, затем сообщает:
")
+ SEND_SOUND(user, sound('sound/machines/ping.ogg'))
+ if(overmind)
+ to_chat(user, "Прогресс Критической Массы: [span_notice("[TOTAL_BLOB_MASS]/[NEEDED_BLOB_MASS].")]")
+ to_chat(user, chemeffectreport(user).Join("\n"))
+ else
+ to_chat(user, "Ядро блоба нейтрализовано. Критическая масса более не достижима.")
+ to_chat(user, typereport(user).Join("\n"))
+ return ATTACK_CHAIN_PROCEED_SUCCESS
+ else
+ return ..()
+
+
+/obj/structure/blob/examine(mob/user)
+ . = ..()
+ var/datum/atom_hud/hud_to_check = GLOB.huds[DATA_HUD_MEDICAL_ADVANCED]
+ if(user.research_scanner || hud_to_check.hudusers[user])
+ . += "Ваш HUD отображает обширный отчет...
"
+ if(overmind)
+ . += overmind.blobstrain.examine(user)
+ else
+ . += "Ядро блоба нейтрализовано. Критическая масса более не достижима."
+ . += chemeffectreport(user)
+ . += typereport(user)
+ else
+ if((user == overmind || isobserver(user)) && overmind)
+ . += overmind.blobstrain.examine(user)
+ . += "Кажется, он состоит из [get_chem_name()]."
+
+
+/obj/structure/blob/proc/scannerreport()
+ return "Обычная плитка. Похоже, кто-то забыл переопределить этот процесс, сообщите администратору и составьте баг-репорт."
+
+
+
+/obj/structure/blob/proc/chemeffectreport(mob/user)
+ RETURN_TYPE(/list)
+ . = list()
+ if(overmind)
+ . += list("Материал: [overmind.blobstrain.name][span_notice(".")]",
+ "Эффект материала: [span_notice("[overmind.blobstrain.analyzerdescdamage]")]",
+ "Свойства материала: [span_notice("[overmind.blobstrain.analyzerdesceffect || "N/A"]")]")
+ else
+ . += "Материал не найден!"
+
+/obj/structure/blob/proc/typereport(mob/user)
+ RETURN_TYPE(/list)
+ return list("Тип плитки: [span_notice("[uppertext(initial(name))]")]",
+ "Здоровье: [span_notice("[obj_integrity]/[max_integrity]")]",
+ "Эффекты: [span_notice("[scannerreport()]")]")
+
+
+/obj/structure/blob/proc/get_chem_name()
+ if(overmind)
+ return overmind.blobstrain.name
+ return "какая-то органическая материя"
+
+
+/obj/structure/blob/proc/get_chem_desc()
+ if(overmind)
+ return overmind.blobstrain.description
+ return "что-то неизвестное"
+
diff --git a/code/modules/antagonists/blob/structures/captured_nuke.dm b/code/modules/antagonists/blob/structures/captured_nuke.dm
new file mode 100644
index 00000000000..a723d0dd0dd
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/captured_nuke.dm
@@ -0,0 +1,31 @@
+/obj/structure/blob/special/captured_nuke //alternative to blob just straight up destroying nukes
+ name = "blob captured nuke"
+ icon_state = "blob"
+ desc = "Ядерная боеголовка спуталась в щупальцах блоба, пульсирующих ужасающим зеленым свечением."
+ max_integrity = BLOB_CAP_NUKE_MAX_HP
+ health_regen = BLOB_CAP_NUKE_HP_REGEN
+ point_return = BLOB_REFUND_CAP_NUKE_COST
+
+/obj/structure/blob/special/captured_nuke/Initialize(mapload, owner_overmind, obj/machinery/nuclearbomb/N)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ N?.forceMove(src)
+ update_icon(UPDATE_OVERLAYS)
+
+
+/obj/structure/blob/special/captured_nuke/update_overlays()
+ . = ..()
+ . += mutable_appearance('icons/mob/blob.dmi', "blob_nuke_overlay", appearance_flags = RESET_COLOR)
+
+
+/obj/structure/blob/special/captured_nuke/Destroy()
+ for(var/obj/machinery/nuclearbomb/O in contents)
+ O.forceMove(loc)
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/structure/blob/special/captured_nuke/process()
+ if(COOLDOWN_FINISHED(src, heal_timestamp))
+ RegenHealth()
+ COOLDOWN_START(src, heal_timestamp, 20)
+
diff --git a/code/modules/antagonists/blob/structures/core.dm b/code/modules/antagonists/blob/structures/core.dm
new file mode 100644
index 00000000000..f364a4d88fd
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/core.dm
@@ -0,0 +1,167 @@
+/obj/structure/blob/special/core
+ name = "blob core"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blank_blob"
+ desc = "Огромная пульсирующая желтая масса."
+ max_integrity = BLOB_CORE_MAX_HP
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 90)
+ explosion_block = 6
+ explosion_vertical_block = 5
+ point_return = BLOB_REFUND_CORE_COST
+ fire_resist = BLOB_CORE_FIRE_RESIST
+ health_regen = BLOB_CORE_HP_REGEN
+ resistance_flags = LAVA_PROOF
+ strong_reinforce_range = BLOB_CORE_STRONG_REINFORCE_RANGE
+ reflector_reinforce_range = BLOB_CORE_REFLECTOR_REINFORCE_RANGE
+ claim_range = BLOB_CORE_CLAIM_RANGE
+ pulse_range = BLOB_CORE_PULSE_RANGE
+ expand_range = BLOB_CORE_EXPAND_RANGE
+ ignore_syncmesh_share = TRUE
+ COOLDOWN
+ var/overmind_get_delay = 0 // we don't want to constantly try to find an overmind, do it every 5 minutes
+ var/is_offspring = null
+ var/selecting = 0
+
+
+/obj/structure/blob/special/core/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/stationloving, FALSE, TRUE)
+
+
+/obj/structure/blob/special/core/Initialize(mapload, client/new_overmind = null, offspring)
+ GLOB.blob_cores += src
+ START_PROCESSING(SSobj, src)
+ GLOB.poi_list |= src
+ update_blob() //so it atleast appears
+ if(!overmind)
+ create_overmind(new_overmind)
+ is_offspring = offspring
+ if(overmind)
+ overmind.blobstrain.on_gain()
+ update_blob()
+ return ..()
+
+
+/obj/structure/blob/special/core/Destroy()
+ GLOB.blob_cores -= src
+ if(overmind)
+ overmind.blob_core = null
+ overmind = null
+ SSticker?.mode?.blob_died()
+ STOP_PROCESSING(SSobj, src)
+ GLOB.poi_list.Remove(src)
+ for(var/atom/movable/atom as anything in contents)
+ if(atom && !QDELETED(atom) && istype(atom))
+ atom.forceMove(get_turf(src))
+ atom.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), 6, 5, src, TRUE, FALSE, null, 3)
+ return ..()
+
+/obj/structure/blob/special/core/scannerreport()
+ return "Управляет расширением блоба, постепенно расширяется и поддерживает близлежащие споры и блобернаутов."
+
+/obj/structure/blob/special/core/update_overlays()
+ . = ..()
+ var/mutable_appearance/blob_overlay = mutable_appearance('icons/mob/blob.dmi', "blob")
+ if(overmind)
+ blob_overlay.color = overmind.blobstrain.color
+ . += blob_overlay
+ . += mutable_appearance('icons/mob/blob.dmi', "blob_core_overlay")
+ if(blocks_emissive)
+ add_overlay(get_emissive_block())
+
+/obj/structure/blob/special/core/update_icon()
+ . = ..()
+ color = null
+
+/obj/structure/blob/special/core/ex_act(severity, target)
+ var/damage = 10 * (severity + 1) //remember, the core takes half brute damage, so this is 20/15/10 damage based on severity
+ take_damage(damage, BRUTE, BOMB, 0)
+ return TRUE
+
+
+/obj/structure/blob/special/core/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, overmind_reagent_trigger = 1)
+ . = ..()
+ if(obj_integrity > 0)
+ if(overmind) //we should have an overmind, but...
+ overmind.update_health_hud()
+
+/obj/structure/blob/special/core/RegenHealth()
+ return // Don't regen, we handle it in Life()
+
+/obj/structure/blob/special/core/process(seconds_per_tick)
+ if(QDELETED(src))
+ return
+ if(!overmind)
+ create_overmind()
+ if(overmind)
+ overmind.blobstrain.core_process()
+ overmind.update_health_hud()
+ pulse_area(overmind, claim_range, pulse_range, expand_range)
+ reinforce_area(seconds_per_tick)
+ ..()
+
+
+/obj/structure/blob/special/core/proc/create_overmind(client/new_overmind, override_delay)
+ if(overmind_get_delay > world.time && !override_delay)
+ return
+
+ overmind_get_delay = world.time + 5 MINUTES
+
+ if(overmind)
+ qdel(overmind)
+ if(new_overmind)
+ get_new_overmind(new_overmind)
+ else
+ INVOKE_ASYNC(src, PROC_REF(get_new_overmind))
+
+
+/obj/structure/blob/special/core/proc/get_new_overmind(client/new_overmind)
+ var/mob/C = null
+ var/list/candidates = list()
+ if(!new_overmind)
+ // sendit
+ if(is_offspring)
+ candidates = SSghost_spawns.poll_candidates("Вы хотите поиграть за потомка блоба?", ROLE_BLOB, TRUE, source = src)
+ else
+ candidates = SSghost_spawns.poll_candidates("Вы хотите поиграть за блоба?", ROLE_BLOB, TRUE, source = src)
+
+ if(length(candidates))
+ C = pick(candidates)
+ else
+ C = new_overmind
+
+ if(C && !QDELETED(src))
+ var/mob/camera/blob/B = new(loc, src)
+ B.blob_core = src
+ B.mind_initialize()
+ B.key = C.key
+ overmind = B
+ B.is_offspring = is_offspring
+ addtimer(CALLBACK(src, PROC_REF(add_datum_if_not_exist)), TIME_TO_ADD_OM_DATUM)
+ log_game("[B.key] has become Blob [is_offspring ? "offspring" : ""]")
+
+
+/obj/structure/blob/special/core/proc/add_datum_if_not_exist()
+ if(!overmind.mind.has_antag_datum(/datum/antagonist/blob_overmind))
+ var/datum/antagonist/blob_overmind/overmind_datum = new
+ overmind_datum.add_to_mode = TRUE
+ overmind_datum.is_offspring = is_offspring
+ if(overmind.blobstrain)
+ overmind_datum.strain = overmind.blobstrain
+ overmind.mind.add_antag_datum(overmind_datum)
+
+/obj/structure/blob/special/core/proc/lateblobtimer()
+ addtimer(CALLBACK(src, PROC_REF(lateblobcheck)), 50)
+
+/obj/structure/blob/special/core/proc/lateblobcheck()
+ if(overmind)
+ overmind.add_points(BLOB_BONUS_POINTS)
+ if(!overmind.mind)
+ log_debug("/obj/structure/blob/core/proc/lateblobcheck: Blob core lacks a overmind.mind.")
+ else
+ log_debug("/obj/structure/blob/core/proc/lateblobcheck: Blob core lacks an overmind.")
+
+/obj/structure/blob/special/core/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer)
+ if(overmind && is_station_level(new_turf?.z))
+ overmind.forceMove(get_turf(src))
+ return ..()
diff --git a/code/modules/antagonists/blob/structures/factory.dm b/code/modules/antagonists/blob/structures/factory.dm
new file mode 100644
index 00000000000..6c42e4c770b
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/factory.dm
@@ -0,0 +1,103 @@
+/obj/structure/blob/special/factory
+ name = "factory blob"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blob_factory"
+ desc = "Толстый шпиль щупалец."
+ max_integrity = BLOB_FACTORY_MAX_HP
+ health_regen = BLOB_FACTORY_HP_REGEN
+ point_return = BLOB_REFUND_FACTORY_COST
+ resistance_flags = LAVA_PROOF
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 25, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 70)
+ ///How many spores this factory can have.
+ var/max_spores = BLOB_FACTORY_MAX_SPORES
+ ///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_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
+
+
+/obj/structure/blob/special/factory/scannerreport()
+ if(blobbernaut)
+ return "В настоящее время он поддерживает блобернаута, что делает ее хрупкой и неспособной производить споры."
+ return "Каждые несколько секунд производит споры."
+
+/obj/structure/blob/special/factory/link_to_overmind(mob/camera/blob/owner_overmind)
+ . = ..()
+ owner_overmind.factory_blobs += src
+ if(!owner_overmind.blobstrain)
+ return .
+ for(var/mob in spores_and_zombies)
+ owner_overmind.assume_direct_control(mob)
+ if(blobbernaut)
+ owner_overmind.assume_direct_control(blobbernaut)
+
+/obj/structure/blob/special/factory/Destroy()
+ spores_and_zombies = null
+ blobbernaut = null
+ if(overmind)
+ overmind.factory_blobs -= src
+ return ..()
+
+/obj/structure/blob/special/factory/Be_Pulsed()
+ . = ..()
+ if(blobbernaut)
+ return
+ if(length(spores_and_zombies) >= max_spores)
+ return
+ if(!COOLDOWN_FINISHED(src, spore_delay))
+ return
+ COOLDOWN_START(src, spore_delay, spore_cooldown)
+ flick("blob_factory_glow", src)
+ var/mob/living/simple_animal/hostile/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/simple_animal/hostile/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("Блобернаут [pick("разрывает", "надрывает", "рвет в клочья")] все на своем пути из фабрики!"))
+ playsound(loc, 'sound/effects/splat.ogg', 50, TRUE)
+
+ blobbernaut = new_naut
+ blobbernaut.link_to_factory(src)
+ RegisterSignal(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/antagonists/blob/structures/node.dm b/code/modules/antagonists/blob/structures/node.dm
new file mode 100644
index 00000000000..2173b212114
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/node.dm
@@ -0,0 +1,57 @@
+/obj/structure/blob/special/node
+ name = "blob node"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blank_blob"
+ desc = "Большая пульсирующая желтая масса."
+ max_integrity = BLOB_NODE_MAX_HP
+ health_regen = BLOB_NODE_HP_REGEN
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 25, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 65, "acid" = 90)
+ point_return = BLOB_REFUND_NODE_COST
+ claim_range = BLOB_NODE_CLAIM_RANGE
+ pulse_range = BLOB_NODE_PULSE_RANGE
+ expand_range = BLOB_NODE_EXPAND_RANGE
+ resistance_flags = LAVA_PROOF
+ ignore_syncmesh_share = TRUE
+
+/obj/structure/blob/special/node/Initialize(mapload)
+ GLOB.blob_nodes += src
+ START_PROCESSING(SSobj, src)
+ . = ..()
+
+
+/obj/structure/blob/special/node/scannerreport()
+ return "Постепенно расширяется и поддерживает близлежащие споры и блобернаутов."
+
+/obj/structure/blob/special/node/update_icon()
+ . = ..()
+ color = null
+
+/obj/structure/blob/special/node/update_overlays()
+ . = ..()
+ var/mutable_appearance/blob_overlay = mutable_appearance('icons/mob/blob.dmi', "blob")
+ if(overmind)
+ blob_overlay.color = overmind.blobstrain.color
+ var/area/A = get_area(src)
+ if(!(A.area_flags & BLOBS_ALLOWED))
+ blob_overlay.color = BlendRGB(overmind.blobstrain.color, COLOR_WHITE, 0.5) //lighten it to indicate an off-station blob
+ . += blob_overlay
+ . += mutable_appearance('icons/mob/blob.dmi', "blob_node_overlay")
+ if(blocks_emissive)
+ add_overlay(get_emissive_block())
+
+
+/obj/structure/blob/special/node/link_to_overmind(mob/camera/blob/owner_overmind)
+ . = ..()
+ overmind.node_blobs += src
+
+/obj/structure/blob/special/node/Destroy()
+ GLOB.blob_nodes -= src
+ STOP_PROCESSING(SSobj, src)
+ if(overmind)
+ overmind.node_blobs -= src
+ return ..()
+
+/obj/structure/blob/special/node/process(seconds_per_tick)
+ if(overmind)
+ pulse_area(overmind, claim_range, pulse_range, expand_range)
+ reinforce_area(seconds_per_tick)
diff --git a/code/modules/antagonists/blob/structures/normal.dm b/code/modules/antagonists/blob/structures/normal.dm
new file mode 100644
index 00000000000..2538f64825a
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/normal.dm
@@ -0,0 +1,49 @@
+/obj/structure/blob/normal
+ name = "normal blob"
+ icon_state = "blob"
+ light_range = 0
+ max_integrity = BLOB_REGULAR_MAX_HP
+ var/initial_integrity = BLOB_REGULAR_HP_INIT
+ health_regen = BLOB_REGULAR_HP_REGEN
+ brute_resist = BLOB_BRUTE_RESIST * 0.5
+
+
+/obj/structure/blob/normal/Initialize(mapload, owner_overmind)
+ . = ..()
+ update_integrity(initial_integrity)
+
+/obj/structure/blob/normal/scannerreport()
+ if(compromised_integrity)
+ return "В настоящее время слаб к урону травмами."
+ return "N/A"
+
+/obj/structure/blob/normal/update_name()
+ . = ..()
+ name = "[(compromised_integrity) ? "fragile " : (overmind ? null : "dead ")][initial(name)]"
+
+/obj/structure/blob/normal/update_desc()
+ . = ..()
+ if(compromised_integrity)
+ desc = "Тонкая решетка слегка подергивающихся щупалец."
+ else if(overmind)
+ desc = "Толстая стена извивающихся щупалец."
+ else
+ desc = "Толстая стена извивающихся щупалец."
+
+/obj/structure/blob/normal/update_icon_state()
+ icon_state = "blob[(compromised_integrity) ? "_damaged" : null]"
+ return ..()
+
+
+/obj/structure/blob/normal/update_state()
+ if(obj_integrity <= 15)
+ compromised_integrity = TRUE
+ else
+ compromised_integrity = FALSE
+
+ if(compromised_integrity)
+ brute_resist = BLOB_BRUTE_RESIST
+ else if(overmind)
+ brute_resist = BLOB_BRUTE_RESIST * 0.5
+ else
+ brute_resist = BLOB_BRUTE_RESIST * 0.5
diff --git a/code/modules/antagonists/blob/structures/resource.dm b/code/modules/antagonists/blob/structures/resource.dm
new file mode 100644
index 00000000000..fabfd35e007
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/resource.dm
@@ -0,0 +1,35 @@
+/obj/structure/blob/special/resource
+ name = "resource blob"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blob_resource"
+ desc = "Тонкий шпиль слегка покачивающихся щупалец."
+ max_integrity = BLOB_RESOURCE_MAX_HP
+ point_return = BLOB_REFUND_RESOURCE_COST
+ resistance_flags = LAVA_PROOF
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 25, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 70)
+ var/resource_delay = 0
+ var/point_rate = BLOB_RESOURCE_POINT_RATE
+
+/obj/structure/blob/special/resource/scannerreport()
+ return "Постепенно снабжает блоба ресурсами, увеличивая скорость расширения."
+
+/obj/structure/blob/special/resource/link_to_overmind(mob/camera/blob/owner_overmind)
+ . = ..()
+ overmind.resource_blobs += src
+
+/obj/structure/blob/special/resource/Destroy()
+ if(overmind)
+ overmind.resource_blobs -= src
+ return ..()
+
+/obj/structure/blob/special/resource/Be_Pulsed()
+ . = ..()
+ if(resource_delay > world.time)
+ return
+ flick("blob_resource_glow", src)
+ if(overmind)
+ overmind.add_points(point_rate)
+ balloon_alert(overmind, "+[point_rate] resource\s")
+ resource_delay = world.time + BLOB_RESOURCE_GATHER_DELAY + overmind.resource_blobs.len * BLOB_RESOURCE_GATHER_ADDED_DELAY //4 seconds plus a quarter second for each resource blob the overmind has
+ else
+ resource_delay = world.time + BLOB_RESOURCE_GATHER_DELAY
diff --git a/code/modules/antagonists/blob/structures/shield.dm b/code/modules/antagonists/blob/structures/shield.dm
new file mode 100644
index 00000000000..a41efe51c48
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/shield.dm
@@ -0,0 +1,68 @@
+/obj/structure/blob/shield
+ name = "strong blob"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blob_shield"
+ desc = "Сплошная стена слегка подергивающихся щупалец."
+ var/damaged_desc = "Стена дергающихся щупалец."
+ max_integrity = BLOB_STRONG_MAX_HP
+ health_regen = BLOB_STRONG_HP_REGEN
+ brute_resist = BLOB_STRONG_BRUTE_RESIST
+ explosion_block = 3
+ explosion_vertical_block = 2
+ point_return = BLOB_REFUND_STRONG_COST
+ atmosblock = TRUE
+ armor = list("melee" = 0, "bullet" = 0, "laser" = 25, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 90)
+
+/obj/structure/blob/shield/scannerreport()
+ if(atmosblock)
+ return "Will prevent the spread of atmospheric changes."
+ return "N/A"
+
+/obj/structure/blob/shield/core // Automatically generated by the core
+ point_return = 0
+
+/obj/structure/blob/shield/update_name(updates)
+ . = ..()
+ name = "[(compromised_integrity) ? "weakened " : null][initial(name)]"
+
+/obj/structure/blob/shield/update_desc(updates)
+ . = ..()
+ desc = (compromised_integrity) ? "[damaged_desc]" : initial(desc)
+
+/obj/structure/blob/shield/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir)
+ . = ..()
+ if(. && obj_integrity > 0)
+ atmosblock = compromised_integrity
+ air_update_turf(TRUE, atmosblock)
+
+/obj/structure/blob/shield/update_icon_state()
+ icon_state = "[initial(icon_state)][(compromised_integrity) ? "_damaged" : null]"
+ return ..()
+
+/obj/structure/blob/shield/update_state()
+ if(obj_integrity < max_integrity * 0.5)
+ compromised_integrity = TRUE
+ else
+ compromised_integrity = FALSE
+ if(compromised_integrity)
+ atmosblock = FALSE
+ else
+ atmosblock = TRUE
+ air_update_turf(1)
+
+
+/obj/structure/blob/shield/reflective
+ name = "reflective blob"
+ desc = "A solid wall of slightly twitching tendrils with a reflective glow."
+ damaged_desc = "A wall of twitching tendrils with a reflective glow."
+ icon_state = "blob_glow"
+ flags_ricochet = RICOCHET_SHINY
+ point_return = BLOB_REFUND_REFLECTOR_COST
+ explosion_block = 2
+ explosion_vertical_block = 1
+ max_integrity = BLOB_REFLECTOR_MAX_HP
+ health_regen = BLOB_REFLECTOR_HP_REGEN
+ brute_resist = BLOB_BRUTE_RESIST
+
+/obj/structure/blob/shield/reflective/core // Automatically generated by the core
+ point_return = 0
diff --git a/code/modules/antagonists/blob/structures/special.dm b/code/modules/antagonists/blob/structures/special.dm
new file mode 100644
index 00000000000..e325cbd5b20
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/special.dm
@@ -0,0 +1,75 @@
+/obj/structure/blob/special // Generic type for nodes/factories/cores/resource
+ // Core and node vars: claiming, pulsing and expanding
+ /// The radius inside which (previously dead) blob tiles are 'claimed' again by the pulsing overmind. Very rarely used.
+ var/claim_range = 0
+ /// The radius inside which blobs are pulsed by this overmind. Does stuff like expanding, making blob spores from factories, make resources from nodes etc.
+ var/pulse_range = 0
+ /// The radius up to which this special structure naturally grows normal blobs.
+ var/expand_range = 0
+
+ // Area reinforcement vars: used by cores and nodes, for strains to modify
+ /// Range this blob free upgrades to strong blobs at: for the core, and for strains
+ var/strong_reinforce_range = 0
+ /// Range this blob free upgrades to reflector blobs at: for the core, and for strains
+ var/reflector_reinforce_range = 0
+
+/obj/structure/blob/special/proc/reinforce_area(seconds_per_tick) // Used by cores and nodes to upgrade their surroundings
+ if(strong_reinforce_range)
+ if(is_there_multiz())
+ for(var/obj/structure/blob/normal/B in urange_multiz(strong_reinforce_range, src))
+ reinforce_tile(B, /obj/structure/blob/shield/core, seconds_per_tick)
+ else
+ for(var/obj/structure/blob/normal/B in range(strong_reinforce_range, src))
+ reinforce_tile(B, /obj/structure/blob/shield/core, seconds_per_tick)
+
+ if(reflector_reinforce_range)
+ if(is_there_multiz())
+ for(var/obj/structure/blob/shield/B in urange_multiz(reflector_reinforce_range, src))
+ reinforce_tile(B, /obj/structure/blob/shield/reflective/core, seconds_per_tick)
+ else
+ for(var/obj/structure/blob/shield/B in range(reflector_reinforce_range, src))
+ reinforce_tile(B, /obj/structure/blob/shield/reflective/core, seconds_per_tick)
+
+
+/obj/structure/blob/special/proc/reinforce_tile(obj/structure/blob/B, type, seconds_per_tick)
+ if(SPT_PROB(BLOB_REINFORCE_CHANCE, seconds_per_tick))
+ B.change_to(type, overmind, B.point_return)
+
+
+/obj/structure/blob/special/proc/pulse_area(mob/camera/blob/pulsing_overmind, claim_range = 10, pulse_range = 3, expand_range = 2)
+ if(QDELETED(pulsing_overmind))
+ pulsing_overmind = overmind
+ Be_Pulsed()
+ var/expanded = FALSE
+ if(prob(70*(1/BLOB_EXPAND_CHANCE_MULTIPLIER)) && expand())
+ expanded = TRUE
+ var/list/blobs_to_affect = list()
+ if(is_there_multiz())
+ for(var/obj/structure/blob/blob in urange_multiz(claim_range, src, 1))
+ blobs_to_affect += blob
+ else
+ for(var/obj/structure/blob/B in urange(claim_range, src, 1))
+ blobs_to_affect += B
+ shuffle_inplace(blobs_to_affect)
+ for(var/L in blobs_to_affect)
+ var/obj/structure/blob/B = L
+ if(!is_location_within_transition_boundaries(get_turf(B)))
+ continue
+ if(!B.overmind && overmind && prob(30))
+ B.link_to_overmind(pulsing_overmind) //reclaim unclaimed, non-core blobs.
+ B.update_blob()
+ var/distance = get_dist(get_turf(src), get_turf(B))
+ var/expand_probablity = max(20 - distance * 8, 1)
+ if(B.Adjacent(src))
+ expand_probablity = 20
+ if(distance <= expand_range)
+ var/can_expand = TRUE
+ if(blobs_to_affect.len >= 120 && !(COOLDOWN_FINISHED(B, heal_timestamp)))
+ can_expand = FALSE
+ if(can_expand && COOLDOWN_FINISHED(B, pulse_timestamp) && prob(expand_probablity*BLOB_EXPAND_CHANCE_MULTIPLIER))
+ if(!expanded)
+ var/obj/structure/blob/newB = B.expand(null, null, !expanded) //expansion falls off with range but is faster near the blob causing the expansion
+ if(newB)
+ expanded = TRUE
+ if(distance <= pulse_range)
+ B.Be_Pulsed()
diff --git a/code/modules/antagonists/blob/structures/storage.dm b/code/modules/antagonists/blob/structures/storage.dm
new file mode 100644
index 00000000000..83b46236417
--- /dev/null
+++ b/code/modules/antagonists/blob/structures/storage.dm
@@ -0,0 +1,25 @@
+/obj/structure/blob/storage
+ name = "storage blob"
+ icon = 'icons/mob/blob.dmi'
+ icon_state = "blob_resource"
+ desc = "Тонкий шпиль из плотно сплетенных щупалец."
+ max_integrity = BLOB_STORAGE_MAX_HP
+ fire_resist = BLOB_STORAGE_FIRE_RESIST
+ point_return = BLOB_REFUND_STORAGE_COST
+
+/obj/structure/blob/storage/Initialize(mapload, owner_overmind)
+ . = ..()
+ update_max_blob_points(BLOB_STORAGE_MAX_POINTS_BONUS)
+
+/obj/structure/blob/storage/link_to_overmind(mob/camera/blob/owner_overmind)
+ . = ..()
+ update_max_blob_points(BLOB_STORAGE_MAX_POINTS_BONUS)
+
+/obj/structure/blob/storage/obj_destruction(damage_flag)
+ if(overmind)
+ overmind.max_blob_points -= BLOB_STORAGE_MAX_POINTS_BONUS
+ ..()
+
+/obj/structure/blob/storage/proc/update_max_blob_points(new_point_increase)
+ if(overmind)
+ overmind.max_blob_points += new_point_increase
diff --git a/code/modules/antagonists/changeling/powers/revive.dm b/code/modules/antagonists/changeling/powers/revive.dm
index 2ef5f374b81..1d0dabcdb89 100644
--- a/code/modules/antagonists/changeling/powers/revive.dm
+++ b/code/modules/antagonists/changeling/powers/revive.dm
@@ -7,6 +7,9 @@
//Revive from regenerative stasis
/datum/action/changeling/revive/sting_action(mob/living/carbon/user)
+ if(istype(user.loc, /obj/structure/blob/special/core))
+ to_chat(user, span_changeling("Окружающие вас щупальца блоба не дают вам регенерировать"))
+ return FALSE
to_chat(user, span_changeling("We have regenerated."))
diff --git a/code/modules/antagonists/space_ninja/ninja_shuttle.dm b/code/modules/antagonists/space_ninja/ninja_shuttle.dm
index 32a38a7411b..619c97e1d41 100644
--- a/code/modules/antagonists/space_ninja/ninja_shuttle.dm
+++ b/code/modules/antagonists/space_ninja/ninja_shuttle.dm
@@ -31,3 +31,4 @@
name = "\improper Spider Clan \"Ombra\" Shuttle"
parallax_movedir = SOUTH
nad_allowed = TRUE
+ area_flags = NONE
diff --git a/code/modules/awaymissions/mission_code/ruins/ussplaboratory.dm b/code/modules/awaymissions/mission_code/ruins/ussplaboratory.dm
index ab5a4c7d973..bae929942c1 100644
--- a/code/modules/awaymissions/mission_code/ruins/ussplaboratory.dm
+++ b/code/modules/awaymissions/mission_code/ruins/ussplaboratory.dm
@@ -3,6 +3,7 @@
/area/ruin/ussp_xeno
atmosalm = ATMOS_ALARM_NONE
has_gravity = STANDARD_GRAVITY
+ area_flags = NONE
/area/ruin/ussp_xeno/engi
name = "Engineering"
diff --git a/code/modules/mob/dead/dead.dm b/code/modules/mob/dead/dead.dm
index baac7eb2ac8..c6c0b535e1d 100644
--- a/code/modules/mob/dead/dead.dm
+++ b/code/modules/mob/dead/dead.dm
@@ -22,7 +22,7 @@
* updates the Z level for dead players
* If they don't have a new z, we'll keep the old one, preventing bugs from ghosting and re-entering, among others
*/
-/mob/dead/proc/update_z(new_z)
+/mob/dead/update_z(new_z)
if(registered_z == new_z)
return
if(registered_z)
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 469f5cff6cb..128b427e089 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -21,6 +21,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER)
light_system = NO_LIGHT_SUPPORT
invisibility = INVISIBILITY_OBSERVER
pass_flags = PASSEVERYTHING
+ hud_type = /datum/hud/ghost
var/can_reenter_corpse
var/bootime = FALSE
var/started_as_observer //This variable is set to 1 when you enter the game as an observer.
diff --git a/code/modules/mob/living/carbon/alien/alien.dm b/code/modules/mob/living/carbon/alien/alien.dm
index dbfbb236453..d3fd0680bab 100644
--- a/code/modules/mob/living/carbon/alien/alien.dm
+++ b/code/modules/mob/living/carbon/alien/alien.dm
@@ -14,7 +14,12 @@
var/nightvision_enabled = FALSE
nightvision = 4
-
+
+ verb_say = "hisses"
+ verb_ask = "hisses curiously"
+ verb_exclaim = "roars"
+ verb_yell = "roars"
+
var/obj/item/card/id/wear_id = null // Fix for station bounced radios -- Skie
var/has_fine_manipulation = FALSE
var/move_delay_add = 0 // movement delay to add
@@ -97,17 +102,12 @@
return GLOB.all_languages[LANGUAGE_XENOS]
/mob/living/carbon/alien/say_quote(var/message, var/datum/language/speaking = null)
- var/verb = "hisses"
var/ending = copytext(message, length(message))
-
+
if(speaking && (speaking.name != "Galactic Common")) //this is so adminbooze xenos speaking common have their custom verbs,
- verb = speaking.get_spoken_verb(ending) //and use normal verbs for their own languages and non-common languages
+ return speaking.get_spoken_verb(ending) //and use normal verbs for their own languages and non-common languages
else
- if(ending=="!")
- verb = "roars"
- else if(ending=="?")
- verb = "hisses curiously"
- return verb
+ return ..()
/mob/living/carbon/alien/adjustToxLoss(
diff --git a/code/modules/mob/living/carbon/alien/death.dm b/code/modules/mob/living/carbon/alien/death.dm
index 777c180bd57..9d0605f5dde 100644
--- a/code/modules/mob/living/carbon/alien/death.dm
+++ b/code/modules/mob/living/carbon/alien/death.dm
@@ -16,7 +16,7 @@
flick("gibbed-a", animation)
xgibs(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
QDEL_IN(src, 15)
@@ -30,7 +30,7 @@
invisibility = INVISIBILITY_ABSTRACT
dust_animation()
new /obj/effect/decal/remains/xeno(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
@@ -42,7 +42,7 @@
animation.master = src
flick("dust-a", animation)
new /obj/effect/decal/remains/xeno(loc)
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
/mob/living/carbon/alien/death(gibbed)
diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
index 98ecfb25b87..4883f40950a 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
@@ -97,6 +97,7 @@
update_inv_l_hand()
update_inv_pockets()
update_fire()
+ update_wet()
if(blocks_emissive)
add_overlay(get_emissive_block())
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
index 1026d476882..b34ba7137e1 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
@@ -5,6 +5,7 @@
max_grab = GRAB_KILL
slowed_by_pull_and_push = FALSE
butcher_results = list(/obj/item/reagent_containers/food/snacks/monstermeat/xenomeat= 5, /obj/item/stack/sheet/animalhide/xeno = 1)
+ hud_type = /datum/hud/alien
var/obj/item/r_store = null
var/obj/item/l_store = null
var/caste = ""
diff --git a/code/modules/mob/living/carbon/alien/humanoid/queen.dm b/code/modules/mob/living/carbon/alien/humanoid/queen.dm
index c8f34c6e826..c871e951ce2 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/queen.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/queen.dm
@@ -85,6 +85,7 @@
update_inv_l_hand()
update_inv_pockets()
update_fire()
+ update_wet()
if(blocks_emissive)
add_overlay(get_emissive_block())
diff --git a/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm b/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm
index 2ec18501a06..9b3ce1de6f1 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/update_icons.dm
@@ -38,6 +38,7 @@
update_inv_hands()
update_inv_pockets()
update_fire()
+ update_wet()
if(blocks_emissive)
add_overlay(get_emissive_block())
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index 2ff4dc5b667..0e11a27fc79 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -18,6 +18,8 @@
death_message = "с тошнотворным шипением выдыха%(ет,ют)% воздух и пада%(ет,ют)% на пол..."
death_sound = null
+ hud_type = /datum/hud/larva
+
var/datum/action/innate/hide/alien_larva/hide_action
diff --git a/code/modules/mob/living/carbon/brain/MMI.dm b/code/modules/mob/living/carbon/brain/MMI.dm
index 7bf60ec50dc..1b92f72d71c 100644
--- a/code/modules/mob/living/carbon/brain/MMI.dm
+++ b/code/modules/mob/living/carbon/brain/MMI.dm
@@ -186,7 +186,7 @@
brainmob.container = null//Reset brainmob mmi var.
brainmob.forceMove(held_brain) //Throw mob into brain.
GLOB.respawnable_list += brainmob
- GLOB.alive_mob_list -= brainmob//Get outta here
+ brainmob.remove_from_alive_mob_list()//Get outta here
held_brain.brainmob = brainmob//Set the brain to use the brainmob
held_brain.brainmob.cancel_camera()
REMOVE_TRAIT(brainmob, TRAIT_NO_SPELLS, UNIQUE_TRAIT_SOURCE(src))
diff --git a/code/modules/mob/living/carbon/brain/robotic_brain.dm b/code/modules/mob/living/carbon/brain/robotic_brain.dm
index 6a5b1baf56e..a3f7db308b6 100644
--- a/code/modules/mob/living/carbon/brain/robotic_brain.dm
+++ b/code/modules/mob/living/carbon/brain/robotic_brain.dm
@@ -265,7 +265,7 @@
brainmob.dna.species = new /datum/species/machine() // Else it will default to human. And we don't want to clone IRC humans now do we?
brainmob.dna.ResetSE()
brainmob.dna.ResetUI()
- GLOB.dead_mob_list -= brainmob
+ brainmob.remove_from_dead_mob_list()
..()
/obj/item/mmi/robotic_brain/attack_ghost(mob/dead/observer/O)
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index b0d89a6afa2..f7e356b1b25 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -28,8 +28,8 @@
if(stat == DEAD)
return
else
- show_message("Блоб атакует!")
- adjustBruteLoss(10)
+ show_message(span_userdanger("Блоб атакует!"))
+ adjustBruteLoss(10)
/mob/living/carbon/Move(atom/newloc, direct = NONE, glide_size_override = 0, update_dir = TRUE)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 63f34de85cc..8fe2746464a 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -335,7 +335,7 @@
if(stat == DEAD)
return
SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, B)
- show_message("The blob attacks you!")
+ show_message(span_userdanger("The blob attacks you!"))
var/dam_zone = list(
BODY_ZONE_CHEST,
BODY_ZONE_PRECISE_GROIN,
@@ -350,8 +350,7 @@
BODY_ZONE_PRECISE_R_FOOT,
)
var/obj/item/organ/external/affecting = get_organ(ran_zone(dam_zone))
- apply_damage(5, BRUTE, affecting, run_armor_check(affecting, "melee"))
-
+ apply_damage(5, BRUTE, affecting, run_armor_check(affecting, MELEE))
// Get rank from ID from hands, wear_id, pda, and then from uniform
/mob/living/carbon/human/proc/get_authentification_rank(var/if_no_id = "No id", var/if_no_job = "No job")
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index b4059f4a60f..040145b9b64 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -15,6 +15,7 @@
num_hands = 0 //Populated on init through list/bodyparts
usable_hands = 0 //Populated on init through list/bodyparts
status_flags = parent_type::status_flags|CANSTAMCRIT
+ hud_type = /datum/hud/human
//Marking colour and style
var/list/m_colours = DEFAULT_MARKING_COLOURS //All colours set to #000000.
var/list/m_styles = DEFAULT_MARKING_STYLES //All markings set to None.
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index 3f08ff83a94..c7c062c702e 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -767,7 +767,8 @@
if(dna.species.update_health_hud())
return
else
-
+ if(SEND_SIGNAL(src, COMSIG_HUMAN_UPDATING_HEALTH_HUD, health) & COMPONENT_OVERRIDE_HEALTH_HUD)
+ return
var/shock_reduction = 0
if(HAS_TRAIT(src, TRAIT_NO_PAIN_HUD))
shock_reduction = INFINITY
diff --git a/code/modules/mob/living/carbon/human/update_icons.dm b/code/modules/mob/living/carbon/human/update_icons.dm
index 17ab8f338e9..05a2af564e0 100644
--- a/code/modules/mob/living/carbon/human/update_icons.dm
+++ b/code/modules/mob/living/carbon/human/update_icons.dm
@@ -518,6 +518,7 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
add_overlay(get_emissive_block())
update_halo_layer()
update_fire()
+ update_wet()
update_ssd_overlay()
update_unconscious_overlay()
SEND_SIGNAL(src, COMSIG_HUMAN_REGENERATE_ICONS)
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 3d8ab5a1387..cf013ce2c08 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -283,6 +283,8 @@
if(healths)
if(stat != DEAD)
. = TRUE
+ if(SEND_SIGNAL(src, COMSIG_CARBON_UPDATING_HEALTH_HUD, shown_health_amount) & COMPONENT_OVERRIDE_HEALTH_HUD)
+ return
if(shown_health_amount == null)
shown_health_amount = health
if(shown_health_amount >= maxHealth)
diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm
index 765ff34e0f8..51d15c7ad56 100644
--- a/code/modules/mob/living/life.dm
+++ b/code/modules/mob/living/life.dm
@@ -4,15 +4,7 @@
SEND_SIGNAL(src, COMSIG_LIVING_LIFE, seconds, times_fired)
- if(client || registered_z) // This is a temporary error tracker to make sure we've caught everything
- var/turf/T = get_turf(src)
- if(client && registered_z != T.z)
- message_admins("[src] [ADMIN_FLW(src, "FLW")] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z]. If you could ask them how that happened and notify the coders, it would be appreciated.")
- add_misc_logs(src, "Z-TRACKING: [src] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z].")
- update_z(T.z)
- else if (!client && registered_z)
- add_misc_logs(src, "Z-TRACKING: [src] of type [src.type] has a Z-registration despite not having a client.")
- update_z(null)
+ track_z()
if(HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return FALSE
@@ -53,6 +45,7 @@
handle_environment(environment)
handle_fire()
+ handle_wet()
var/datum/antagonist/vampire/vamp = mind?.has_antag_datum(/datum/antagonist/vampire)
if(vamp)
@@ -229,13 +222,13 @@
severity = 6
livingdoll.icon_state = "living[severity]"
if(!livingdoll.filtered)
- livingdoll.filtered = TRUE
var/icon/mob_mask = icon(icon, icon_state)
if(mob_mask.Height() > world.icon_size || mob_mask.Width() > world.icon_size)
var/health_doll_icon_state = health_doll_icon ? health_doll_icon : "megasprite"
mob_mask = icon('icons/mob/screen_gen.dmi', health_doll_icon_state) //swap to something generic if they have no special doll
livingdoll.add_filter("mob_shape_mask", 1, alpha_mask_filter(icon = mob_mask))
livingdoll.add_filter("inset_drop_shadow", 2, drop_shadow_filter(size = -1))
+ livingdoll.filtered = TRUE
if(severity > 0)
overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
else
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index f966df04963..792360172b2 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -7,6 +7,8 @@
faction += "\ref[src]"
determine_move_and_pull_forces()
gravity_setup()
+ if(unique_name)
+ set_name()
if(ventcrawler_trait)
var/static/list/ventcrawler_sanity = list(
TRAIT_VENTCRAWLER_ALWAYS,
@@ -756,7 +758,9 @@
ExtinguishMob()
CureAllDiseases(FALSE)
fire_stacks = 0
+ fire_stacks = 0
on_fire = 0
+ is_wet = FALSE
suiciding = 0
if(buckled) //Unbuckle the mob and clear the alerts.
buckled.unbuckle_mob(src, force = TRUE)
@@ -788,6 +792,7 @@
get_up(instant = TRUE)
update_fire()
+ update_wet()
regenerate_icons()
restore_blood()
if(human_mob)
@@ -1627,35 +1632,6 @@
target.devoured(grabber)
-/mob/living/proc/update_z(new_z) // 1+ to register, null to unregister
- if(registered_z == new_z)
- return
- if(registered_z)
- SSmobs.clients_by_zlevel[registered_z] -= src
- if(isnull(client))
- registered_z = null
- return
- if(!new_z)
- registered_z = new_z
- return
- //Figure out how many clients were here before
- var/oldlen = SSmobs.clients_by_zlevel[new_z].len
- SSmobs.clients_by_zlevel[new_z] += src
- for(var/index in length(SSidlenpcpool.idle_mobs_by_zlevel[new_z]) to 1 step -1) //Backwards loop because we're removing (guarantees optimal rather than worst-case performance), it's fine to use .len here but doesn't compile on 511
- var/mob/living/simple_animal/animal = SSidlenpcpool.idle_mobs_by_zlevel[new_z][index]
- if(animal)
- if(!oldlen)
- //Start AI idle if nobody else was on this z level before (mobs will switch off when this is the case)
- animal.toggle_ai(AI_IDLE)
- //If they are also within a close distance ask the AI if it wants to wake up
- if(get_dist(get_turf(src), get_turf(animal)) < MAX_SIMPLEMOB_WAKEUP_RANGE)
- animal.consider_wakeup() // Ask the mob if it wants to turn on it's AI
- //They should clean up in destroy, but often don't so we get them here
- else
- SSidlenpcpool.idle_mobs_by_zlevel[new_z] -= animal
- registered_z = new_z
-
-
/mob/living/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents = TRUE)
..()
update_z(new_turf?.z)
@@ -1838,6 +1814,9 @@
return TRUE
return FALSE
+/mob/living/examine(mob/user, infix, suffix)
+ . = ..()
+ SEND_SIGNAL(src, COMSIG_LIVING_EXAMINE, user, .)
/**
* Sets the mob's direction lock towards a given atom.
@@ -2165,8 +2144,8 @@
update_blind_effects()
update_blurry_effects()
update_unconscious_overlay()
- GLOB.alive_mob_list += src
- GLOB.dead_mob_list -= src
+ add_to_alive_mob_list()
+ remove_from_dead_mob_list()
switch(stat) //Current stat.
if(CONSCIOUS)
@@ -2179,8 +2158,8 @@
SetLoseBreath(0)
SetDisgust(0)
SetEyeBlurry(0)
- GLOB.alive_mob_list -= src
- GLOB.dead_mob_list += src
+ remove_from_alive_mob_list()
+ add_to_dead_mob_list()
/// Updates hands HUD element.
@@ -2322,3 +2301,9 @@
. |= RECHARGE_SUCCESSFUL
to_chat(src, span_notice("You feel [(. & RECHARGE_SUCCESSFUL) ? "raw magical energy flowing through you, it feels good!" : "very strange for a moment, but then it passes."]"))
+
+/mob/living/proc/set_name()
+ if(numba == 0)
+ numba = rand(1, 1000)
+ name = "[name] ([numba])"
+ real_name = name
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 26a49da0a8e..60f9252cc82 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -85,6 +85,13 @@
)
return shock_damage
+/mob/living/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ . = ..()
+ if(HAS_TRAIT(src, TRAIT_BLOB_ZOMBIFIED) || QDELETED(src))
+ return FALSE
+ if(stat == DEAD)
+ forceMove(voring_core)
+
/mob/living/emp_act(severity)
..()
@@ -185,8 +192,8 @@
/mob/living/proc/IgniteMob()
if(fire_stacks > 0 && !on_fire)
on_fire = TRUE
- visible_message("[src.declent_ru(NOMINATIVE)] загора[pluralize_ru(src.gender,"ется","ются")]!", \
- "[pluralize_ru(src.gender,"Ты загораешься","Вы загораетесь")]!")
+ visible_message(span_warning("[src.declent_ru(NOMINATIVE)] загора[pluralize_ru(src.gender,"ется","ются")]!"), \
+ span_userdanger("[pluralize_ru(src.gender,"Ты загораешься","Вы загораетесь")]!"))
set_light_range(light_range + 3)
set_light_color("#ED9200")
throw_alert("fire", /atom/movable/screen/alert/fire)
@@ -212,6 +219,8 @@
/mob/living/proc/adjust_fire_stacks(add_fire_stacks) //Adjusting the amount of fire_stacks we have on person
SEND_SIGNAL(src, COMSIG_MOB_ADJUST_FIRE)
fire_stacks = clamp(fire_stacks + add_fire_stacks, -20, 20)
+ if(wet_stacks)
+ combine_wet_and_fire()
if(on_fire && fire_stacks <= 0)
ExtinguishMob()
@@ -239,6 +248,65 @@
SEND_SIGNAL(src, COMSIG_LIVING_FIRE_TICK)
return TRUE
+/mob/living/proc/update_wet()
+ if(is_wet)
+ if(wet_effect)
+ return
+ wet_effect = new(src, /particles/droplets)
+ else
+ qdel(wet_effect)
+ wet_effect = null
+
+
+/mob/living/proc/adjust_wet_stacks(add_wet_stacks) //Adjusting the amount of fire_stacks we have on person
+ if(wet_immunity)
+ return
+ SEND_SIGNAL(src, COMSIG_MOB_ADJUST_WET)
+ wet_stacks = clamp(wet_stacks + add_wet_stacks, -20, 20)
+ if(fire_stacks)
+ combine_wet_and_fire()
+ if(is_wet && wet_stacks <= 0)
+ DryMob()
+
+/mob/living/proc/combine_wet_and_fire()
+ var/buf_stacks = wet_stacks
+ wet_stacks = clamp(buf_stacks - fire_stacks, 0, 20)
+ fire_stacks = clamp(fire_stacks - buf_stacks, 0, 20)
+
+/mob/living/proc/WetMob()
+ if(!wet_immunity && wet_stacks > 0 && !is_wet )
+ is_wet = TRUE
+ visible_message(span_warning("[src.declent_ru(NOMINATIVE)] намока[pluralize_ru(src.gender,"ет","ют")]!"), \
+ span_userdanger("[pluralize_ru(src.gender,"Ты намокаешь","Вы намокаете")]!"))
+ AddComponent(/datum/component/slippery, 5 SECONDS)
+ update_wet()
+ SEND_SIGNAL(src, COMSIG_LIVING_WET)
+ return TRUE
+ return FALSE
+
+/mob/living/proc/DryMob()
+ if(is_wet)
+ qdel(GetComponent(/datum/component/slippery))
+ is_wet = FALSE
+ wet_stacks = 0
+ update_wet()
+
+/**
+ * Burns a mob and slowly puts the fires out. Returns TRUE if the mob is on fire
+ */
+/mob/living/proc/handle_wet()
+ if(wet_stacks < 0) //If we've doused ourselves in water to avoid wet, dry off slowly
+ wet_stacks = min(0, wet_stacks + 1)//So we dry ourselves back to default, nonflammable.
+ if(!is_wet)
+ return FALSE
+ if(wet_stacks > 0)
+ adjust_wet_stacks(-0.1) //the wet is slowly consumed
+ else
+ DryMob()
+ return FALSE
+ SEND_SIGNAL(src, COMSIG_LIVING_WET_TICK)
+ return TRUE
+
/mob/living/fire_act(datum/gas_mixture/air, exposed_temperature, exposed_volume, global_overlay = TRUE)
..()
adjust_fire_stacks(3)
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index 0ee33dffb53..79fc76328c7 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -38,6 +38,13 @@
var/on_fire = 0 //The "Are we on fire?" var
var/fire_stacks = 0 //Tracks how many stacks of fire we have on, max is usually 20
+ /// The "Are we wet?" var
+ var/is_wet = 0
+ /// Tracks how many stacks of wet we have on, max is usually 20
+ var/wet_stacks = 0
+ var/wet_immunity = FALSE
+
+
var/mob_size = MOB_SIZE_HUMAN
var/metabolism_efficiency = 1 //more or less efficiency to metabolize helpful/harmful reagents and regulate body temperature..
var/digestion_ratio = 1 //controls how quickly reagents metabolize; largely governered by species attributes.
@@ -66,6 +73,9 @@
var/gene_stability = DEFAULT_GENE_STABILITY
var/ignore_gene_stability = 0
+ /// the id a mob gets when it's created
+ var/numba = 0
+ var/unique_name = FALSE
/// A log of what we've said, plain text, no spans or junk, essentially just each individual "message"
var/list/say_log
@@ -141,3 +151,5 @@
/// Famous last words -- if succumbing, what the user's last words were
var/last_words
+
+ var/obj/effect/abstract/particle_holder/wet_effect
diff --git a/code/modules/mob/living/living_infected_blob_mobs.dm b/code/modules/mob/living/living_infected_blob_mobs.dm
index 65970863476..b86bf2aac47 100644
--- a/code/modules/mob/living/living_infected_blob_mobs.dm
+++ b/code/modules/mob/living/living_infected_blob_mobs.dm
@@ -101,6 +101,10 @@
return FALSE
+/mob/living/simple_animal/hostile/illusion/can_be_blob()
+ return FALSE
+
+
/mob/living/simple_animal/hostile/asteroid/can_be_blob()
return FALSE
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index be1a0129fd8..1600c935e10 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -205,6 +205,10 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key)
if(check_mute(client.ckey, MUTE_IC))
to_chat(src, span_danger("You cannot speak in IC (Muted)."))
return FALSE
+
+ var/sigreturn = SEND_SIGNAL(src, COMSIG_MOB_TRY_SPEECH, message)
+ if(sigreturn & COMPONENT_CANNOT_SPEAK)
+ return FALSE
if(sanitize)
message = trim_strip_html_properly(message, 512)
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 74bb56e8bd9..c2f2b0ed6cd 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -47,6 +47,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
sight = SEE_TURFS | SEE_MOBS | SEE_OBJS
nightvision = 8
can_buckle_to = FALSE
+ hud_type = /datum/hud/ai
var/list/network = list("SS13","Telecomms","Research Outpost","Mining Outpost")
var/obj/machinery/camera/current = null
var/list/connected_robots = list()
@@ -659,9 +660,8 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
/mob/living/silicon/ai/blob_act(obj/structure/blob/B)
if(stat != DEAD)
adjustBruteLoss(60)
- return 1
- return 0
-
+ return TRUE
+ return TRUE
/mob/living/silicon/ai/emp_act(severity)
..()
@@ -1351,7 +1351,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list(
return TRUE
-/mob/living/silicon/ai/proc/can_see(atom/A)
+/mob/living/silicon/ai/can_see(atom/A)
if(isturf(loc)) //AI in core, check if on cameras
//get_turf_pixel() is because APCs in maint aren't actually in view of the inner camera
//apc_override is needed here because AIs use their own APC when depowered
diff --git a/code/modules/mob/living/silicon/death.dm b/code/modules/mob/living/silicon/death.dm
index e3383f980b5..df12411bfd4 100644
--- a/code/modules/mob/living/silicon/death.dm
+++ b/code/modules/mob/living/silicon/death.dm
@@ -16,7 +16,7 @@
drop_hat()
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
spawn(15)
if(animation) qdel(animation)
if(src) qdel(src)
@@ -28,7 +28,7 @@
icon = null
invisibility = INVISIBILITY_ABSTRACT
dust_animation()
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index 709c20446e5..8efe5264e71 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -230,8 +230,8 @@
/mob/living/silicon/pai/blob_act()
if(stat != DEAD)
adjustBruteLoss(60)
- return 1
- return 0
+ return TRUE
+ return FALSE
/mob/living/silicon/pai/emp_act(severity)
diff --git a/code/modules/mob/living/silicon/robot/death.dm b/code/modules/mob/living/silicon/robot/death.dm
index b58059c48a3..3ce665ce098 100644
--- a/code/modules/mob/living/silicon/robot/death.dm
+++ b/code/modules/mob/living/silicon/robot/death.dm
@@ -22,8 +22,8 @@
drop_hat()
- GLOB.alive_mob_list -= src
- GLOB.dead_mob_list -= src
+ remove_from_alive_mob_list()
+ remove_from_dead_mob_list()
QDEL_IN(animation, 15)
QDEL_IN(src, 15)
return TRUE
@@ -36,7 +36,7 @@
invisibility = INVISIBILITY_ABSTRACT
if(mmi)
qdel(mmi) //Delete the MMI first so that it won't go popping out.
- GLOB.dead_mob_list -= src
+ remove_from_dead_mob_list()
QDEL_IN(src, 15)
return TRUE
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index dbe4d67f6b1..9083e45418b 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -110,6 +110,7 @@ GLOBAL_LIST_INIT(robot_verbs_default, list(
var/updating = 0 //portable camera camerachunk update
hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_BATT_HUD)
+ hud_type = /datum/hud/robot
var/default_cell_type = /obj/item/stock_parts/cell/high
///Jetpack-like effect.
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 06714b5a316..0c76e13fbc4 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -4,6 +4,7 @@
bubble_icon = "machine"
has_unlimited_silicon_privilege = 1
weather_immunities = list(TRAIT_WEATHER_IMMUNE)
+ wet_immunity = TRUE
var/syndicate = 0
var/obj/item/gps/cyborg/gps
var/const/MAIN_CHANNEL = "Main Frequency"
diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm
index 70bdf1172f2..2c7a527d58f 100644
--- a/code/modules/mob/living/simple_animal/animal_defense.dm
+++ b/code/modules/mob/living/simple_animal/animal_defense.dm
@@ -156,7 +156,10 @@
adjustBruteLoss(bloss)
/mob/living/simple_animal/blob_act(obj/structure/blob/B)
- adjustBruteLoss(20)
+ var/result = ..()
+ if(result)
+ adjustBruteLoss(20)
+ return result
/mob/living/simple_animal/do_attack_animation(atom/A, visual_effect_icon, used_item, no_effect)
if(!no_effect && !visual_effect_icon && melee_damage_upper)
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index d5f7031309d..7d72a880884 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -24,6 +24,10 @@
light_system = MOVABLE_LIGHT
+ hud_type = /datum/hud/bot
+
+ wet_immunity = TRUE
+
var/obj/machinery/bot_core/bot_core = null
var/bot_core_type = /obj/machinery/bot_core
var/list/users = list() //for dialog updates
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index 3f4e0f0ccac..089dc0ef118 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -127,6 +127,7 @@
force_threshold = 11
playstyle_string = "You are a Juggernaut. Though slow, your shell can withstand extreme punishment, \
create shield walls, rip apart enemies and walls alike, and even deflect energy weapons."
+ hud_type = /datum/hud/construct/armoured
/mob/living/simple_animal/hostile/construct/armoured/hostile //actually hostile, will move around, hit things
AIStatus = AI_ON
@@ -177,6 +178,7 @@
retreat_distance = 2 //AI wraiths will move in and out of combat
playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, and even able to phase through walls."
tts_seed = "Kelthuzad"
+ hud_type = /datum/hud/construct/wraith
/mob/living/simple_animal/hostile/construct/wraith/hostile //actually hostile, will move around, hit things
AIStatus = AI_ON
@@ -228,6 +230,7 @@
use magic missile, repair allied constructs (by clicking on them), \
and, most important of all, create new constructs by producing soulstones to capture souls, \
and shells to place those soulstones into."
+ hud_type = /datum/hud/construct/builder
/mob/living/simple_animal/hostile/construct/builder/Found(atom/A) //what have we found here?
@@ -309,6 +312,7 @@
attack_sound = 'sound/weapons/punch4.ogg'
force_threshold = 11
construct_type = "behemoth"
+ hud_type = /datum/hud/construct/armoured
var/energy = 0
var/max_energy = 1000
@@ -341,6 +345,7 @@
retreat_distance = 2 //AI harvesters will move in and out of combat, like wraiths, but shittier
playstyle_string = "You are a Harvester. You are not strong, but your powers of domination will assist you in your role: \
Bring those who still cling to this world of illusion back to the master so they may know Truth."
+ hud_type = /datum/hud/construct/harvester
/mob/living/simple_animal/hostile/construct/harvester/Process_Spacemove(movement_dir = NONE, continuous_move = FALSE)
diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm
index 1b3978b8299..84c6470ba4e 100644
--- a/code/modules/mob/living/simple_animal/friendly/dog.dm
+++ b/code/modules/mob/living/simple_animal/friendly/dog.dm
@@ -25,6 +25,7 @@
turns_per_move = 10
mob_size = MOB_SIZE_SMALL
gold_core_spawnable = FRIENDLY_SPAWN
+ hud_type = /datum/hud/corgi
var/bark_sound = list('sound/creatures/dog_bark1.ogg','sound/creatures/dog_bark2.ogg') //Used in emote.
var/bark_emote = list("ла%(ет,ют)%.", "гавка%(ет,ют)%.") // used in emote.
var/growl_sound = list('sound/creatures/dog_grawl1.ogg','sound/creatures/dog_grawl2.ogg') //Used in emote.
diff --git a/code/modules/mob/living/simple_animal/friendly/slugcat.dm b/code/modules/mob/living/simple_animal/friendly/slugcat.dm
index 3f51d2c7fd3..8e770fbf9c0 100644
--- a/code/modules/mob/living/simple_animal/friendly/slugcat.dm
+++ b/code/modules/mob/living/simple_animal/friendly/slugcat.dm
@@ -136,6 +136,7 @@
add_overlay(head_icon)
update_fire()
+ update_wet()
if(blocks_emissive)
add_overlay(get_emissive_block())
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
index 55d751ee2b4..5a6d250ae59 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -41,6 +41,7 @@
var/busy = 0
footstep_type = FOOTSTEP_MOB_CLAW
AI_delay_max = 0.5 SECONDS
+ hud_type = /datum/hud/simple_animal/spider
/mob/living/simple_animal/hostile/poison/giant_spider/ComponentInitialize()
AddComponent( \
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 32908854ec8..f25826fb2bd 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -171,16 +171,39 @@
if(!search_objects)
. = hearers(vision_range, targets_from) - src //Remove self, so we don't suicide
- var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/spacepod))
-
- for(var/HM in typecache_filter_list(range(vision_range, targets_from), hostile_machines))
- if(can_see(targets_from, HM, vision_range))
+ var/static/possible_targets = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/spacepod, /mob/living))
+ for(var/HM in typecache_filter_list(range(vision_range, targets_from), possible_targets))
+ if(targets_from.can_see(HM, vision_range))
. += HM
else
. = oview(vision_range, targets_from)
if(retaliate_only)
return . &= enemies // Remove all entries that aren't in enemies
+/mob/living/simple_animal/hostile/can_see(atom/target, length)
+ if(!target || target.invisibility > see_invisible)
+ return FALSE
+ var/turf/current_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
+ if(!current_turf || !target_turf) // nullspace
+ return FALSE
+ if(get_dist(current_turf, target_turf) > length)
+ return FALSE
+ if(current_turf == target_turf)//they are on the same turf, source can see the target
+ return TRUE
+ if(isliving(target) && (sight & SEE_MOBS))//if a mob sees mobs through walls, it always sees the target mob within line of sight
+ return TRUE
+ var/steps = 1
+ current_turf = get_step_towards(current_turf, target_turf)
+ while(current_turf != target_turf)
+ if(steps > length)
+ return FALSE
+ if(IS_OPAQUE_TURF(current_turf))
+ return FALSE
+ current_turf = get_step_towards(current_turf, target_turf)
+ steps++
+ return TRUE
+
/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets)//Step 2, filter down possible targets to things we actually care about
if(QDELETED(src))
@@ -316,7 +339,7 @@
if(L in friends)
return FALSE
else
- if((faction_check && !attack_same) || L.stat)
+ if((faction_check && !attack_same) || L.stat > stat_attack)
return FALSE
return TRUE
@@ -463,7 +486,9 @@
SEND_SIGNAL(src, COMSIG_HOSTILE_ATTACKINGTARGET, target)
if(!client)
mob_attack_logs += "[time_stamp()] Attacked [target] at [COORD(src)]"
- return target.attack_animal(src)
+ var/result = target.attack_animal(src)
+ SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result)
+ return result
/mob/living/simple_animal/hostile/proc/Aggro()
diff --git a/code/modules/mob/living/simple_animal/hostile/illusion.dm b/code/modules/mob/living/simple_animal/hostile/illusion.dm
index d3437761665..fdfdbf8bae4 100644
--- a/code/modules/mob/living/simple_animal/hostile/illusion.dm
+++ b/code/modules/mob/living/simple_animal/hostile/illusion.dm
@@ -18,6 +18,7 @@
var/multiply_chance = 0 //if we multiply on hit
deathmessage = "vanishes into thin air! It was a fake!"
del_on_death = 1
+ wet_immunity = TRUE
/mob/living/simple_animal/hostile/illusion/Life()
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
index 833ef4de31a..71c05d106ec 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/pet.dm
@@ -20,4 +20,5 @@
unique_pet = TRUE
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 2, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
gender = FEMALE
+ hud_type = /datum/hud/simple_animal/spider
diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
index 593dbc8434b..9f5dda6e667 100644
--- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
+++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/terror_spiders.dm
@@ -88,6 +88,9 @@ GLOBAL_LIST_EMPTY(ts_spiderling_list)
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
sight = SEE_TURFS|SEE_MOBS|SEE_OBJS
+ // HUD
+ hud_type = /datum/hud/simple_animal/spider
+
// AI aggression settings
var/ai_target_method = TS_DAMAGE_SIMPLE
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 49f45d9499b..eefbe9bc914 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -9,6 +9,8 @@
universal_speak = 0
status_flags = CANPUSH
+ hud_type = /datum/hud/simple_animal
+
var/icon_living = ""
var/icon_dead = ""
var/icon_resting = ""
@@ -369,12 +371,9 @@
/mob/living/simple_animal/say_quote(message)
- var/verb = "says"
-
- if(speak_emote.len)
- verb = pick(speak_emote)
-
- return verb
+ if(speak_emote?.len)
+ return get_verb(speak_emote)
+ return ..()
/mob/living/simple_animal/proc/set_varspeed(var_value)
@@ -690,6 +689,7 @@
add_overlay("[collar_type]tag")
update_fire()
+ update_wet()
if(blocks_emissive)
add_overlay(get_emissive_block())
@@ -697,6 +697,11 @@
/mob/living/simple_animal/Login()
..()
SSmove_manager.stop_looping(src) // if mob is moving under ai control, then stop AI movement
+ toggle_ai(AI_OFF)
+
+/mob/living/simple_animal/Logout()
+ . = ..()
+ toggle_ai(AI_ON)
/mob/living/simple_animal/say(message, verb = "says", sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE)
diff --git a/code/modules/mob/living/simple_animal/slime/say.dm b/code/modules/mob/living/simple_animal/slime/say.dm
index 8679d64a699..5c3ccd7558d 100644
--- a/code/modules/mob/living/simple_animal/slime/say.dm
+++ b/code/modules/mob/living/simple_animal/slime/say.dm
@@ -1,14 +1,3 @@
-/mob/living/simple_animal/slime/say_quote(text, datum/language/speaking)
- var/verb = "blorbles"
- var/ending = copytext(text, length(text))
-
- if(ending == "?")
- verb = "inquisitively blorbles"
- else if(ending == "!")
- verb = "loudly blorbles"
-
- return verb
-
/mob/living/simple_animal/slime/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
if(speaker != src && !stat)
if(speaker in Friends)
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index 6b51b2320a1..997dc6e86ad 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -20,7 +20,10 @@
response_disarm = "shoos"
response_harm = "stomps on"
emote_see = list("jiggles", "bounces in place")
- speak_emote = list("blorbles")
+ verb_say = "blorbles"
+ verb_ask = "inquisitively blorbles"
+ verb_exclaim = "loudly blorbles"
+ verb_yell = "loudly blorbles"
bubble_icon = "slime"
tts_seed = "Chen"
@@ -40,6 +43,8 @@
footstep_type = FOOTSTEP_MOB_SLIME
+ hud_type = /datum/hud/slime
+
var/cores = 1 // the number of /obj/item/slime_extract's the slime has left inside
var/mutation_chance = 30 // Chance of mutating, should be between 25 and 35
var/chance_reproduce = 80
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index d4134aae515..0e9bd6c5f23 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -29,8 +29,7 @@
if(!client)
return FALSE
canon_client = client
- GLOB.player_list |= src
- GLOB.keyloop_list |= src
+ add_to_player_list()
last_known_ckey = ckey
update_Login_details()
world.update_status()
diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm
index 44d97c84b3a..4ad2bd7fd1f 100644
--- a/code/modules/mob/logout.dm
+++ b/code/modules/mob/logout.dm
@@ -3,8 +3,7 @@
set_typing_indicator(FALSE)
SStgui.on_logout(src) // Cleanup any TGUIs the user has open
unset_machine()
- GLOB.player_list -= src
- GLOB.keyloop_list -= src
+ remove_from_player_list()
log_access_out(src)
add_game_logs("OWNERSHIP: [key_name(src)] is no longer owning mob [src]([src.type])")
// `holder` is nil'd out by now, so we check the `admin_datums` array directly
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index d1020156e82..2133ffa1ab2 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1,7 +1,7 @@
/mob/Destroy()//This makes sure that mobs with clients/keys are not just deleted from the game.
- GLOB.mob_list -= src
- GLOB.dead_mob_list -= src
- GLOB.alive_mob_list -= src
+ remove_from_mob_list()
+ remove_from_alive_mob_list()
+ remove_from_dead_mob_list()
focus = null
QDEL_NULL(hud_used)
if(mind && mind.current == src)
@@ -25,11 +25,11 @@
return ..()
/mob/Initialize(mapload)
- GLOB.mob_list += src
+ add_to_mob_list()
if(stat == DEAD)
- GLOB.dead_mob_list += src
+ add_to_dead_mob_list()
else
- GLOB.alive_mob_list += src
+ add_to_alive_mob_list()
set_focus(src)
prepare_huds()
. = ..()
@@ -591,8 +591,9 @@
/mob/proc/get_status_tab_items()
SHOULD_CALL_PARENT(TRUE)
- var/list/status_tab_data = list()
- return status_tab_data
+ . = list()
+ SEND_SIGNAL(src, COMSIG_MOB_GET_STATUS_TAB_ITEMS, .)
+ return .
// facing verbs
/mob/proc/canface()
@@ -638,7 +639,7 @@
return FALSE
/mob/proc/can_use_machinery(obj/machinery/mach)
- return IsAdvancedToolUser()
+ return IsAdvancedToolUser()
/mob/proc/swap_hand()
return
@@ -673,7 +674,7 @@
var/mob/living/picked = tgui_input_list(usr, "Please select an NPC to respawn as", "Respawn as NPC", allowed_creatures)
if(!picked)
return
-
+
if(picked == "Mouse")
become_mouse()
return
@@ -684,7 +685,7 @@
if(QDELETED(picked_mob) || picked_mob.key || picked_mob.stat == DEAD)
to_chat(usr, span_warning("[capitalize(picked_mob)] is no longer available to respawn!"))
return
-
+
if(istype(picked_mob, /mob/living/simple_animal/borer))
var/mob/living/simple_animal/borer/borer = picked_mob
borer.transfer_personality(usr.client)
@@ -693,7 +694,7 @@
to_chat(usr, span_notify(message))
GLOB.respawnable_list -= usr
picked_mob.key = key
-
+
/mob/proc/become_mouse()
var/timedifference = world.time - client.time_joined_as_mouse
@@ -1161,3 +1162,43 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list(
if(update)
update_actionspeed()
+/mob/proc/update_z(new_z) // 1+ to register, null to unregister
+ if(registered_z == new_z)
+ return
+ if(registered_z)
+ SSmobs.clients_by_zlevel[registered_z] -= src
+ if(isnull(client))
+ registered_z = null
+ return
+ if(!new_z)
+ registered_z = new_z
+ return
+ //Figure out how many clients were here before
+ var/oldlen = SSmobs.clients_by_zlevel[new_z].len
+ SSmobs.clients_by_zlevel[new_z] += src
+ for(var/index in length(SSidlenpcpool.idle_mobs_by_zlevel[new_z]) to 1 step -1) //Backwards loop because we're removing (guarantees optimal rather than worst-case performance), it's fine to use .len here but doesn't compile on 511
+ var/mob/living/simple_animal/animal = SSidlenpcpool.idle_mobs_by_zlevel[new_z][index]
+ if(animal)
+ if(!oldlen)
+ //Start AI idle if nobody else was on this z level before (mobs will switch off when this is the case)
+ animal.toggle_ai(AI_IDLE)
+ //If they are also within a close distance ask the AI if it wants to wake up
+ if(get_dist(get_turf(src), get_turf(animal)) < MAX_SIMPLEMOB_WAKEUP_RANGE)
+ animal.consider_wakeup() // Ask the mob if it wants to turn on it's AI
+ //They should clean up in destroy, but often don't so we get them here
+ else
+ SSidlenpcpool.idle_mobs_by_zlevel[new_z] -= animal
+ registered_z = new_z
+
+/mob/proc/track_z()
+ if(client || registered_z) // This is a temporary error tracker to make sure we've caught everything
+ var/turf/T = get_turf(src)
+ if(client && registered_z != T.z)
+ message_admins("[src] [ADMIN_FLW(src, "FLW")] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z]. If you could ask them how that happened and notify the coders, it would be appreciated.")
+ add_misc_logs(src, "Z-TRACKING: [src] has somehow ended up in Z-level [T.z] despite being registered in Z-level [registered_z].")
+ update_z(T.z)
+ else if(!client && registered_z)
+ add_misc_logs(src, "Z-TRACKING: [src] of type [src.type] has a Z-registration despite not having a client.")
+ update_z(null)
+
+
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index a76ad7796fa..66ecf8a68a2 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -93,7 +93,11 @@
var/list/datum/language/languages
/// For reagents that grant language knowlege.
var/list/temporary_languages
- var/list/speak_emote = list("says") // Verbs used when speaking. Defaults to 'say' if speak_emote is null.
+ var/list/speak_emote = list() // Verbs used when speaking. Defaults to 'say' if speak_emote is null.
+ var/verb_say = "says"
+ var/verb_ask = "asks"
+ var/verb_exclaim = list("exclaims", "shouts")
+ var/verb_yell = "yells"
/// Define emote default type, EMOTE_VISIBLE for seen emotes, EMOTE_AUDIBLE for heard emotes.
var/emote_type = EMOTE_VISIBLE
var/name_archive //For admin things like possession
@@ -123,6 +127,8 @@
var/obj/item/clothing/mask/wear_mask = null //Carbon
var/datum/hud/hud_used = null
+ /// Mob hud type
+ var/hud_type = /datum/hud
hud_possible = list(SPECIALROLE_HUD)
diff --git a/code/modules/mob/mob_lists.dm b/code/modules/mob/mob_lists.dm
new file mode 100644
index 00000000000..5d793ee56bd
--- /dev/null
+++ b/code/modules/mob/mob_lists.dm
@@ -0,0 +1,93 @@
+///Adds the mob reference to the list and directory of all mobs. Called on Initialize().
+/mob/proc/add_to_mob_list()
+ GLOB.mob_list |= src
+
+///Removes the mob reference from the list and directory of all mobs. Called on Destroy().
+/mob/proc/remove_from_mob_list()
+ GLOB.mob_list -= src
+
+///Adds the mob reference to the list of all mobs alive. If mob is cliented, it adds it to the list of all living player-mobs.
+/mob/proc/add_to_alive_mob_list()
+ if(QDELETED(src))
+ return
+ GLOB.alive_mob_list |= src
+ if(client)
+ add_to_current_living_players()
+
+///Removes the mob reference from the list of all mobs alive. If mob is cliented, it removes it from the list of all living player-mobs.
+/mob/proc/remove_from_alive_mob_list()
+ GLOB.alive_mob_list -= src
+ if(client)
+ remove_from_current_living_players()
+
+///Adds the mob reference to the list of all the dead mobs. If mob is cliented, it adds it to the list of all dead player-mobs.
+/mob/proc/add_to_dead_mob_list()
+ if(QDELETED(src))
+ return
+ GLOB.dead_mob_list |= src
+ if(client)
+ add_to_current_dead_players()
+
+///Remvoes the mob reference from list of all the dead mobs. If mob is cliented, it adds it to the list of all dead player-mobs.
+/mob/proc/remove_from_dead_mob_list()
+ GLOB.dead_mob_list -= src
+ if(client)
+ remove_from_current_dead_players()
+
+
+///Adds the cliented mob reference to the list of all player-mobs, besides to either the of dead or alive player-mob lists, as appropriate. Called on Login().
+/mob/proc/add_to_player_list()
+ SHOULD_CALL_PARENT(TRUE)
+ GLOB.player_list |= src
+ if(client.holder)
+ GLOB.keyloop_list |= src
+ else if(stat != DEAD)
+ GLOB.keyloop_list |= src
+ if(stat == DEAD)
+ add_to_current_dead_players()
+ else
+ add_to_current_living_players()
+
+///Removes the mob reference from the list of all player-mobs, besides from either the of dead or alive player-mob lists, as appropriate. Called on Logout().
+/mob/proc/remove_from_player_list()
+ SHOULD_CALL_PARENT(TRUE)
+ GLOB.player_list -= src
+ GLOB.keyloop_list -= src
+ if(stat == DEAD)
+ remove_from_current_dead_players()
+ else
+ remove_from_current_living_players()
+
+
+///Adds the cliented mob reference to either the list of dead player-mobs or to the list of observers, depending on how they joined the game.
+/mob/proc/add_to_current_dead_players()
+ GLOB.dead_player_list |= src
+
+/mob/dead/observer/add_to_current_dead_players()
+ if(started_as_observer)
+ GLOB.current_observers_list |= src
+ return
+ return ..()
+
+/mob/dead/new_player/add_to_current_dead_players()
+ return
+
+///Removes the mob reference from either the list of dead player-mobs or from the list of observers, depending on how they joined the game.
+/mob/proc/remove_from_current_dead_players()
+ GLOB.dead_player_list -= src
+
+/mob/dead/observer/remove_from_current_dead_players()
+ if(started_as_observer)
+ GLOB.current_observers_list -= src
+ return
+ return ..()
+
+
+///Adds the cliented mob reference to the list of living player-mobs. If the mob is an antag, it adds it to the list of living antag player-mobs.
+/mob/proc/add_to_current_living_players()
+ GLOB.alive_player_list |= src
+
+///Removes the mob reference from the list of living player-mobs. If the mob is an antag, it removes it from the list of living antag player-mobs.
+/mob/proc/remove_from_current_living_players()
+ GLOB.alive_player_list -= src
+
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 34adbed0045..50f0010cdf2 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -121,17 +121,23 @@
/mob/proc/say_quote(message, datum/language/speaking = null)
- var/verb = "says"
- var/ending = copytext(message, length(message))
-
+ var/ending = copytext_char(message, -1)
if(speaking)
- verb = genderize_decode(src, speaking.get_spoken_verb(ending))
- else
- if(ending == "!")
- verb = pick("exclaims", "shouts", "yells")
- else if(ending == "?")
- verb = "asks"
- return verb
+ return genderize_decode(src, speaking.get_spoken_verb(ending))
+ else if(ending == "!")
+ return get_verb(verb_exclaim)
+ else if(ending == "?")
+ return get_verb(verb_ask)
+ else if(copytext_char(message, -2) == "!!")
+ return get_verb(verb_yell)
+ return get_verb(verb_say)
+
+/mob/proc/get_verb(list/verbs)
+ if(!verbs)
+ return ""
+ if(!istype(verbs))
+ return verbs
+ return pick(verbs)
/// Transforms the speech emphasis mods from [/atom/movable/proc/say_emphasis] into the appropriate HTML tags
#define ENCODE_HTML_EMPHASIS(input, char, html, varname) \
diff --git a/code/modules/mob/mob_transformation_simple.dm b/code/modules/mob/mob_transformation_simple.dm
index 5e66d1a35f4..4f22cfe517f 100644
--- a/code/modules/mob/mob_transformation_simple.dm
+++ b/code/modules/mob/mob_transformation_simple.dm
@@ -47,7 +47,9 @@
mind.transfer_to(M)
else
M.key = key
-
+
+ SEND_SIGNAL(src, COMSIG_MOB_CHANGED_TYPE, M)
+
if(delete_old_mob)
spawn(1)
qdel(src)
diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm
index f68feabe24c..aa7e2381b4d 100644
--- a/code/modules/mob/new_player/new_player.dm
+++ b/code/modules/mob/new_player/new_player.dm
@@ -16,7 +16,7 @@
if(flags & INITIALIZED)
stack_trace("Warning: [src]([type]) initialized multiple times!")
flags |= INITIALIZED
- GLOB.mob_list += src
+ add_to_mob_list()
return INITIALIZE_HINT_NORMAL
/mob/new_player/proc/privacy_consent()
diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm
index 598647c2b75..f8dc37b811a 100644
--- a/code/modules/power/gravitygenerator.dm
+++ b/code/modules/power/gravitygenerator.dm
@@ -14,6 +14,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
#define GRAV_NEEDS_PLASTEEL 2
#define GRAV_NEEDS_WRENCH 3
+#define BLOB_HITS_NEED 4
+
//
// Abstract Generator
//
@@ -27,6 +29,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
use_power = NO_POWER_USE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | NO_MALF_EFFECT
var/sprite_number = 0
+ /// Number of successful blob hits
+ var/blob_hits = 0
/obj/machinery/gravity_generator/ex_act(severity)
@@ -35,7 +39,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
/obj/machinery/gravity_generator/blob_act(obj/structure/blob/B)
- if(prob(20))
+ blob_hits++
+ if(blob_hits >= BLOB_HITS_NEED)
set_broken()
diff --git a/code/modules/power/singularity/field_generator.dm b/code/modules/power/singularity/field_generator.dm
index 0ed692764d0..75371cdce4f 100644
--- a/code/modules/power/singularity/field_generator.dm
+++ b/code/modules/power/singularity/field_generator.dm
@@ -146,7 +146,7 @@ field_generator power level display
/obj/machinery/field/generator/blob_act(obj/structure/blob/B)
if(active)
- return 0
+ return FALSE
else
..()
diff --git a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
index bcd6b2b01ab..61f5602019e 100644
--- a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
+++ b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
@@ -134,10 +134,6 @@ So, hopefully this is helpful if any more icons are to be added/changed/wonderin
master.toggle_power()
investigate_log("was moved whilst active; it powered down.", INVESTIGATE_ENGINE)
-/obj/machinery/particle_accelerator/control_box/blob_act(obj/structure/blob/B)
- if(prob(50) && !QDELETED(src))
- qdel(src)
-
/obj/structure/particle_accelerator/update_icon_state()
switch(construction_state)
if(ACCELERATOR_UNWRENCHED, ACCELERATOR_WRENCHED)
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index 2973373acc0..3954a0bb1d7 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -355,7 +355,7 @@
consume(user)
-/obj/machinery/power/supermatter_shard/proc/get_integrity()
+/obj/machinery/power/supermatter_shard/proc/get_internal_integrity()
var/integrity = damage / explosion_point
integrity = round(100 - integrity * 100)
integrity = integrity < 0 ? 0 : integrity
@@ -541,16 +541,16 @@
if(!air)
return SUPERMATTER_ERROR
- if(get_integrity() < 25)
+ if(get_internal_integrity() < 25)
return SUPERMATTER_DELAMINATING
- if(get_integrity() < 50)
+ if(get_internal_integrity() < 50)
return SUPERMATTER_EMERGENCY
- if(get_integrity() < 75)
+ if(get_internal_integrity() < 75)
return SUPERMATTER_DANGER
- if((get_integrity() < 100) || (air.temperature > CRITICAL_TEMPERATURE))
+ if((get_internal_integrity() < 100) || (air.temperature > CRITICAL_TEMPERATURE))
return SUPERMATTER_WARNING
if(air.temperature > (CRITICAL_TEMPERATURE * 0.8))
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 2c68d8c9cb3..edce723f9f8 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -456,8 +456,12 @@
/obj/item/projectile/proc/check_ricochet_flag(atom/A)
- if(A.flags & CHECK_RICOCHET)
+ if((flag in list(ENERGY, LASER)) && (A.flags_ricochet & RICOCHET_SHINY))
return TRUE
+
+ if((flag in list(BOMB, BULLET)) && (A.flags_ricochet & RICOCHET_HARD))
+ return TRUE
+
return FALSE
diff --git a/code/modules/reagents/chemistry/holder.dm b/code/modules/reagents/chemistry/holder.dm
index 31326844b87..5bd7961ac18 100644
--- a/code/modules/reagents/chemistry/holder.dm
+++ b/code/modules/reagents/chemistry/holder.dm
@@ -137,9 +137,12 @@
/datum/reagents/proc/copy_to(obj/target, amount = 1, multiplier = 1, preserve_data = TRUE, safety = FALSE)
if(!target)
return
- if(!target.reagents || total_volume <= 0)
+ if(total_volume <= 0)
+ return
+
+ var/datum/reagents/R =(istype(target, /datum/reagents))? target : target?.reagents
+ if(!R || !istype(R))
return
- var/datum/reagents/R = target.reagents
amount = min(min(amount, total_volume), R.maximum_volume - R.total_volume)
var/part = amount / total_volume
var/trans_data = null
@@ -648,7 +651,7 @@
handle_reactions()
return FALSE
- var/datum/reagent/D = GLOB.chemical_reagents_list[reagent]
+ var/datum/reagent/D = (ispath(reagent))? new reagent() : GLOB.chemical_reagents_list[reagent]
if(D)
var/datum/reagent/R = new D.type()
diff --git a/code/modules/reagents/chemistry/machinery/chem_master.dm b/code/modules/reagents/chemistry/machinery/chem_master.dm
index 3348df63932..150cfbdf9c4 100644
--- a/code/modules/reagents/chemistry/machinery/chem_master.dm
+++ b/code/modules/reagents/chemistry/machinery/chem_master.dm
@@ -112,10 +112,6 @@
if(powered())
. += "waitlight"
-/obj/machinery/chem_master/blob_act(obj/structure/blob/B)
- if(prob(50) && !QDELETED(src))
- qdel(src)
-
/obj/machinery/chem_master/power_change()
if(!..())
return
diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm
index d22721d6f5f..96b7e362f07 100644
--- a/code/modules/reagents/chemistry/reagents.dm
+++ b/code/modules/reagents/chemistry/reagents.dm
@@ -68,6 +68,8 @@
return
/datum/reagent/proc/on_mob_life(mob/living/M)
+ if(current_cycle == 1)
+ on_mob_start_metabolize(M)
current_cycle++
var/total_depletion_rate = metabolization_rate * M.metabolism_efficiency * M.digestion_ratio // Cache it
@@ -75,8 +77,16 @@
sate_addiction(M)
holder.remove_reagent(id, total_depletion_rate) //By default it slowly disappears.
+ if(volume <= 0)
+ on_mob_end_metabolize(M)
return STATUS_UPDATE_NONE
+/datum/reagent/proc/on_mob_start_metabolize(mob/living/metabolizer)
+ return
+
+/datum/reagent/proc/on_mob_end_metabolize(mob/living/metabolizer)
+ return
+
/datum/reagent/proc/handle_addiction(mob/living/M, consumption_rate)
if(addiction_chance && count_by_type(M.reagents.addiction_list, addict_supertype) < 1)
var/datum/reagent/new_reagent = new addict_supertype()
diff --git a/code/modules/reagents/chemistry/reagents/blob.dm b/code/modules/reagents/chemistry/reagents/blob.dm
deleted file mode 100644
index b1a7952973e..00000000000
--- a/code/modules/reagents/chemistry/reagents/blob.dm
+++ /dev/null
@@ -1,195 +0,0 @@
-// These can only be applied by blobs. They are what blobs are made out of.
-// The 4 damage
-/datum/reagent/blob
- description = ""
- var/complementary_color = "#000000"
- var/message = "Блоб наносит вам удар" //message sent to any mob hit by the blob
- var/message_living = null //extension to first mob sent to only living mobs i.e. silicons have no skin to be burnt
- can_synth = FALSE
-
-/datum/reagent/blob/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume, show_message, touch_protection)
- return round(volume * min(1.5 - touch_protection, 1), 0.1) //full touch protection means 50% volume, any prot below 0.5 means 100% volume.
-
-/datum/reagent/blob/proc/damage_reaction(obj/structure/blob/B, damage, damage_type, damage_flag) //when the blob takes damage, do this
- return damage
-
-/datum/reagent/blob/ripping_tendrils //does brute and a little stamina damage
- name = "Разрывающие щупальца"
- description = "Наносит высокий урон травмами, а также урон выносливости."
- id = "ripping_tendrils"
- color = "#7F0000"
- complementary_color = "#a15656"
- message_living = ", и вы чувствуете, как ваша кожа рвется и слезает."
-
-/datum/reagent/blob/ripping_tendrils/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.6*volume, BRUTE)
- M.adjustStaminaLoss(volume)
- if(iscarbon(M))
- M.emote("scream")
-
-/datum/reagent/blob/boiling_oil //sets you on fire, does burn damage
- name = "Кипящее масло"
- description = "Наносит высокий урон ожогами и поджигает жертву."
- id = "boiling_oil"
- color = "#B68D00"
- complementary_color = "#c0a856"
- message = "Блоб обдает вас горящим маслом"
- message_living = ", и вы чувствуете, как ваша кожа обугливается и плавится"
-
-/datum/reagent/blob/boiling_oil/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- M.adjust_fire_stacks(round(volume/10))
- volume = ..()
- M.apply_damage(0.6*volume, BURN)
- M.IgniteMob()
- M.emote("scream")
-
-/datum/reagent/blob/envenomed_filaments //toxin, hallucination, and some bonus spore toxin
- name = "Ядовитые нити"
- description = "Наносит высокий урон токсинами, вызывает галлюцинации и вводит споры в кровоток."
- id = "envenomed_filaments"
- color = "#9ACD32"
- complementary_color = "#b0cd73"
- message_living = ", и вы чувствуете себя плохо. Вас тошнит"
-
-/datum/reagent/blob/envenomed_filaments/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.6 * volume, TOX)
- M.AdjustHallucinate(1.2 SECONDS * volume)
- if(M.reagents)
- M.reagents.add_reagent("spore", 0.4*volume)
-
-/datum/reagent/blob/lexorin_jelly //does tons of oxygen damage and a little brute
- name = "Лексориновое желе"
- description = "Наносит средний урон травмами, но огромный урон гипоксией."
- id = "lexorin_jelly"
- color = "#00FFC5"
- complementary_color = "#56ebc9"
- message_living = ", и ваши легкие кажутся тяжелыми и слабыми"
-
-/datum/reagent/blob/lexorin_jelly/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4*volume, BRUTE)
- M.apply_damage(1*volume, OXY)
- M.AdjustLoseBreath(round(0.6 SECONDS * volume))
-
-
-/datum/reagent/blob/kinetic //does semi-random brute damage
- name = "Кинетический желатин"
- description = "Наносит случайный урон травмами, в 0,33–2,33 раза превышающий стандартное количество."
- id = "kinetic"
- color = "#FFA500"
- complementary_color = "#ebb756"
- message = "Блоб избивает вас"
-
-/datum/reagent/blob/kinetic/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- var/damage = rand(5, 35)/25
- M.apply_damage(damage*volume, BRUTE)
-
-/datum/reagent/blob/cryogenic_liquid //does low burn damage and stamina damage and cools targets down
- name = "Криогенная жидкость"
- description = "Наносит средний урон травмами, урон выносливости и вводит в жертв ледяное масло, замораживая их до смерти."
- id = "cryogenic_liquid"
- color = "#8BA6E9"
- complementary_color = "#a8b7df"
- message = "Блоб обливает вас ледяной жидкостью"
- message_living = ", и вы чувствуете себя холодным и усталым"
-
-/datum/reagent/blob/cryogenic_liquid/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4*volume, BURN)
- M.adjustStaminaLoss(volume)
- if(M.reagents)
- M.reagents.add_reagent("frostoil", 0.4*volume)
-
-/datum/reagent/blob/b_sorium
- name = "Сорий"
- description = "Наносит высокий урон травмами и отбрасывает людей в стороны."
- id = "b_sorium"
- color = "#808000"
- complementary_color = "#a2a256"
- message = "Блоб врезается в вас и отбрасывает в сторону."
-
-/datum/reagent/blob/b_sorium/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- reagent_vortex(M, 1, volume)
- volume = ..()
- M.apply_damage(0.6*volume, BRUTE)
-
-/datum/reagent/blob/proc/reagent_vortex(mob/living/M, setting_type, volume)
- var/turf/pull = get_turf(M)
- var/range_power = clamp(round(volume/5, 1), 1, 5)
- for(var/atom/movable/X in range(range_power,pull))
- if(iseffect(X))
- continue
- if(X.move_resist <= MOVE_FORCE_DEFAULT && !X.anchored)
- var/distance = get_dist(X, pull)
- var/moving_power = max(range_power - distance, 1)
- spawn(0)
- if(moving_power > 2) //if the vortex is powerful and we're close, we get thrown
- if(setting_type)
- var/atom/throw_target = get_edge_target_turf(X, get_dir(X, get_step_away(X, pull)))
- var/throw_range = 5 - distance
- X.throw_at(throw_target, throw_range, 1)
- else
- X.throw_at(pull, distance, 1)
- else
- if(setting_type)
- for(var/i = 0, i < moving_power, i++)
- sleep(2)
- if(!step_away(X, pull))
- break
- else
- for(var/i = 0, i < moving_power, i++)
- sleep(2)
- if(!step_towards(X, pull))
- break
-
-/datum/reagent/blob/radioactive_gel
- name = "Радиоактивный гель"
- description = "Наносит средний урон токсинами и небольшой урон травмами, но облучает тех, кого задевает."
- id = "radioactive_gel"
- color = "#2476f0"
- complementary_color = "#24f0f0"
- message_living = ", и вы чувствуете странное тепло изнутри"
-
-/datum/reagent/blob/radioactive_gel/reaction_mob(mob/living/M, method = REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.3 * volume, TOX)
- M.apply_damage(0.2 * volume, BRUTE) // lets not have IPC / plasmaman only take 7.5 damage from this
- if(M.reagents)
- M.reagents.add_reagent("uranium", 0.3 * volume)
-
-/datum/reagent/blob/teslium_paste
- name = "Теслиевая паста"
- description = "Наносит средний урон ожогами и вызывает удары током у тех, кого задевает, со временем."
- id = "teslium_paste"
- color = "#20324D"
- complementary_color = "#412968"
- message_living = ", и вы чувствуете удар статическим электричеством"
-
-/datum/reagent/blob/teslium_paste/reaction_mob(mob/living/M, method=REAGENT_TOUCH, volume)
- if(method == REAGENT_TOUCH)
- volume = ..()
- M.apply_damage(0.4 * volume, BURN)
- if(M.reagents)
- if(M.reagents.has_reagent("teslium") && prob(0.6 * volume))
- M.electrocute_act((0.5 * volume), "разряда блоба", flags = SHOCK_NOGLOVES)
- M.reagents.del_reagent("teslium")
- return //don't add more teslium after you shock it out of someone.
- M.reagents.add_reagent("teslium", 0.125 * volume) // a little goes a long way
-
-/datum/reagent/blob/proc/send_message(mob/living/M)
- var/totalmessage = message
- if(message_living && !issilicon(M))
- totalmessage += message_living
- totalmessage += "!"
- to_chat(M, "[totalmessage]")
diff --git a/code/modules/reagents/chemistry/reagents/toxins.dm b/code/modules/reagents/chemistry/reagents/toxins.dm
index a8d96df05b4..b5a41014fa3 100644
--- a/code/modules/reagents/chemistry/reagents/toxins.dm
+++ b/code/modules/reagents/chemistry/reagents/toxins.dm
@@ -6,10 +6,11 @@
color = "#CF3600" // rgb: 207, 54, 0
taste_mult = 1.2
taste_description = "bitterness"
+ var/toxpwr = 2
/datum/reagent/toxin/on_mob_life(mob/living/M)
var/update_flags = STATUS_UPDATE_NONE
- update_flags |= M.adjustToxLoss(2, FALSE)
+ update_flags |= M.adjustToxLoss(toxpwr, FALSE)
return ..() | update_flags
/datum/reagent/spider_venom
@@ -511,19 +512,35 @@
return ..() | update_flags
-/datum/reagent/spore
+/datum/reagent/toxin/spore
name = "Spore Toxin"
- id = "spore"
description = "A natural toxin produced by blob spores that inhibits vision when ingested."
color = "#9ACD32"
+ id = "spore"
+ toxpwr = 1
+ can_synth = FALSE
taste_description = "bitterness"
-/datum/reagent/spore/on_mob_life(mob/living/M)
- var/update_flags = STATUS_UPDATE_NONE
- update_flags |= M.adjustToxLoss(1, FALSE)
- M.damageoverlaytemp = 60
- M.EyeBlurry(6 SECONDS)
- return ..() | update_flags
+/datum/reagent/toxin/spore/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.damageoverlaytemp = 60
+ affected_mob.update_damage_hud()
+ affected_mob.EyeBlurry(6 SECONDS * REM * seconds_per_tick)
+
+/datum/reagent/toxin/spore_burning
+ name = "Burning Spore Toxin"
+ description = "A natural toxin produced by blob spores that induces combustion in its victim."
+ color = "#9ACD32"
+ id = "spore_burn"
+ toxpwr = 0.5
+ taste_description = "burning"
+ can_synth = FALSE
+
+/datum/reagent/toxin/spore_burning/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired)
+ . = ..()
+ affected_mob.adjust_fire_stacks(2 * REM * seconds_per_tick)
+ affected_mob.IgniteMob()
+
/datum/reagent/beer2 //disguised as normal beer for use by emagged service borgs
name = "Beer"
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index df0112bc7ed..b7b8bedf1cd 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -100,6 +100,10 @@
name = "oil slime extract"
icon_state = "oil slime extract"
+/obj/item/slime_extract/oil/blob_vore_act(obj/structure/blob/special/core/voring_core)
+ obj_destruction(MELEE)
+
+
/obj/item/slime_extract/adamantine
name = "adamantine slime extract"
icon_state = "adamantine slime extract"
diff --git a/code/modules/ruins/ruin_areas.dm b/code/modules/ruins/ruin_areas.dm
index cb4ac6913f4..6b7f98c6f1b 100644
--- a/code/modules/ruins/ruin_areas.dm
+++ b/code/modules/ruins/ruin_areas.dm
@@ -9,6 +9,9 @@
ambientsounds = RUINS_SOUNDS
sound_environment = SOUND_ENVIRONMENT_STONEROOM
+/area/ruin/space
+ area_flags = NONE
+
/area/ruin/unpowered
always_unpowered = FALSE
@@ -31,6 +34,7 @@
/area/ruin/powered/space_bar
name = "Space Bar"
+ area_flags = NONE
/area/ruin/powered/shuttle
name = "Shuttle"
@@ -56,3 +60,4 @@
/area/ruin/spaceprison
name = "Space Prison"
icon_state = "spaceprison"
+ area_flags = NONE
diff --git a/code/modules/spacepods/spacepod.dm b/code/modules/spacepods/spacepod.dm
index 2419f889047..041ead235b6 100644
--- a/code/modules/spacepods/spacepod.dm
+++ b/code/modules/spacepods/spacepod.dm
@@ -291,7 +291,7 @@
update_icons()
-/obj/spacepod/proc/repair_damage(var/repair_amount)
+/obj/spacepod/repair_damage(repair_amount)
if(health)
health = min(initial(health), health + repair_amount)
update_icons()
diff --git a/code/modules/surgery/organs/organ_external.dm b/code/modules/surgery/organs/organ_external.dm
index 4eacc7768a4..b06c1aa65eb 100644
--- a/code/modules/surgery/organs/organ_external.dm
+++ b/code/modules/surgery/organs/organ_external.dm
@@ -438,6 +438,9 @@
return update_state()
+/obj/item/organ/external/blob_act()
+ external_receive_damage(max_damage, forced = TRUE)
+
/obj/item/organ/external/emp_act(severity)
if(!is_robotic() || emp_proof)
return
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index e825e855573..e87749c7242 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/effects/particles/bonfire.dmi b/icons/effects/particles/bonfire.dmi
new file mode 100644
index 00000000000..e8e2e36346d
Binary files /dev/null and b/icons/effects/particles/bonfire.dmi differ
diff --git a/icons/effects/particles/echo.dmi b/icons/effects/particles/echo.dmi
new file mode 100644
index 00000000000..60a243a8a7b
Binary files /dev/null and b/icons/effects/particles/echo.dmi differ
diff --git a/icons/effects/particles/generic.dmi b/icons/effects/particles/generic.dmi
new file mode 100644
index 00000000000..41776efdbfd
Binary files /dev/null and b/icons/effects/particles/generic.dmi differ
diff --git a/icons/effects/particles/goop.dmi b/icons/effects/particles/goop.dmi
new file mode 100644
index 00000000000..673c1a7ad5b
Binary files /dev/null and b/icons/effects/particles/goop.dmi differ
diff --git a/icons/effects/particles/pollen.dmi b/icons/effects/particles/pollen.dmi
new file mode 100644
index 00000000000..559c4d1846f
Binary files /dev/null and b/icons/effects/particles/pollen.dmi differ
diff --git a/icons/effects/particles/smoke.dmi b/icons/effects/particles/smoke.dmi
new file mode 100644
index 00000000000..99123beeb59
Binary files /dev/null and b/icons/effects/particles/smoke.dmi differ
diff --git a/icons/effects/particles/stink.dmi b/icons/effects/particles/stink.dmi
new file mode 100644
index 00000000000..29b92acbe67
Binary files /dev/null and b/icons/effects/particles/stink.dmi differ
diff --git a/icons/effects/particles/voidwalker.dmi b/icons/effects/particles/voidwalker.dmi
new file mode 100644
index 00000000000..d7f94c98797
Binary files /dev/null and b/icons/effects/particles/voidwalker.dmi differ
diff --git a/icons/effects/weather_effects.dmi b/icons/effects/weather_effects.dmi
index 00083c464a2..7cc1ce758a3 100644
Binary files a/icons/effects/weather_effects.dmi and b/icons/effects/weather_effects.dmi differ
diff --git a/icons/hud/blob.dmi b/icons/hud/blob.dmi
new file mode 100644
index 00000000000..552f511004f
Binary files /dev/null and b/icons/hud/blob.dmi differ
diff --git a/icons/mob/blob.dmi b/icons/mob/blob.dmi
index 3a73ccf0994..6313a92db0f 100644
Binary files a/icons/mob/blob.dmi and b/icons/mob/blob.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 85c7ad843a5..66628e3c044 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -69,6 +69,7 @@
#include "code\__DEFINES\footstep.dm"
#include "code\__DEFINES\game.dm"
#include "code\__DEFINES\gamemode.dm"
+#include "code\__DEFINES\generators.dm"
#include "code\__DEFINES\genetics.dm"
#include "code\__DEFINES\gravity.dm"
#include "code\__DEFINES\hud.dm"
@@ -105,6 +106,7 @@
#include "code\__DEFINES\obj_flags.dm"
#include "code\__DEFINES\organ_defines.dm"
#include "code\__DEFINES\overlays.dm"
+#include "code\__DEFINES\particles.dm"
#include "code\__DEFINES\path.dm"
#include "code\__DEFINES\pda.dm"
#include "code\__DEFINES\pipes.dm"
@@ -119,6 +121,7 @@
#include "code\__DEFINES\rolebans.dm"
#include "code\__DEFINES\rust_g.dm"
#include "code\__DEFINES\rust_g_overrides.dm"
+#include "code\__DEFINES\say.dm"
#include "code\__DEFINES\secret_documents.dm"
#include "code\__DEFINES\sensor_devices.dm"
#include "code\__DEFINES\shuttle.dm"
@@ -154,6 +157,7 @@
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\mapping.dm"
#include "code\__DEFINES\dcs\signals.dm"
+#include "code\__DEFINES\dcs\signals_blob.dm"
#include "code\__DEFINES\dcs\signals_object.dm"
#include "code\__DEFINES\traits\_traits.dm"
#include "code\__DEFINES\traits\declarations.dm"
@@ -167,6 +171,7 @@
#include "code\__HELPERS\atmospherics.dm"
#include "code\__HELPERS\atoms.dm"
#include "code\__HELPERS\bitflag_lists.dm"
+#include "code\__HELPERS\chat.dm"
#include "code\__HELPERS\cmp.dm"
#include "code\__HELPERS\constants.dm"
#include "code\__HELPERS\experimental.dm"
@@ -188,6 +193,7 @@
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\qdel.dm"
#include "code\__HELPERS\reagents_helpers.dm"
+#include "code\__HELPERS\ref.dm"
#include "code\__HELPERS\russian.dm"
#include "code\__HELPERS\sanitize_values.dm"
#include "code\__HELPERS\shell.dm"
@@ -251,6 +257,7 @@
#include "code\_onclick\hud\alien.dm"
#include "code\_onclick\hud\alien_larva.dm"
#include "code\_onclick\hud\blob_overmind.dm"
+#include "code\_onclick\hud\blobbernaut.dm"
#include "code\_onclick\hud\bot.dm"
#include "code\_onclick\hud\cogscarab.dm"
#include "code\_onclick\hud\constructs.dm"
@@ -304,6 +311,7 @@
#include "code\controllers\subsystem\early_assets.dm"
#include "code\controllers\subsystem\events.dm"
#include "code\controllers\subsystem\fires.dm"
+#include "code\controllers\subsystem\fluids.dm"
#include "code\controllers\subsystem\game_events.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\ghost_spawns.dm"
@@ -434,12 +442,15 @@
#include "code\datums\components\after_attacks_hub.dm"
#include "code\datums\components\animal_temperature.dm"
#include "code\datums\components\aura_healing.dm"
+#include "code\datums\components\blob_minion.dm"
+#include "code\datums\components\blob_turf_consuming.dm"
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\boss_music.dm"
#include "code\datums\components\caltrop.dm"
#include "code\datums\components\chasm.dm"
#include "code\datums\components\codeword_hearing.dm"
#include "code\datums\components\combo_attacks.dm"
+#include "code\datums\components\connect_containers.dm"
#include "code\datums\components\connect_loc_behalf.dm"
#include "code\datums\components\connect_mob_behalf.dm"
#include "code\datums\components\contsruction_regenerate.dm"
@@ -455,6 +466,7 @@
#include "code\datums\components\examine_override.dm"
#include "code\datums\components\force_move.dm"
#include "code\datums\components\fullauto.dm"
+#include "code\datums\components\ghost_direct_control.dm"
#include "code\datums\components\hide_highest_offset.dm"
#include "code\datums\components\jackboots.dm"
#include "code\datums\components\jetpack.dm"
@@ -470,6 +482,7 @@
#include "code\datums\components\spawner.dm"
#include "code\datums\components\spooky.dm"
#include "code\datums\components\squeak.dm"
+#include "code\datums\components\stationloving.dm"
#include "code\datums\components\surgery_initiator.dm"
#include "code\datums\components\swarming.dm"
#include "code\datums\components\transforming.dm"
@@ -683,6 +696,7 @@
#include "code\datums\status_effects\debuffs.dm"
#include "code\datums\status_effects\gas.dm"
#include "code\datums\status_effects\neutral.dm"
+#include "code\datums\status_effects\screwy_hud.dm"
#include "code\datums\status_effects\status_effect.dm"
#include "code\datums\status_effects\status_effects_absorption.dm"
#include "code\datums\weather\weather.dm"
@@ -755,17 +769,6 @@
#include "code\game\gamemodes\blob\blob.dm"
#include "code\game\gamemodes\blob\blob_finish.dm"
#include "code\game\gamemodes\blob\blob_report.dm"
-#include "code\game\gamemodes\blob\overmind.dm"
-#include "code\game\gamemodes\blob\powers.dm"
-#include "code\game\gamemodes\blob\theblob.dm"
-#include "code\game\gamemodes\blob\blobs\blob_mobs.dm"
-#include "code\game\gamemodes\blob\blobs\captured_nuke.dm"
-#include "code\game\gamemodes\blob\blobs\core.dm"
-#include "code\game\gamemodes\blob\blobs\factory.dm"
-#include "code\game\gamemodes\blob\blobs\node.dm"
-#include "code\game\gamemodes\blob\blobs\resource.dm"
-#include "code\game\gamemodes\blob\blobs\shield.dm"
-#include "code\game\gamemodes\blob\blobs\storage.dm"
#include "code\game\gamemodes\changeling\changeling.dm"
#include "code\game\gamemodes\changeling\thief_chan.dm"
#include "code\game\gamemodes\changeling\traitor_chan.dm"
@@ -1082,6 +1085,7 @@
#include "code\game\objects\effects\mines.dm"
#include "code\game\objects\effects\misc.dm"
#include "code\game\objects\effects\overlays.dm"
+#include "code\game\objects\effects\particle_holder.dm"
#include "code\game\objects\effects\portals.dm"
#include "code\game\objects\effects\snowcloud.dm"
#include "code\game\objects\effects\spiders.dm"
@@ -1112,6 +1116,9 @@
#include "code\game\objects\effects\effect_system\effects_smoke.dm"
#include "code\game\objects\effects\effect_system\effects_sparks.dm"
#include "code\game\objects\effects\effect_system\effects_water.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\_fluid_spread.dm"
+#include "code\game\objects\effects\effect_system\fluid_spread\effects_smoke.dm"
+#include "code\game\objects\effects\particles\water.dm"
#include "code\game\objects\effects\spawners\airlock_spawner.dm"
#include "code\game\objects\effects\spawners\bombspawner.dm"
#include "code\game\objects\effects\spawners\gibspawner.dm"
@@ -1585,7 +1592,45 @@
#include "code\modules\antagonists\_common\antag_team.dm"
#include "code\modules\antagonists\blob\blob_actions.dm"
#include "code\modules\antagonists\blob\blob_infected_datum.dm"
+#include "code\modules\antagonists\blob\blob_minion.dm"
#include "code\modules\antagonists\blob\blob_overmind_datum.dm"
+#include "code\modules\antagonists\blob\blobs_attack.dm"
+#include "code\modules\antagonists\blob\overmind.dm"
+#include "code\modules\antagonists\blob\powers.dm"
+#include "code\modules\antagonists\blob\powers_verbs.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_mob.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_spore.dm"
+#include "code\modules\antagonists\blob\blob_minions\blob_zombie.dm"
+#include "code\modules\antagonists\blob\blob_minions\blobbernaut.dm"
+#include "code\modules\antagonists\blob\blobstrains\_blobstrain.dm"
+#include "code\modules\antagonists\blob\blobstrains\_reagent.dm"
+#include "code\modules\antagonists\blob\blobstrains\blazing_oil.dm"
+#include "code\modules\antagonists\blob\blobstrains\blob_sorium.dm"
+#include "code\modules\antagonists\blob\blobstrains\cryogenic_poison.dm"
+#include "code\modules\antagonists\blob\blobstrains\debris_devourer.dm"
+#include "code\modules\antagonists\blob\blobstrains\distributed_neurons.dm"
+#include "code\modules\antagonists\blob\blobstrains\electromagnetic_web.dm"
+#include "code\modules\antagonists\blob\blobstrains\energized_jelly.dm"
+#include "code\modules\antagonists\blob\blobstrains\explosive_lattice.dm"
+#include "code\modules\antagonists\blob\blobstrains\multiplex.dm"
+#include "code\modules\antagonists\blob\blobstrains\networked_fibers.dm"
+#include "code\modules\antagonists\blob\blobstrains\pressurized_slime.dm"
+#include "code\modules\antagonists\blob\blobstrains\radioactive_gel.dm"
+#include "code\modules\antagonists\blob\blobstrains\reactive_spines.dm"
+#include "code\modules\antagonists\blob\blobstrains\regenerative_materia.dm"
+#include "code\modules\antagonists\blob\blobstrains\replicating_foam.dm"
+#include "code\modules\antagonists\blob\blobstrains\shifting_fragments.dm"
+#include "code\modules\antagonists\blob\blobstrains\synchronous_mesh.dm"
+#include "code\modules\antagonists\blob\structures\_blob.dm"
+#include "code\modules\antagonists\blob\structures\captured_nuke.dm"
+#include "code\modules\antagonists\blob\structures\core.dm"
+#include "code\modules\antagonists\blob\structures\factory.dm"
+#include "code\modules\antagonists\blob\structures\node.dm"
+#include "code\modules\antagonists\blob\structures\normal.dm"
+#include "code\modules\antagonists\blob\structures\resource.dm"
+#include "code\modules\antagonists\blob\structures\shield.dm"
+#include "code\modules\antagonists\blob\structures\special.dm"
+#include "code\modules\antagonists\blob\structures\storage.dm"
#include "code\modules\antagonists\borer\borer_action.dm"
#include "code\modules\antagonists\borer\borer_datum.dm"
#include "code\modules\antagonists\borer\borer_focus.dm"
@@ -2339,6 +2384,7 @@
#include "code\modules\mob\mob_defines.dm"
#include "code\modules\mob\mob_emote.dm"
#include "code\modules\mob\mob_helpers.dm"
+#include "code\modules\mob\mob_lists.dm"
#include "code\modules\mob\mob_movement.dm"
#include "code\modules\mob\mob_say.dm"
#include "code\modules\mob\mob_transformation_simple.dm"
@@ -2901,7 +2947,6 @@
#include "code\modules\reagents\chemistry\machinery\reagentgrinder.dm"
#include "code\modules\reagents\chemistry\reagents\admin.dm"
#include "code\modules\reagents\chemistry\reagents\alcohol.dm"
-#include "code\modules\reagents\chemistry\reagents\blob.dm"
#include "code\modules\reagents\chemistry\reagents\disease.dm"
#include "code\modules\reagents\chemistry\reagents\drink_base.dm"
#include "code\modules\reagents\chemistry\reagents\drink_cold.dm"