diff --git a/code/__DEFINES/armor.dm b/code/__DEFINES/armor.dm
index 4d7ccaeb285..2acd96b17c9 100644
--- a/code/__DEFINES/armor.dm
+++ b/code/__DEFINES/armor.dm
@@ -369,6 +369,25 @@
"wound" = 0, \
"damage_threshold" = 11)
+/* Robot Heavy Milbot (Boss-tier)
+ * High DR across the board
+ * Still weak to lasers (sensors and shit)
+ * Very resistant to bullets (thick armor plating)
+ * Very high DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_ROBOT_MILITARY_HEAVY list(\
+ "melee" = 25, \
+ "bullet" = 45, \
+ "laser" = -15, \
+ "energy" = -15, \
+ "bomb" = 10, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 20, \
+ "acid" = 20, \
+ "wound" = 0, \
+ "damage_threshold" = 15)
+
/* Deathclaw Commonboy
* Just about impervious to bullets
* Melee is... okay ish
@@ -664,6 +683,452 @@
"wound" = 5, \
"damage_threshold" = 3)
+/* Mirelurk armor
+ * Hard shell - resistant to bullets and melee
+ * Weak to energy weapons
+ * Water creature - good vs bio/rad
+ * Medium DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK list(\
+ "melee" = 35, \
+ "bullet" = 40, \
+ "laser" = 10, \
+ "energy" = 10, \
+ "bomb" = 15, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 5, \
+ "acid" = 50, \
+ "wound" = 10, \
+ "damage_threshold" = 8)
+
+/* Mirelurk Hunter armor
+ * Tougher shell - even better defense
+ * Higher DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK_HUNTER list(\
+ "melee" = 45, \
+ "bullet" = 50, \
+ "laser" = 15, \
+ "energy" = 15, \
+ "bomb" = 20, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 10, \
+ "acid" = 60, \
+ "wound" = 15, \
+ "damage_threshold" = 12)
+
+/* Mirelurk Baby armor
+ * Soft shell - much weaker
+ * Low DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK_BABY list(\
+ "melee" = 15, \
+ "bullet" = 20, \
+ "laser" = 5, \
+ "energy" = 5, \
+ "bomb" = 5, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 0, \
+ "acid" = 25, \
+ "wound" = 5, \
+ "damage_threshold" = 3)
+
+/* Mirelurk Queen armor
+ * Massive armored carapace
+ * Very high DT
+ * Boss-tier
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK_QUEEN list(\
+ "melee" = 60, \
+ "bullet" = 65, \
+ "laser" = 25, \
+ "energy" = 25, \
+ "bomb" = 35, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 20, \
+ "acid" = 80, \
+ "wound" = 25, \
+ "damage_threshold" = 18)
+
+/* Mirelurk Softshell armor
+ * Weak variant - hasn't hardened yet
+ * Low DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK_SOFT list(\
+ "melee" = 20, \
+ "bullet" = 15, \
+ "laser" = 5, \
+ "energy" = 5, \
+ "bomb" = 5, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 0, \
+ "acid" = 30, \
+ "wound" = 5, \
+ "damage_threshold" = 4)
+
+/* Mirelurk King armor
+ * Humanoid variant - different armor profile
+ * More balanced, less bullet resistance
+ * High DT, boss-tier
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_MIRELURK_KING list(\
+ "melee" = 50, \
+ "bullet" = 45, \
+ "laser" = 35, \
+ "energy" = 35, \
+ "bomb" = 30, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 25, \
+ "acid" = 70, \
+ "wound" = 20, \
+ "damage_threshold" = 15)
+
+/* Ant Queen armor
+ * Boss variant - heavily armored matriarch
+ * Very high melee/bullet resistance
+ * High DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_ANTS_QUEEN list(\
+ "melee" = 45, \
+ "bullet" = 40, \
+ "laser" = 10, \
+ "energy" = 10, \
+ "bomb" = 5, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 5, \
+ "acid" = 0, \
+ "wound" = 0, \
+ "damage_threshold" = 8)
+
+/* Radscorpion armor
+ * Armored carapace - good all-around protection
+ * Strong melee/bullet resistance
+ * Medium DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_RADSCORPION list(\
+ "melee" = 35, \
+ "bullet" = 30, \
+ "laser" = 15, \
+ "energy" = 15, \
+ "bomb" = 10, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 10, \
+ "acid" = 25, \
+ "wound" = 5, \
+ "damage_threshold" = 7)
+
+/* Black Radscorpion armor
+ * Tougher variant - thicker black exoskeleton
+ * Higher armor across the board
+ * Higher DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_RADSCORPION_BLACK list(\
+ "melee" = 45, \
+ "bullet" = 40, \
+ "laser" = 20, \
+ "energy" = 20, \
+ "bomb" = 15, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 15, \
+ "acid" = 35, \
+ "wound" = 10, \
+ "damage_threshold" = 10)
+
+/* Cazador armor
+ * Flying insect - weak armor, relies on speed
+ * Low protection overall
+ * Very low DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_CAZADOR list(\
+ "melee" = 10, \
+ "bullet" = 5, \
+ "laser" = 5, \
+ "energy" = 5, \
+ "bomb" = 0, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 0, \
+ "acid" = 10, \
+ "wound" = 0, \
+ "damage_threshold" = 2)
+
+/* Tunneler armor
+ * Deadly swarm creature with tough hide
+ * Moderate melee/bullet resistance
+ * Medium DT, relies on pack tactics
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_TUNNELER list(\
+ "melee" = 30, \
+ "bullet" = 25, \
+ "laser" = 15, \
+ "energy" = 15, \
+ "bomb" = 10, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 5, \
+ "acid" = 20, \
+ "wound" = 10, \
+ "damage_threshold" = 6)
+
+/* Centaur armor
+ * FEV mutant, ranged spitter
+ * Light armor coverage
+ * Low DT, relies on poison attacks
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_CENTAUR list(\
+ "melee" = 20, \
+ "bullet" = 15, \
+ "laser" = 10, \
+ "energy" = 10, \
+ "bomb" = 5, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 10, \
+ "acid" = 30, \
+ "wound" = 5, \
+ "damage_threshold" = 3)
+
+/* Abomination armor
+ * Massive FEV horror
+ * Extremely heavy armor
+ * Very high DT, nearly unstoppable
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_ABOMINATION list(\
+ "melee" = 60, \
+ "bullet" = 55, \
+ "laser" = 40, \
+ "energy" = 40, \
+ "bomb" = 30, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 20, \
+ "acid" = 50, \
+ "wound" = 30, \
+ "damage_threshold" = 15)
+
+/* Horror armor
+ * Failed FEV experiment
+ * Heavy armor coverage
+ * High DT, dangerous melee combatant
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_HORROR list(\
+ "melee" = 50, \
+ "bullet" = 45, \
+ "laser" = 30, \
+ "energy" = 30, \
+ "bomb" = 20, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 15, \
+ "acid" = 40, \
+ "wound" = 20, \
+ "damage_threshold" = 12)
+
+/* * * * * * * * * * * * * * *
+ * FACTION NPC ARMOR VALUES *
+ * * * * * * * * * * * * * * */
+
+/* Tribal armor
+ * Very basic protection
+ * Cloth and leather scraps
+ * Minimal resistance
+ * Low DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_TRIBAL list(\
+ "melee" = 15, \
+ "bullet" = 10, \
+ "laser" = 0, \
+ "energy" = 0, \
+ "bomb" = 0, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 5, \
+ "acid" = 0, \
+ "wound" = 5, \
+ "damage_threshold" = 1)
+
+/* Legion recruit armor
+ * Sports equipment and leather
+ * Better melee protection than bullet
+ * Weak to energy weapons
+ * Low DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_LEGION list(\
+ "melee" = 25, \
+ "bullet" = 20, \
+ "laser" = -5, \
+ "energy" = -10, \
+ "bomb" = 0, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 0, \
+ "acid" = 0, \
+ "wound" = 10, \
+ "damage_threshold" = 2)
+
+/* Legion veteran armor
+ * Reinforced sports armor with metal pieces
+ * Good melee protection
+ * Decent bullet resistance
+ * Still weak to energy weapons
+ * Decent DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_LEGION_VETERAN list(\
+ "melee" = 40, \
+ "bullet" = 30, \
+ "laser" = 10, \
+ "energy" = 0, \
+ "bomb" = 5, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 10, \
+ "acid" = 0, \
+ "wound" = 15, \
+ "damage_threshold" = 5)
+
+/* NCR trooper armor
+ * Standard combat armor
+ * Balanced protection
+ * Decent against most threats
+ * Decent DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_NCR list(\
+ "melee" = 30, \
+ "bullet" = 35, \
+ "laser" = 25, \
+ "energy" = 10, \
+ "bomb" = 10, \
+ "bio" = 0, \
+ "rad" = 0, \
+ "fire" = 10, \
+ "acid" = 10, \
+ "wound" = 15, \
+ "damage_threshold" = 4)
+
+/* NCR Ranger armor
+ * Elite riot gear
+ * Heavy protection across the board
+ * Well-rounded defense
+ * High DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_NCR_RANGER list(\
+ "melee" = 50, \
+ "bullet" = 55, \
+ "laser" = 45, \
+ "energy" = 20, \
+ "bomb" = 25, \
+ "bio" = 20, \
+ "rad" = 20, \
+ "fire" = 35, \
+ "acid" = 30, \
+ "wound" = 25, \
+ "damage_threshold" = 8)
+
+/* Brotherhood of Steel armor
+ * T-45d or T-51b Power Armor
+ * Strong protection
+ * Good energy resistance
+ * High DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_BOS list(\
+ "melee" = 70, \
+ "bullet" = 75, \
+ "laser" = 70, \
+ "energy" = 30, \
+ "bomb" = 50, \
+ "bio" = 80, \
+ "rad" = 40, \
+ "fire" = 70, \
+ "acid" = 70, \
+ "wound" = 40, \
+ "damage_threshold" = 10)
+
+/* Brotherhood Paladin armor
+ * Superior Power Armor (T-51b or T-60)
+ * Enhanced protection
+ * Better energy resistance
+ * Very high DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_BOS_PALADIN list(\
+ "melee" = 85, \
+ "bullet" = 90, \
+ "laser" = 85, \
+ "energy" = 40, \
+ "bomb" = 70, \
+ "bio" = 90, \
+ "rad" = 50, \
+ "fire" = 85, \
+ "acid" = 85, \
+ "wound" = 45, \
+ "damage_threshold" = 12)
+
+/* Enclave soldier armor
+ * Advanced Combat Armor or X-01 Power Armor
+ * Excellent protection
+ * Superior energy resistance
+ * Very high DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_ENCLAVE list(\
+ "melee" = 80, \
+ "bullet" = 85, \
+ "laser" = 80, \
+ "energy" = 35, \
+ "bomb" = 65, \
+ "bio" = 85, \
+ "rad" = 45, \
+ "fire" = 80, \
+ "acid" = 80, \
+ "wound" = 40, \
+ "damage_threshold" = 11)
+
+/* Enclave Advanced Power Armor
+ * Top-tier X-02 Advanced Power Armor
+ * Near-impenetrable protection
+ * Best energy resistance
+ * Extremely high DT
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_ENCLAVE_APA list(\
+ "melee" = 95, \
+ "bullet" = 100, \
+ "laser" = 95, \
+ "energy" = 45, \
+ "bomb" = 80, \
+ "bio" = 100, \
+ "rad" = 55, \
+ "fire" = 95, \
+ "acid" = 95, \
+ "wound" = 50, \
+ "damage_threshold" = 14)
+
+/* Chinese Remnant Soldier
+ * Ghoulified Chinese soldiers with military equipment
+ * Decent bullet resistance from worn armor
+ * Laser/energy resistance from ghoul biology
+ * Weak to melee due to degraded condition
+ * Low DT from aged equipment
+ * * * * * * * * * * * */
+#define ARMOR_VALUE_CHINESE_REMNANT list(\
+ "melee" = -25, \
+ "bullet" = 30, \
+ "laser" = 20, \
+ "energy" = 25, \
+ "bomb" = 0, \
+ "bio" = 100, \
+ "rad" = 100, \
+ "fire" = 10, \
+ "acid" = 10, \
+ "wound" = 0, \
+ "damage_threshold" = 2)
+
/* Armor Subclass multipliers
* Modifies base armor slots by these values
* Environmental and wound armors are multiplied by these values
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index d8a21f8fe23..9282768c3be 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -11,6 +11,15 @@
#define MOVE_INTENT_WALK "walk"
#define MOVE_INTENT_RUN "run"
+// Vision cone definitions (cone-based directional vision system)
+#define CONE_FRONT 1 // 90° front cone - best vision
+#define CONE_PERIPHERAL 2 // 45° side cones - reduced vision
+#define CONE_REAR 3 // Behind mob - no vision, sound only
+
+// Sound cone definitions (rear sound detection)
+#define SOUND_REAR_CENTER 1 // 90° rear cone - normal sound detection
+#define SOUND_REAR_PERIPHERAL 2 // 45° rear side cones - reduced sound detection
+
/// Normal baseline blood volume
#define BLOOD_VOLUME_NORMAL 1000
/// The amount blood typically regenerates to on its own
@@ -176,6 +185,7 @@
#define MOB_SIZE_SMALL 1
#define MOB_SIZE_HUMAN 2
#define MOB_SIZE_LARGE 3
+#define MOB_SIZE_HUGE 5
//Ventcrawling defines
#define VENTCRAWLER_NONE 0
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 3651a8a7453..6ba363c34c5 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -198,6 +198,7 @@
#define TRAIT_NO_MIDROUND_ANTAG "no-midround-antag" //can't be turned into an antag by random events
#define TRAIT_PUGILIST "pugilist" //This guy punches people for a living
#define TRAIT_KI_VAMPIRE "ki-vampire" //when someone with this trait rolls maximum damage on a punch and stuns the target, they regain some stamina and do clone damage
+#define TRAIT_ASSASSIN "assassin" //Can perform deadly assassination strikes on unaware enemies
#define TRAIT_PASSTABLE "passtable"
#define TRAIT_GIANT "giant"
#define TRAIT_DWARF "dwarf"
diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm
index 008195fc540..57f222efd4a 100644
--- a/code/controllers/subsystem/atoms.dm
+++ b/code/controllers/subsystem/atoms.dm
@@ -17,6 +17,11 @@ SUBSYSTEM_DEF(atoms)
/datum/controller/subsystem/atoms/Initialize(timeofday)
GLOB.fire_overlay.appearance_flags = RESET_COLOR
setupGenetics()
+
+ // OPTIMIZATION: Pre-allocate GLOB.machines to avoid repeated list resizing during init
+ // Typical maps have 1000-2500 machines, so pre-allocate to avoid performance hits
+ GLOB.machines = new /list(2500)
+
initialized = INITIALIZATION_INNEW_MAPLOAD
InitializeAtoms()
return ..()
@@ -30,6 +35,7 @@ SUBSYSTEM_DEF(atoms)
LAZYINITLIST(late_loaders)
var/count
+ var/batch_count = 0
var/list/mapload_arg = list(TRUE)
if(atoms)
count = atoms.len
@@ -37,14 +43,18 @@ SUBSYSTEM_DEF(atoms)
var/atom/A = I
if(!(A.flags_1 & INITIALIZED_1))
InitAtom(I, mapload_arg)
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
else
count = 0
for(var/atom/A in world)
if(!(A.flags_1 & INITIALIZED_1))
InitAtom(A, mapload_arg)
++count
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
testing("Initialized [count] atoms")
pass(count)
@@ -52,12 +62,37 @@ SUBSYSTEM_DEF(atoms)
initialized = INITIALIZATION_INNEW_REGULAR
if(late_loaders.len)
+ batch_count = 0
for(var/I in late_loaders)
var/atom/A = I
A.LateInitialize()
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
testing("Late initialized [late_loaders.len] atoms")
- late_loaders.Cut()
-
+ late_loaders.Cut()
+ // OPTIMIZATION: Batch camera visibility updates after all atoms initialized
+ if(atoms) // Only during mapload
+ testing("Updating camera visibility for all turfs...")
+ batch_count = 0
+ for(var/turf/T in world)
+ T.visibilityChanged()
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
+ testing("Camera visibility updated")
+
+ // OPTIMIZATION: Batch sunlight border smoothing after all turfs exist
+ if(atoms)
+ testing("Smoothing sunlight borders...")
+ batch_count = 0
+ for(var/turf/T in world)
+ if(T.sunlight_state == SUNLIGHT_BORDER && isnull(T.border_neighbors))
+ T.smooth_sunlight_border()
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
+ testing("Sunlight borders smoothed")
/datum/controller/subsystem/atoms/proc/InitAtom(atom/A, list/arguments)
var/the_type = A.type
if(QDELING(A))
diff --git a/code/controllers/subsystem/icon_smooth.dm b/code/controllers/subsystem/icon_smooth.dm
index 6f4040299ae..3ab6d65b242 100644
--- a/code/controllers/subsystem/icon_smooth.dm
+++ b/code/controllers/subsystem/icon_smooth.dm
@@ -32,11 +32,14 @@ SUBSYSTEM_DEF(icon_smooth)
smooth_zlevel(2,TRUE)
var/queue = smooth_queue
smooth_queue = list()
+ var/batch_count = 0
for(var/V in queue)
var/atom/A = V
if(!A || A.z <= 2)
continue
smooth_icon(A)
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
return ..()
diff --git a/code/controllers/subsystem/idlenpcpool.dm b/code/controllers/subsystem/idlenpcpool.dm
index 64419b443f0..9c84cf50ada 100644
--- a/code/controllers/subsystem/idlenpcpool.dm
+++ b/code/controllers/subsystem/idlenpcpool.dm
@@ -2,7 +2,7 @@ SUBSYSTEM_DEF(idlenpcpool)
name = "Idling NPC Pool"
flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT
priority = FIRE_PRIORITY_IDLE_NPC
- wait = 60
+ wait = 2 SECONDS // Reduced from 6s to 2s for faster initial mob response
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
var/list/currentrun = list()
diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm
index d9c52f12567..2cf27b5e42f 100644
--- a/code/controllers/subsystem/lighting.dm
+++ b/code/controllers/subsystem/lighting.dm
@@ -32,6 +32,7 @@ SUBSYSTEM_DEF(lighting)
if(!init_tick_checks)
MC_SPLIT_TICK
var/list/queue = GLOB.lighting_update_lights
+ var/batch_count = 0
while(length(queue))
var/datum/light_source/light_datum = queue[length(queue)]
queue.len--
@@ -41,7 +42,9 @@ SUBSYSTEM_DEF(lighting)
light_datum.needs_update = LIGHTING_NO_UPDATE
if(init_tick_checks)
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
else if (MC_TICK_CHECK)
break
@@ -49,6 +52,7 @@ SUBSYSTEM_DEF(lighting)
MC_SPLIT_TICK
queue = GLOB.lighting_update_corners
+ batch_count = 0
while(length(queue))
var/datum/lighting_corner/corner_datum = queue[length(queue)]
queue.len--
@@ -56,7 +60,9 @@ SUBSYSTEM_DEF(lighting)
corner_datum.update_objects()
corner_datum.needs_update = FALSE
if(init_tick_checks)
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
else if (MC_TICK_CHECK)
break
@@ -64,6 +70,7 @@ SUBSYSTEM_DEF(lighting)
MC_SPLIT_TICK
queue = GLOB.lighting_update_objects
+ batch_count = 0
while(length(queue))
var/atom/movable/lighting_object/lighting_object = queue[length(queue)]
queue.len--
@@ -74,7 +81,9 @@ SUBSYSTEM_DEF(lighting)
lighting_object.update()
lighting_object.needs_update = FALSE
if(init_tick_checks)
- CHECK_TICK
+ if(++batch_count >= 100)
+ batch_count = 0
+ CHECK_TICK
else if (MC_TICK_CHECK)
break
diff --git a/code/datums/traits/good.dm b/code/datums/traits/good.dm
index e5424cb7858..d0f01d88903 100644
--- a/code/datums/traits/good.dm
+++ b/code/datums/traits/good.dm
@@ -1,12 +1,14 @@
-GLOBAL_LIST_INIT(chemwhiz_recipes, list(
+GLOBAL_LIST_INIT(chemwhiz_recipes_basic, list(
+ /datum/crafting_recipe/cheap_stimpak,
+ /datum/crafting_recipe/medx/chemistry))
+
+GLOBAL_LIST_INIT(chemwhiz_recipes_advanced, list(
/datum/crafting_recipe/jet,
/datum/crafting_recipe/turbo,
/datum/crafting_recipe/psycho,
/datum/crafting_recipe/medx,
- /datum/crafting_recipe/medx/chemistry,
/datum/crafting_recipe/stimpak/chemistry,
/datum/crafting_recipe/stimpak5/chemistry,
- /datum/crafting_recipe/cheap_stimpak,
/datum/crafting_recipe/buffout,
/datum/crafting_recipe/steady))
@@ -89,7 +91,7 @@ GLOBAL_LIST_INIT(pa_repair, list(
/datum/crafting_recipe/repair_t45_helm,
/datum/crafting_recipe/scrap_pa,
/datum/crafting_recipe/scrap_pa_helm))
-
+
GLOBAL_LIST_INIT(white_legs_recipes, list(
/datum/crafting_recipe/tribalwar/whitelegs/lightarmour,
/datum/crafting_recipe/tribalwar/whitelegs/armour,
@@ -397,7 +399,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_WEAPONSMITH
gain_text = span_notice("You are adept at crafting makeshift weapons.")
lose_text = span_danger("You feel less adept at crafting makeshift weapons.")
- locked = TRUE
+ locked = TRUE
/datum/quirk/gunsmith/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -456,7 +458,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
gain_text = span_notice("The shadows seem a little less dark.")
lose_text = span_danger("Everything seems a little darker.")
-
/datum/quirk/night_vision/on_spawn()
var/mob/living/carbon/human/H = quirk_holder
H.update_sight()
@@ -510,27 +511,43 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
lose_text = span_danger("You feel like skipping practice.")
locked = TRUE
*/
+
/datum/quirk/chemwhiz
name = "Chem Whiz"
- desc = "You've been playing around with chemicals all your life. You know how to use chemistry machinery."
+ desc = "You've been playing around with chemicals all your life. You know how to use chemistry machinery. High intelligence unlocks advanced formulas."
value = 3
mob_trait = TRAIT_CHEMWHIZ
gain_text = span_notice("The mysteries of chemistry are revealed to you.")
lose_text = span_danger("You forget how the periodic table works.")
- locked = TRUE
+ locked = TRUE
/datum/quirk/chemwhiz/add()
var/mob/living/carbon/human/H = quirk_holder
- // I made the quirks add the same recipes as the trait books. Feel free to nerf this
if(!H.mind.learned_recipes)
H.mind.learned_recipes = list()
- H.mind.learned_recipes |= GLOB.chemwhiz_recipes
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic
+ if(H.special_i >= 6)
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
+ to_chat(H, span_notice("Your sharp mind unlocks advanced chemical formulas."))
+ else
+ to_chat(H, span_notice("You can make basic stabilization supplies, but advanced formulas are beyond you for now. (Requires 6 INT)"))
/datum/quirk/chemwhiz/remove()
var/mob/living/carbon/human/H = quirk_holder
if(H)
- H.mind.learned_recipes -= GLOB.chemwhiz_recipes
-
+ H.mind.learned_recipes -= GLOB.chemwhiz_recipes_basic
+ H.mind.learned_recipes -= GLOB.chemwhiz_recipes_advanced
+
+// Call this wherever INT (special_i) changes
+/mob/living/carbon/human/proc/update_chemwhiz_recipes()
+ if(!HAS_TRAIT(src, TRAIT_CHEMWHIZ) || !mind)
+ return
+ if(!mind.learned_recipes)
+ mind.learned_recipes = list()
+ if(special_i >= 6)
+ mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
+ else
+ mind.learned_recipes -= GLOB.chemwhiz_recipes_advanced
/datum/quirk/pa_wear
name = "Power Armor Training"
@@ -745,7 +762,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_WHITELEGS_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/whitelegstraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -765,7 +782,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_DEADHORSES_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/deadhorsestraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -785,7 +802,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_RUSTWALKERS_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/rustwalkerstraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -805,7 +822,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_EIGHTIES_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/eightiestraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -825,7 +842,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_SORROWS_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/sorrowstraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -845,7 +862,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_WAYFARER_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/wayfarertraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -865,7 +882,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_BONEDANCER_TRAD
gain_text = span_notice("The mysteries of your ancestors are revealed to you.")
lose_text = span_danger("You forget how your ancestors created their garments.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/bonedancertraditions/add()
var/mob/living/carbon/human/H = quirk_holder
@@ -885,7 +902,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_PUSHIMMUNE
gain_text = span_notice("You feel stronger than a brick wall.")
lose_text = span_danger("Your feel like you could get thrown down again.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/heatresist
name = "Heat Resistant"
@@ -894,7 +911,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_RESISTHEAT
gain_text = span_notice("It could be a little warmer in here.")
lose_text = span_danger("You know? Being hot kind of sucks actually.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/coldresist
name = "Cold Resistant"
@@ -903,7 +920,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_RESISTCOLD
gain_text = span_notice("It could be a little colder in here.")
lose_text = span_danger("You know? Being cold kind of sucks actually.")
- locked = FALSE
+ locked = FALSE
/*
/datum/quirk/radimmune
@@ -913,7 +930,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_RADIMMUNE
gain_text = span_notice("You've decided radiation just doesn't matter.")
lose_text = span_danger("You no longer feel like you could probably live in a microwave while it's on.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/radimmuneish
name = "Radiation - Mostly Immune"
@@ -922,8 +939,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_75_RAD_RESIST
gain_text = span_notice("You've decided radiation just doesn't matter much.")
lose_text = span_danger("You no longer feel like you could roll around in a rad puddle for a while.")
- locked = FALSE
-
+ locked = FALSE
/datum/quirk/radimmunesorta
name = "Radiation - Sorta Immune"
@@ -932,8 +948,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_50_RAD_RESIST
gain_text = span_notice("You've decided radiation only kind of matters.")
lose_text = span_danger("You no longer think you should hang out next to rad puddles.")
- locked = TRUE
-
+ locked = TRUE
/datum/quirk/nohunger
name = "Does not Eat"
@@ -942,7 +957,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_NOHUNGER
gain_text = span_notice("Your need for food has left you.")
lose_text = span_danger("GOD YOU WANT A BURGER SO BAD.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/thickskin
name = "Thick Skin"
@@ -951,7 +966,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_PIERCEIMMUNE
gain_text = span_notice("Your skin feels way stronger.")
lose_text = span_danger("You feel like your skin is about as tough as tissue paper.")
- locked = TRUE
+ locked = TRUE
*/
/datum/quirk/barbedwire
@@ -970,7 +985,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_QUICKER_CARRY
gain_text = span_notice("You feel like a MASTER fireman!")
lose_text = span_danger("Your ability to carry folk seems massively diminished.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/quickcarry
name = "Quick Carry"
@@ -979,7 +994,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_QUICK_CARRY
gain_text = span_notice("You feel like an ACCEPTABLE fireman!")
lose_text = span_danger("Your ability to carry folk seems a bit diminished.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/builder
name = "Experienced Builder"
@@ -988,7 +1003,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_QUICK_BUILD
gain_text = span_notice("You could throw up a house if you wanted to!")
lose_text = span_danger("What's a two by four again?")
- locked = FALSE
+ locked = FALSE
/*
/datum/quirk/grappler
@@ -998,7 +1013,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_STRONG_GRABBER
gain_text = span_notice("You could wrassle a deathclaw!!")
lose_text = span_danger("You no longer feel like you should wrestle deathclaws.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/mastermartialartist
name = "Master Martial Artist"
@@ -1007,7 +1022,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_KI_VAMPIRE
gain_text = span_notice("They are already dead.")
lose_text = span_danger("Your fists no longer feel so powerful.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/surestrike
name = "Sure Strike"
@@ -1016,7 +1031,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_PERFECT_ATTACKER
gain_text = span_notice("They are already dead.")
lose_text = span_danger("Your fists no longer feel so powerful.")
- locked = FALSE
+ locked = FALSE
*/
/datum/quirk/quietstep
@@ -1026,7 +1041,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_SILENT_STEP
gain_text = span_notice("Your footsteps fade away.")
lose_text = span_danger("You find yourself surprised by the sound of your own footsteps.")
- locked = FALSE
+ locked = FALSE
/*
/datum/quirk/deadeye
name = "Dead Eye"
@@ -1035,7 +1050,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_INSANE_AIM
gain_text = span_notice("Your aim is legendary, and you know it.")
lose_text = span_danger("Your aim could use some work...")
- locked = FALSE
+ locked = FALSE
/datum/quirk/straightshooter
name = "Straight Shooter"
@@ -1044,8 +1059,9 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_NICE_SHOT
gain_text = span_notice("Your aim is amazing, and you know it.")
lose_text = span_danger("Your aim could use some work...")
- locked = TRUE
+ locked = TRUE
*/
+
/datum/quirk/bowtrained
name = "Bow Trained"
desc = "You've trained quite a bit with bows of many types, and are pretty good with them for it."
@@ -1053,7 +1069,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_AUTO_DRAW
gain_text = span_notice("You feel like all that training with bows has paid off.")
lose_text = span_danger("Guns were always better...")
- locked = FALSE
+ locked = FALSE
/datum/quirk/masterrifleman
name = "Bolt Worker"
@@ -1062,7 +1078,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_FAST_PUMP
gain_text = span_notice("In a sudden haze you realize that the Mosin-Nagant was God's gift to mankind.")
lose_text = span_danger("After picking some 250 year old cosmoline out from under one of your nails you realize that... Uh, no, the Mosin-Nagant is a piece of shit.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/playdead
name = "Class Act"
@@ -1071,7 +1087,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
mob_trait = TRAIT_PLAY_DEAD
gain_text = span_notice("You feel confident at playing dead.")
lose_text = span_danger("You feel that laying down in a field of gunfire may not be such a good idea after all.")
- locked = FALSE
+ locked = FALSE
/datum/quirk/ratfriend
name = "Beast Friend - Rats"
@@ -1079,7 +1095,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
value = 2
mob_trait = TRAIT_BEASTFRIEND_RAT
gain_text = span_notice("Rats are friends!")
- lose_text = span_danger("God of rats curses your name...") // Perhaps make killing related mobs lose the quirk?
+ lose_text = span_danger("God of rats curses your name...")
locked = FALSE
/datum/quirk/ratfriend/add()
@@ -1185,7 +1201,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
gain_text = span_notice("You remember the old ways of your tribe...")
lose_text = span_notice("You've forgotten the ways of your ancestors...")
-
/datum/quirk/tribespeak/add()
var/mob/living/carbon/human/H = quirk_holder
H.grant_language(/datum/language/tribal)
@@ -1202,7 +1217,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
gain_text = span_notice("You remember the old tongue of the Mexican cartels.")
lose_text = span_notice("You've forgotten the tongue of the Mexican cartels.")
-
/datum/quirk/spanishspeak/add()
var/mob/living/carbon/human/H = quirk_holder
H.grant_language(/datum/language/spanish)
@@ -1243,3 +1257,198 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list(
var/mob/living/carbon/human/H = quirk_holder
if(!QDELETED(H))
H.remove_language(/datum/language/japanese)
+
+/datum/quirk/assassin
+ name = "Assassin"
+ desc = "Years of practice have made you lethal at close range. While sneaking with a melee weapon in your active hand, you can assassinate unaware enemies within 2 tiles by attacking from behind."
+ value = 4
+ mob_trait = TRAIT_ASSASSIN
+ gain_text = span_notice("You feel like you could kill someone very quietly.")
+ lose_text = span_danger("You feel less lethal.")
+ locked = FALSE
+ var/datum/action/cooldown/assassinate/assassinate_action
+
+/datum/quirk/assassin/add()
+ assassinate_action = new()
+ assassinate_action.Grant(quirk_holder)
+ assassinate_action.UpdateButtonIcon()
+
+/datum/quirk/assassin/remove()
+ if(assassinate_action)
+ assassinate_action.Remove(quirk_holder)
+ QDEL_NULL(assassinate_action)
+
+// ========== ASSASSINATE ACTION ==========
+/datum/action/cooldown/assassinate
+ name = "Assassinate"
+ desc = "Strike a killing blow on an unaware enemy within 2 tiles from behind. Requires a melee weapon in your active hand and sneak mode. Target must not be alerted to your presence."
+ button_icon_state = "dagger"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ check_flags = AB_CHECK_CONSCIOUS
+ cooldown_time = 15 SECONDS
+
+/// Check if assassin is behind the target (in their rear cone)
+/datum/action/cooldown/assassinate/proc/is_behind_target(mob/attacker, mob/target)
+ if(!attacker || !target)
+ return FALSE
+
+ var/turf/attacker_turf = get_turf(attacker)
+ var/turf/target_turf = get_turf(target)
+
+ if(!attacker_turf || !target_turf)
+ return FALSE
+
+ if(attacker_turf == target_turf)
+ return FALSE
+
+ var/dx = attacker_turf.x - target_turf.x
+ var/dy = attacker_turf.y - target_turf.y
+
+ var/target_angle = 0
+ switch(target.dir)
+ if(NORTH)
+ target_angle = 0
+ if(SOUTH)
+ target_angle = 180
+ if(EAST)
+ target_angle = 90
+ if(WEST)
+ target_angle = 270
+
+ var/attacker_angle = arctan(dy, dx)
+ var/relative_angle = attacker_angle - target_angle
+
+ while(relative_angle > 180)
+ relative_angle -= 360
+ while(relative_angle < -180)
+ relative_angle += 360
+
+ if(relative_angle > 90 || relative_angle < -90)
+ return TRUE
+
+ return FALSE
+
+/datum/action/cooldown/assassinate/IsAvailable(silent = FALSE)
+ if(!..())
+ return FALSE
+
+ if(!ishuman(owner))
+ return FALSE
+
+ var/mob/living/carbon/human/H = owner
+
+ if(!H.sneaking)
+ return FALSE
+
+ var/obj/item/held_item = H.get_active_held_item()
+ if(!held_item)
+ return FALSE
+
+ if(!istype(held_item, /obj/item/melee) && !istype(held_item, /obj/item/kitchen) && held_item.force <= 0)
+ return FALSE
+
+ for(var/mob/living/L in range(2, H))
+ if(L == H)
+ continue
+ if(L.stat != CONSCIOUS)
+ continue
+ if(!is_behind_target(H, L))
+ continue
+ if(istype(L, /mob/living/simple_animal/hostile))
+ var/mob/living/simple_animal/hostile/enemy = L
+ if(enemy.faction_check_mob(H))
+ continue
+ if(enemy.target == H)
+ continue
+ return TRUE
+ else if(ishuman(L) && L != H)
+ return TRUE
+
+ return FALSE
+
+/datum/action/cooldown/assassinate/Grant(mob/M)
+ ..()
+ if(!ishuman(M))
+ return
+ UpdateButtonIcon()
+
+/datum/action/cooldown/assassinate/Trigger()
+ if(!..())
+ return FALSE
+
+ if(!ishuman(owner))
+ return FALSE
+
+ var/mob/living/carbon/human/H = owner
+ var/mob/living/target = null
+ var/closest_dist = 999
+
+ for(var/mob/living/L in range(2, H))
+ if(L == H)
+ continue
+ if(L.stat != CONSCIOUS)
+ continue
+ if(!is_behind_target(H, L))
+ continue
+
+ var/dist = get_dist(H, L)
+
+ if(istype(L, /mob/living/simple_animal/hostile))
+ var/mob/living/simple_animal/hostile/enemy = L
+ if(enemy.faction_check_mob(H))
+ continue
+ if(enemy.target == H)
+ continue
+ if(dist < closest_dist)
+ closest_dist = dist
+ target = L
+ else if(ishuman(L))
+ if(dist < closest_dist)
+ closest_dist = dist
+ target = L
+
+ if(!target)
+ to_chat(H, span_warning("No valid target found (must attack from behind)."))
+ return FALSE
+
+ var/obj/item/weapon = H.get_active_held_item()
+ if(!weapon)
+ to_chat(H, span_warning("You need a weapon in your active hand!"))
+ return FALSE
+
+ if(!istype(weapon, /obj/item/melee) && !istype(weapon, /obj/item/kitchen) && weapon.force <= 0)
+ to_chat(H, span_warning("[weapon] is not suitable for assassination!"))
+ return FALSE
+
+ var/current_distance = get_dist(H, target)
+ if(current_distance == 2)
+ var/lunge_dir = get_dir(H, target)
+ step(H, lunge_dir)
+ H.visible_message(
+ span_danger("[H] lunges at [target]!"),
+ span_userdanger("You lunge at [target]!")
+ )
+ playsound(H.loc, 'sound/weapons/thudswoosh.ogg', 50, TRUE)
+
+ H.do_attack_animation(target)
+ H.visible_message(
+ span_danger("[H] strikes with lethal precision!"),
+ span_userdanger("You strike at [target]'s vitals!")
+ )
+
+ var/base_damage = weapon.force
+ var/assassination_damage = base_damage + 40
+ var/secondary_damage = round(assassination_damage * 0.4)
+
+ target.apply_damage(assassination_damage, BRUTE, ran_zone())
+ target.apply_damage(secondary_damage, BRUTE, ran_zone())
+
+ shake_camera(H, 3, 1.5)
+
+ if(isliving(target))
+ target.Stun(20)
+
+ log_combat(H, target, "assassinated")
+ StartCooldown()
+ UpdateButtonIcon()
+ return TRUE
diff --git a/code/game/objects/effects/overlays.dm b/code/game/objects/effects/overlays.dm
index 2e5d6717fb5..791a2f22fd4 100644
--- a/code/game/objects/effects/overlays.dm
+++ b/code/game/objects/effects/overlays.dm
@@ -635,3 +635,283 @@
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
plane = WALL_PLANE
layer = ABOVE_OBJ_LAYER
+
+// SNEAKING MODE OVERLAYS
+// Visual effects for the sneak mode system
+
+// Sneak icon overlay that appears above player's head
+// Sneak mode indicator - overlay appearance for add_overlay
+/obj/effect/overlay/sneak_icon
+ name = ""
+ icon = 'icons/mob/actions/actions_changeling.dmi'
+ icon_state = "ling_augmented_eyesight"
+ layer = ABOVE_MOB_LAYER
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ appearance_flags = RESET_COLOR | TILE_BOUND | PIXEL_SCALE
+ alpha = 102 // 60% transparent (40% opaque)
+
+/obj/effect/overlay/sneak_icon/Initialize()
+ . = ..()
+ pixel_y = 20 // Above the head
+ // Make 30% smaller
+ transform = matrix().Scale(0.7, 0.7)
+
+
+// VISION CONE OVERLAY - Fixed angle calculation version
+// Properly fills cone areas using dot product comparison
+
+/obj/effect/overlay/vision_cone
+ name = ""
+ icon = null
+ layer = BELOW_MOB_LAYER
+ plane = CHAT_PLANE // Use CHAT_PLANE to bypass FoV hiding
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ anchored = TRUE
+ alpha = 80
+
+ var/list/cone_images = list() // Images shown to the viewer's client
+ var/mob/living/carbon/human/viewer = null // The player viewing these cones
+ var/mob/living/simple_animal/hostile/tracked_mob = null
+ var/cone_dir = NORTH
+ var/cone_range = 9
+ var/cone_icon_state = "white" // Use white base for color tinting
+ var/cone_alpha = 80 // Alpha value for this cone
+ var/cone_angle = 62.5 // Default: 125° large center cones (62.5° half-angle)
+
+ // Sector mode variables
+ var/sector_start_angle = 0 // Start angle for sector mode
+ var/sector_end_angle = 0 // End angle for sector mode
+ var/use_sector_mode = FALSE // Use angular sector instead of cone
+
+/obj/effect/overlay/vision_cone/Destroy()
+ // Remove images from client
+ if(viewer && viewer.client)
+ viewer.client.images -= cone_images
+ cone_images.Cut()
+ return ..()
+
+/obj/effect/overlay/vision_cone/proc/setup_cone(mob/living/simple_animal/hostile/H, mob_dir, range_tiles, icon_state_name = "white")
+ tracked_mob = H
+ cone_dir = mob_dir
+ cone_range = range_tiles
+ cone_icon_state = icon_state_name
+ cone_alpha = alpha // Capture the alpha value that was set on this cone
+ cone_angle = 62.5 // 125° large center cones with overlap
+
+// Setup a narrow cone (for peripheral/side zones)
+/obj/effect/overlay/vision_cone/proc/setup_narrow_cone(mob/living/simple_animal/hostile/H, mob_dir, range_tiles, angle_width = 16.5)
+ tracked_mob = H
+ cone_dir = mob_dir
+ cone_range = range_tiles
+ cone_icon_state = "white"
+ cone_alpha = alpha
+ cone_angle = angle_width // Use specified angle width
+ use_sector_mode = FALSE
+
+// Setup for sector mode
+/obj/effect/overlay/vision_cone/proc/setup_sector(mob/living/simple_animal/hostile/H, base_dir, start_deg, end_deg, range_tiles, icon_state_name = "white")
+ tracked_mob = H
+ cone_dir = base_dir
+ cone_range = range_tiles
+ sector_start_angle = start_deg
+ sector_end_angle = end_deg
+ use_sector_mode = TRUE
+ cone_alpha = alpha
+ cone_icon_state = icon_state_name
+
+// Generate sector images using quadrant-based angle calculation
+/obj/effect/overlay/vision_cone/proc/generate_cone_image()
+ var/turf/center = get_turf(tracked_mob)
+ if(!center)
+ return FALSE
+
+ // Remove old images from client
+ if(viewer && viewer.client)
+ viewer.client.images -= cone_images
+ cone_images.Cut()
+
+ // Get all turfs in the sector using quadrant-based calculation
+ var/list/cone_turfs
+ if(use_sector_mode)
+ cone_turfs = get_sector_turfs(center, cone_dir, cone_range, sector_start_angle, sector_end_angle)
+ else
+ return FALSE
+
+ if(!cone_turfs || !cone_turfs.len)
+ return FALSE
+
+ // Create images at each turf location on CHAT_PLANE
+ for(var/turf/T in cone_turfs)
+ var/image/img = image('icons/effects/alphacolors.dmi', T, cone_icon_state)
+ img.layer = BELOW_MOB_LAYER
+ img.plane = CHAT_PLANE // CHAT_PLANE bypasses FoV
+ img.alpha = cone_alpha
+ cone_images += img
+
+ // Add all images to viewer's client at once
+ if(viewer && viewer.client)
+ viewer.client.images += cone_images
+
+ return TRUE
+
+/obj/effect/overlay/vision_cone/proc/get_sector_turfs(turf/center, base_direction, range, start_angle_deg, end_angle_deg)
+ var/list/turfs = list()
+
+ // Get base direction angle (0° = NORTH, clockwise)
+ var/base_angle = dir_to_angle(base_direction)
+
+ // Convert sector angles to absolute angles
+ var/absolute_start = base_angle + start_angle_deg
+ var/absolute_end = base_angle + end_angle_deg
+
+ // Normalize angles to 0-360
+ while(absolute_start < 0)
+ absolute_start += 360
+ while(absolute_start >= 360)
+ absolute_start -= 360
+ while(absolute_end < 0)
+ absolute_end += 360
+ while(absolute_end >= 360)
+ absolute_end -= 360
+
+ // Check all turfs in range
+ for(var/turf/T in range(range, center))
+ if(T == center)
+ continue
+
+ var/dx = T.x - center.x
+ var/dy = T.y - center.y
+
+ if(dx == 0 && dy == 0)
+ continue
+
+ // QUADRANT-BASED ANGLE CALCULATION
+ var/turf_angle = calculate_angle_from_offset(dx, dy)
+
+ // Check if turf angle is within sector
+ var/in_sector = FALSE
+ if(absolute_start < absolute_end)
+ in_sector = (turf_angle >= absolute_start && turf_angle < absolute_end)
+ else
+ in_sector = (turf_angle >= absolute_start || turf_angle < absolute_end)
+
+ if(in_sector)
+ turfs += T
+
+ return turfs
+
+/obj/effect/overlay/vision_cone/proc/get_turf_in_direction(turf/start, direction, distance)
+ var/turf/current = start
+ for(var/i = 1 to distance)
+ current = get_step(current, direction)
+ if(!current)
+ return null
+ return current
+
+// Helper: Convert BYOND direction to angle in degrees
+// 0° = NORTH (up), increases CLOCKWISE
+// NORTH = 0°, EAST = 90°, SOUTH = 180°, WEST = 270°
+/obj/effect/overlay/vision_cone/proc/dir_to_angle(byond_dir)
+ switch(byond_dir)
+ if(NORTH)
+ return 0 // Up
+ if(NORTHEAST)
+ return 45 // Up-right diagonal
+ if(EAST)
+ return 90 // Right
+ if(SOUTHEAST)
+ return 135 // Down-right diagonal
+ if(SOUTH)
+ return 180 // Down
+ if(SOUTHWEST)
+ return 225 // Down-left diagonal
+ if(WEST)
+ return 270 // Left
+ if(NORTHWEST)
+ return 315 // Up-left diagonal
+ return 0
+
+// Helper: Calculate angle from dx/dy offset using quadrant-based logic
+// Returns angle in degrees: 0° = NORTH, clockwise
+/obj/effect/overlay/vision_cone/proc/calculate_angle_from_offset(dx, dy)
+ var/angle = 0
+
+ // Handle cardinal directions first (avoids arctan edge cases)
+ if(dx == 0)
+ if(dy > 0)
+ return 0 // Pure NORTH
+ else
+ return 180 // Pure SOUTH
+ else if(dy == 0)
+ if(dx > 0)
+ return 90 // Pure EAST
+ else
+ return 270 // Pure WEST
+
+ // Diagonal - use arctan on absolute values to get acute angle
+ var/acute_angle = arctan(abs(dy), abs(dx))
+
+ // Map to correct quadrant based on dx/dy signs
+ if(dx > 0 && dy > 0)
+ // NE quadrant (0° to 90°)
+ angle = acute_angle
+ else if(dx < 0 && dy > 0)
+ // NW quadrant (270° to 360°)
+ angle = 360 - acute_angle
+ else if(dx > 0 && dy < 0)
+ // SE quadrant (90° to 180°)
+ angle = 180 - acute_angle
+ else // dx < 0 && dy < 0
+ // SW quadrant (180° to 270°)
+ angle = 180 + acute_angle
+
+ // Normalize
+ while(angle < 0)
+ angle += 360
+ while(angle >= 360)
+ angle -= 360
+
+ return angle
+
+/obj/effect/overlay/vision_cone/proc/get_turf_perpendicular(turf/start, direction, offset)
+ // Get turf perpendicular to the facing direction
+ var/perp_dir
+ var/abs_offset = abs(offset)
+
+ switch(direction)
+ if(NORTH)
+ perp_dir = offset > 0 ? EAST : WEST
+ if(SOUTH)
+ perp_dir = offset > 0 ? WEST : EAST
+ if(EAST)
+ perp_dir = offset > 0 ? SOUTH : NORTH
+ if(WEST)
+ perp_dir = offset > 0 ? NORTH : SOUTH
+ if(NORTHEAST)
+ if(offset > 0)
+ perp_dir = SOUTHEAST
+ else
+ perp_dir = NORTHWEST
+ if(NORTHWEST)
+ if(offset > 0)
+ perp_dir = NORTHEAST
+ else
+ perp_dir = SOUTHWEST
+ if(SOUTHEAST)
+ if(offset > 0)
+ perp_dir = NORTHEAST
+ else
+ perp_dir = SOUTHWEST
+ if(SOUTHWEST)
+ if(offset > 0)
+ perp_dir = NORTHWEST
+ else
+ perp_dir = SOUTHEAST
+
+ var/turf/result = start
+ for(var/i = 1 to abs_offset)
+ result = get_step(result, perp_dir)
+ if(!result)
+ return null
+
+ return result
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index f12c94bed07..b81d110b190 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -736,6 +736,16 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
playsound(hit_atom, 'sound/weapons/genhit.ogg',volume, TRUE, -1)
else
playsound(hit_atom, 'sound/weapons/throwtap.ogg', 1, volume, -1)
+
+ // HOSTILE MOB DETECTION: Alert nearby hostile mobs about thrown item
+ if(throwingdatum && throwingdatum.thrower)
+ for(var/mob/living/simple_animal/hostile/H in range(7, src))
+ if(H.stat == DEAD || H.ckey)
+ continue
+ // Only alert mobs that can actually SEE the thrown item
+ if(!can_see(H, src, 7))
+ continue
+ H.detect_thrown_item(src, throwingdatum.thrower)
return hit_atom.hitby(src, 0, itempush, throwingdatum=throwingdatum)
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index e7da571b701..37a44ec4eb6 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -78,30 +78,54 @@
assemble_baseturfs()
- levelupdate()
+ // OPTIMIZATION: Merge levelupdate() and Entered() into single loop to avoid iterating objects twice
+ for(var/atom/movable/AM in src)
+ Entered(AM)
+ // Handle level 1 object hiding (was in levelupdate())
+ if(isobj(AM))
+ var/obj/O = AM
+ if(O.level == 1 && (O.flags_1 & INITIALIZED_1))
+ O.hide(src.intact)
+
if(smooth)
queue_smooth(src)
- visibilityChanged()
+
+ // OPTIMIZATION: Skip camera updates during mapload - batched update happens after all turfs init
+ if(!mapload)
+ visibilityChanged()
if(initial(opacity)) // Could be changed by the initialization of movable atoms in the turf.
base_opacity = initial(opacity)
directional_opacity = ALL_CARDINALS
- for(var/atom/movable/AM in src)
- Entered(AM)
-
+ // Cache area lookup - used multiple times below
var/area/A = loc
+
if(!IS_DYNAMIC_LIGHTING(src) && IS_DYNAMIC_LIGHTING(A))
add_overlay(/obj/effect/fullbright)
else
if(A.outdoors == TRUE)
sunlight_state = SUNLIGHT_SOURCE
- switch(sunlight_state)
- if(SUNLIGHT_SOURCE)
- setup_sunlight_source()
- if(SUNLIGHT_BORDER)
- border_neighbors = null
- smooth_sunlight_border()
+ // OPTIMIZATION: Defer sunlight smoothing during mapload - batched after all turfs exist
+ if(mapload)
+ // Store state for batch processing, but skip the expensive neighbor checks
+ if(sunlight_state == SUNLIGHT_SOURCE)
+ vis_contents += SSnightcycle.sunlight_source_object
+ luminosity = 1
+ // Mark neighbors, but don't smooth yet
+ for(var/dir in GLOB.alldirs)
+ var/turf/neighbor = get_step(src, dir)
+ if(!neighbor || !neighbor.type || neighbor?.sunlight_state && neighbor?.sunlight_state != NO_SUNLIGHT)
+ continue
+ neighbor.sunlight_state = SUNLIGHT_BORDER
+ else
+ // Not mapload, do normal smoothing
+ switch(sunlight_state)
+ if(SUNLIGHT_SOURCE)
+ setup_sunlight_source()
+ if(SUNLIGHT_BORDER)
+ border_neighbors = null
+ smooth_sunlight_border()
if(requires_activation)
CALCULATE_ADJACENT_TURFS(src)
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index bf672c49741..3df0dddf80e 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -189,3 +189,5 @@
/// If the client is currently under the restrictions of the interview system
var/interviewee = FALSE
var/is_fullscreen = 0
+ /// Cached hash of current verb list to prevent unnecessary rebuilds
+ var/cached_verb_hash = null
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 192a0418d0e..91eb27014a2 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -358,7 +358,9 @@ GLOBAL_LIST_INIT(warning_ckeys, list())
// Initialize tgui panel
tgui_panel.initialize()
- // Initialize statbrowser properly
+ // Initialize statbrowser on first client connection
+ // This only runs once per connection - body switches (ghosting/re-entering) only refresh verbs
+ // Use "Fix Statpanel" verb if issues occur during play
load_statbrowser()
@@ -1110,11 +1112,16 @@ GLOBAL_LIST_EMPTY(every_fucking_sound_file)
return prefs.pref_species.mutant_bodyparts[part_name] || (part_name in GLOB.unlocked_mutant_parts)
/// compiles a full list of verbs and sends it to the browser
-/client/proc/init_verbs()
+/// Only rebuilds if verbs have changed to prevent unnecessary statpanel refreshes
+/client/proc/init_verbs(force = FALSE)
if(IsAdminAdvancedProcCall())
return
+
+ // Build verb list and calculate hash
var/list/verblist = list()
- verb_tabs.Cut()
+ var/list/new_verb_tabs = list()
+ var/verb_hash = ""
+
for(var/thing in (verbs + mob?.verbs))
var/procpath/verb_to_init = thing
if(!verb_to_init)
@@ -1123,8 +1130,17 @@ GLOBAL_LIST_EMPTY(every_fucking_sound_file)
continue
if(!istext(verb_to_init.category))
continue
- verb_tabs |= verb_to_init.category
+ new_verb_tabs |= verb_to_init.category
verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name)
+ verb_hash += "[verb_to_init.category]:[verb_to_init.name];"
+
+ // Only rebuild if verbs have changed or forced
+ if(!force && verb_hash == cached_verb_hash)
+ return // Verbs haven't changed, skip rebuild
+
+ // Update cache and send to browser
+ cached_verb_hash = verb_hash
+ verb_tabs = new_verb_tabs
src << output("[url_encode(json_encode(verb_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs")
/client/proc/check_panel_loaded()
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index 63d35322703..31302561fcb 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -311,7 +311,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
set name = "Fix Stat Panel"
set hidden = TRUE
- init_verbs()
+ init_verbs(force = TRUE)
/client/proc/GetOOCName()
return key
diff --git a/code/modules/jobs/job_types/bos.dm b/code/modules/jobs/job_types/bos.dm
index 4ea7d5b8162..5aee2324a42 100644
--- a/code/modules/jobs/job_types/bos.dm
+++ b/code/modules/jobs/job_types/bos.dm
@@ -621,7 +621,10 @@ Senior Scribe
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_rca_convert)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_convert)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_helm_convert)
- H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes)
+ if(!H.mind.learned_recipes)
+ H.mind.learned_recipes = list()
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
ADD_TRAIT(H, TRAIT_CHEMWHIZ, src)
ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src)
ADD_TRAIT(H, TRAIT_CYBERNETICIST, src)
@@ -721,7 +724,10 @@ Scribe
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_rca_convert)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_convert)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_helm_convert)
- H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes)
+ if(!H.mind.learned_recipes)
+ H.mind.learned_recipes = list()
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
ADD_TRAIT(H, TRAIT_CHEMWHIZ, src)
ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src)
ADD_TRAIT(H, TRAIT_CYBERNETICIST, src)
diff --git a/code/modules/jobs/job_types/eastwood.dm b/code/modules/jobs/job_types/eastwood.dm
index 9a7493c8d33..f386212e0cd 100644
--- a/code/modules/jobs/job_types/eastwood.dm
+++ b/code/modules/jobs/job_types/eastwood.dm
@@ -705,11 +705,15 @@ Mayor
/obj/item/storage/firstaid/regular,
/obj/item/clothing/accessory/armband/medblue
)
+
/datum/outfit/job/den/f13dendoc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
..()
if(visualsOnly)
return
- H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes)
+ if(!H.mind.learned_recipes)
+ H.mind.learned_recipes = list()
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
H.mind.teach_crafting_recipe(/datum/crafting_recipe/pico_manip)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/super_matter_bin)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/phasic_scanning)
diff --git a/code/modules/jobs/job_types/holiday.dm b/code/modules/jobs/job_types/holiday.dm
index a15a9483665..a196b6f7dad 100644
--- a/code/modules/jobs/job_types/holiday.dm
+++ b/code/modules/jobs/job_types/holiday.dm
@@ -705,11 +705,15 @@ Mayor
/obj/item/storage/firstaid/regular,
/obj/item/clothing/accessory/armband/medblue
)
+
/datum/outfit/job/denholiday/f13dendoc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
..()
if(visualsOnly)
return
- H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes)
+ if(!H.mind.learned_recipes)
+ H.mind.learned_recipes = list()
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic
+ H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced
H.mind.teach_crafting_recipe(/datum/crafting_recipe/pico_manip)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/super_matter_bin)
H.mind.teach_crafting_recipe(/datum/crafting_recipe/phasic_scanning)
@@ -719,6 +723,7 @@ Mayor
ADD_TRAIT(H, TRAIT_GENERIC, src)
ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src)
+
/datum/outfit/loadout/rescueranger
name = "Search and Rescue"
backpack_contents = list(/obj/item/clothing/head/f13/police/sergeant = 1,
diff --git a/code/modules/keybindings/keybind/movement.dm b/code/modules/keybindings/keybind/movement.dm
index 0f3199ef6a8..b409f1d7772 100644
--- a/code/modules/keybindings/keybind/movement.dm
+++ b/code/modules/keybindings/keybind/movement.dm
@@ -219,3 +219,19 @@
var/mob/M = user.mob
M.tilt_left()
return TRUE
+
+/datum/keybinding/mob/toggle_sneak
+ hotkey_keys = list("K")
+ name = "toggle_sneak"
+ full_name = "Toggle Sneak Mode"
+ description = "Enter/exit sneak mode. Shows enemy vision cones and reduces sound."
+ category = CATEGORY_MOVEMENT
+
+/datum/keybinding/mob/toggle_sneak/down(client/user)
+ . = ..()
+ if(.)
+ return
+ var/mob/living/carbon/human/H = user.mob
+ if(istype(H))
+ H.toggle_sneak_mode()
+ return TRUE
diff --git a/code/modules/lighting/lighting_setup.dm b/code/modules/lighting/lighting_setup.dm
index 5086b0c9d29..136f953bf2a 100644
--- a/code/modules/lighting/lighting_setup.dm
+++ b/code/modules/lighting/lighting_setup.dm
@@ -10,4 +10,3 @@
new/atom/movable/lighting_object(T)
CHECK_TICK
- CHECK_TICK
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index bb06bf85bb1..cd1302a6fad 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -437,7 +437,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
// Check if mind.current exists and isn't being deleted
if(!mind.current || QDELETED(mind.current))
- to_chat(src, span_warning("You have no body."))
+ to_chat(src, span_warning("You have no body. Your corpse may have been destroyed, or you may be too far from respawn."))
return
// Check if we can re-enter
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index e55e87ebd9b..a3018d8007b 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -495,6 +495,7 @@
W.plane = initial(W.plane)
SetNextAction(0)
update_equipment_speed_mods() // In case cuffs ever change speed
+ update_mobility() //update mobility flags so we can use items/move properly after removing restraints
/mob/living/carbon/proc/clear_cuffs(obj/item/I, cuff_break)
if(!I.loc || buckled)
@@ -521,6 +522,7 @@
legcuffed = null
I.dropped(src)
update_inv_legcuffed()
+ update_mobility() //update mobility flags so we can use items/move properly after removing restraints
return
else
dropItemToGround(I)
@@ -960,6 +962,7 @@
update_action_buttons_icon() //some of our action buttons might be unusable when we're handcuffed.
update_inv_handcuffed()
update_hud_handcuffed()
+ update_mobility() //update mobility flags so we can use items again after breaking free
/mob/living/carbon/proc/can_revive(ignore_timelimit = FALSE, maximum_brute_dam = MAX_REVIVE_BRUTE_DAMAGE, maximum_fire_dam = MAX_REVIVE_FIRE_DAMAGE, ignore_heart = FALSE)
//var/tlimit = DEFIB_TIME_LIMIT * 10
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 47d746ebb41..c5d76c3edc4 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -16,6 +16,13 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0)
var/movement_fatigue = 0
var/last_armor_warning_time = 0
COOLDOWN_DECLARE(movement_fatigue_recovery)
+
+ // Sneaking mode system
+ var/sneaking = FALSE // Is the player in sneak mode?
+ var/mutable_appearance/sneak_indicator = null // Ninja icon overlay above head
+ var/list/visible_vision_cones = list() // List of vision cone overlays we're showing
+ var/vision_cone_timer = null // Timer for periodic cone updates
+ var/previous_move_intent = null // Saves movement speed before sneak mode
/mob/living/carbon/human/Initialize()
add_verb(src, /mob/living/proc/mob_sleep)
@@ -82,22 +89,10 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0)
gloves = null
shoes = null
- var/result = ..()
-
QDEL_NULL(physiology)
- if(client && client.screen && client.screen.len)
- addtimer(CALLBACK(src, PROC_REF(defer_screen_cleanup)), 0, TIMER_DELETE_ME)
-
- return result
+ return ..()
-/mob/living/carbon/human/proc/defer_screen_cleanup()
- // Called next tick to avoid GC spike from screen deletion
- // Clears all screen objects attached to this mob's client
- if(!src || QDELETED(src) || !client)
- return
- if(client.screen && client.screen.len)
- QDEL_LIST(client.screen)
/mob/living/carbon/human/prepare_data_huds()
//Update med hud images...
@@ -278,6 +273,356 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0)
ghostize()
qdel(src)
+// Calculate movement sound level for rear detection by hostile mobs
+// Returns a multiplier based on movement speed and armor weight
+/mob/living/carbon/human/proc/get_movement_sound_level()
+ // Base sound level: 1.0 for walking, 2.0 for running
+ var/base_movement = 1.0
+ if(m_intent == MOVE_INTENT_RUN)
+ base_movement = 2.0
+
+ // Calculate armor weight modifier from slowdown values
+ var/armor_slowdown = 0
+ if(wear_suit)
+ armor_slowdown += wear_suit.slowdown
+ if(shoes)
+ armor_slowdown += shoes.slowdown
+ if(head)
+ armor_slowdown += head.slowdown
+
+ // Light armor (low slowdown) makes you quieter, heavy armor makes you louder
+ // 0 slowdown = 0.5x (very quiet), 0.25 slowdown = 1.0x (normal), 0.5+ slowdown = increasingly loud
+ var/armor_multiplier = 0.5 + (armor_slowdown * 2)
+
+ var/sound_level = base_movement * armor_multiplier
+
+ // Sneaking no longer reduces sound - it only shows vision cones
+
+ return sound_level
+
+// Toggle sneak mode when K is pressed
+/mob/living/carbon/human/verb/toggle_sneak_mode()
+ set name = "Toggle Sneak"
+ set category = "IC"
+ set hidden = TRUE // Only accessible via keybind
+
+ if(stat != CONSCIOUS)
+ return
+
+ sneaking = !sneaking
+
+ // Update assassination button availability
+ for(var/datum/action/cooldown/assassinate/A in actions)
+ A.UpdateButtonIcon()
+
+ if(sneaking)
+ // Add movement slowdown (2x slower)
+ add_movespeed_modifier(/datum/movespeed_modifier/sneak_mode)
+ // Entered sneak mode
+ to_chat(src, span_notice("You enter sneak mode. Enemy vision cones are now visible."))
+
+ // Create ninja icon as overlay above head
+ if(!sneak_indicator)
+ var/obj/effect/overlay/sneak_icon/indicator_obj = new()
+ sneak_indicator = mutable_appearance(indicator_obj.icon, indicator_obj.icon_state, indicator_obj.layer)
+ sneak_indicator.alpha = 102 // 60% transparent
+ sneak_indicator.pixel_y = 20 // Above head
+ sneak_indicator.transform = matrix().Scale(0.7, 0.7) // 30% smaller
+ sneak_indicator.appearance_flags = RESET_COLOR | TILE_BOUND | PIXEL_SCALE
+ add_overlay(sneak_indicator)
+
+ // Start showing vision cones
+ update_vision_cones()
+
+ // Start periodic refresh timer (every 1 second) to catch mob deaths and lighting changes
+ vision_cone_timer = addtimer(CALLBACK(src, PROC_REF(update_vision_cones)), 10, TIMER_STOPPABLE | TIMER_LOOP)
+
+ // Save current movement intent and auto-switch to walk if running
+ previous_move_intent = m_intent
+ if(m_intent == MOVE_INTENT_RUN)
+ toggle_move_intent()
+ to_chat(src, span_warning("You automatically switch to walking."))
+ else
+ // Remove movement slowdown
+ remove_movespeed_modifier(/datum/movespeed_modifier/sneak_mode)
+ // Exited sneak mode
+ to_chat(src, span_notice("You exit sneak mode."))
+
+ // Restore previous movement intent if it was different
+ if(previous_move_intent && previous_move_intent != m_intent)
+ toggle_move_intent()
+ to_chat(src, span_notice("Movement speed restored to [previous_move_intent]."))
+ previous_move_intent = null
+
+ // Stop periodic refresh timer
+ if(vision_cone_timer)
+ deltimer(vision_cone_timer)
+ vision_cone_timer = null
+
+ // Remove ninja icon overlay
+ if(sneak_indicator)
+ cut_overlay(sneak_indicator)
+ sneak_indicator = null
+
+ // Clear all vision cones
+ clear_vision_cones()
+
+// Force exit sneak mode (used when exhausted or other forced exit)
+/mob/living/carbon/human/proc/force_exit_sneak_mode()
+ if(!sneaking)
+ return
+
+ sneaking = FALSE
+
+ // Update assassination button
+ for(var/datum/action/cooldown/assassinate/A in actions)
+ A.UpdateButtonIcon()
+
+ // Remove movement slowdown
+ remove_movespeed_modifier(/datum/movespeed_modifier/sneak_mode)
+
+ // Stop periodic refresh timer
+ if(vision_cone_timer)
+ deltimer(vision_cone_timer)
+ vision_cone_timer = null
+
+ // Remove ninja icon overlay
+ if(sneak_indicator)
+ cut_overlay(sneak_indicator)
+ sneak_indicator = null
+
+ // Clear all vision cones
+ clear_vision_cones()
+
+ // Don't restore previous move intent when forced out
+ previous_move_intent = null
+
+// Clean up when player dies/disconnects
+/mob/living/carbon/human/death()
+ . = ..( )
+ if(sneaking)
+ force_exit_sneak_mode()
+
+// Update visible vision cones for nearby hostile mobs
+// TWO-LAYER SYSTEM:
+// Layer 1: ACTIVATION - Based on mob's base vision_range (ignores YOUR penalties)
+// Layer 2: DISPLAY - Shows ACTUAL detection range (includes YOUR darkness/direction penalties)
+/mob/living/carbon/human/proc/update_vision_cones()
+ if(!sneaking || !client)
+ return
+
+ // Clear old cones
+ clear_vision_cones()
+
+ // ACTIVATION RANGE: Find all hostile mobs within their BASE vision range
+ // This ignores YOUR darkness penalties - it's based on THEIR potential awareness
+ for(var/mob/living/simple_animal/hostile/H in view(15, src)) // Extended view to catch edge cases
+ if(H.stat == DEAD || H.ckey)
+ continue
+
+ // Check if they're hostile to us
+ if(H.faction_check_mob(src))
+ continue // Friendly, don't show cone
+
+ // Hide cones if mob is actively targeting the player
+ if(H.target == src)
+ continue // Aggroed onto us, hide cone
+
+ var/actual_distance = get_dist(src, H)
+
+ // ACTIVATION TRIGGER: Use mob's BASE vision_range (9 tiles typically)
+ // This is their "awareness bubble" - you can see their cone when you're in it
+ // EVEN IF they can't see you yet due to darkness
+ var/activation_range = H.vision_range // Base range, no penalties
+
+ // Also activate if we're within sound detection range
+ // Use the LARGER center range (3x multiplier) for activation
+ var/max_sound_range = 0
+ if(last_move_time && (world.time - last_move_time) <= 20)
+ var/sound_cone = H.get_sound_cone(src)
+ if(sound_cone)
+ var/sound_level = get_movement_sound_level()
+ max_sound_range = round(sound_level * 3) // Use center multiplier (largest)
+
+ // Show cone if within activation range OR sound range
+ if(actual_distance <= activation_range || (max_sound_range > 0 && actual_distance <= max_sound_range))
+ show_vision_cone_for_mob(H)
+
+// Show vision cone with ACTUAL detection range (affected by darkness/direction/sound)
+// DISPLAY LAYER: Shows how far they can ACTUALLY detect you
+// PIE CHART SECTORS - Creates 6 clean non-overlapping 60° sectors
+/mob/living/carbon/human/proc/show_vision_cone_for_mob(mob/living/simple_animal/hostile/H)
+ if(!H || !sneaking)
+ return
+
+ if(H.stat == DEAD)
+ return
+
+ var/mob_dir = H.dir
+ if(!mob_dir || mob_dir == 0)
+ return
+
+ // Use the correct vision range function based on low-light vision
+ var/effective_range = 0
+ if(H.has_low_light_vision)
+ effective_range = H.get_effective_vision_range_lowlight(src)
+ else
+ effective_range = H.get_effective_vision_range(src)
+
+ var/actual_distance = get_dist(src, H)
+ var/your_cone = H.get_vision_cone(src)
+ var/currently_detected = (actual_distance <= effective_range && effective_range > 0)
+
+ // Calculate BOTH sound ranges (center and peripheral have different multipliers)
+ var/sound_level = 0.0
+ var/sound_center_range = 0
+ var/sound_peripheral_range = 0
+
+ if(last_move_time && (world.time - last_move_time) <= 20)
+ // Get movement sound level
+ sound_level = get_movement_sound_level()
+
+ // Always calculate ranges when moving (shows potential detection from any angle)
+ // The color/alpha logic will indicate whether detection is actually relevant
+ sound_center_range = round(sound_level * 3) // Rear center: sound travels clearly
+ sound_peripheral_range = round(sound_level * 2.5) // Rear peripheral: slightly muffled
+
+ // ============================================
+ // CENTERED SECTOR LAYOUT (6 slices, 60° each)
+ // ============================================
+ // Sectors are CENTERED on cardinal directions:
+ //
+ // FRONT CENTER (RED): -30° to +30° from facing
+ // RIGHT PERIPHERAL (YELLOW): 30° to 90°
+ // RIGHT REAR (CYAN): 90° to 150°
+ // REAR CENTER (GRAY): 150° to 210° (±30° from opposite)
+ // LEFT REAR (CYAN): 210° to 270°
+ // LEFT PERIPHERAL (YELLOW): 270° to 330°
+ // ============================================
+
+ // Calculate ranges for each sector
+ var/front_range = H.vision_range
+ var/front_alpha = 100 // Medium opacity for vision detection
+
+ if(your_cone == CONE_FRONT)
+ front_range = effective_range
+ if(currently_detected)
+ front_alpha = 180 // Brighten significantly when detected
+ else
+ front_alpha = 100
+ else
+ var/turf/front_check = get_step(H, mob_dir)
+ if(front_check)
+ var/light_amount = front_check.get_lumcount()
+ if(light_amount >= 0.5)
+ front_range = H.vision_range
+ else if(light_amount >= 0.2)
+ front_range = max(round(H.vision_range * 0.6), 3)
+ else
+ front_range = max(round(H.vision_range * 0.4), 3)
+
+ // Apply stealth boy penalty consistently with darkness
+ if(alpha < 100) // Cloaked - reduce range to 20%
+ front_range = max(round(front_range * 0.2), 1)
+
+ front_alpha = 100
+
+ var/peripheral_range = max(round(front_range * 0.6), 2)
+ var/peripheral_alpha = 70 // Lower opacity than front cone
+ if(your_cone == CONE_PERIPHERAL)
+ peripheral_range = effective_range
+ if(currently_detected)
+ peripheral_alpha = 150 // Brighten when detected
+ else
+ peripheral_alpha = 70
+ else
+ peripheral_alpha = 70
+
+ var/rear_peripheral_range = 3
+ var/rear_peripheral_alpha = 60 // Light opacity for sound detection
+
+ // Show sound detection ranges when moving (always show potential range for visibility)
+ // Use alpha to indicate whether ACTUALLY detected
+ // Use PERIPHERAL multiplier (2.5x) for cyan zones
+ if(sound_peripheral_range > 0)
+ rear_peripheral_range = sound_peripheral_range
+ var/sound_cone = H.get_sound_cone(src)
+ if(actual_distance <= sound_peripheral_range && sound_cone == SOUND_REAR_PERIPHERAL)
+ rear_peripheral_alpha = 130 // Brighten when detected in sound zone
+ else if(actual_distance <= sound_peripheral_range && sound_cone)
+ rear_peripheral_alpha = 100 // Within range but wrong zone
+ else
+ rear_peripheral_alpha = 60 // Light opacity base
+
+ var/rear_range = 3
+ var/rear_color = "#808080" // Gray for sound zones
+ var/rear_alpha = 60 // Light opacity for sound detection
+
+ // Show sound detection ranges when moving (always show potential range for visibility)
+ // Use alpha to indicate whether ACTUALLY detected
+ // Use CENTER multiplier (3x) for gray zone - sound travels clearly from behind
+ if(sound_center_range > 0)
+ rear_range = sound_center_range
+
+ // Keep gray color for sound zones (no color change needed)
+ rear_color = "#808080"
+
+ // Alpha: Brightness indicates detection state
+ var/sound_cone = H.get_sound_cone(src)
+ if(actual_distance <= sound_center_range && sound_cone == SOUND_REAR_CENTER)
+ rear_alpha = 130 // Brighten when detected in sound zone
+ else if(actual_distance <= sound_center_range && sound_cone)
+ rear_alpha = 100 // Within range but wrong zone
+ else
+ rear_alpha = 60 // Light opacity base
+
+ // Create 6 sectors, each 60° wide, CENTERED on cardinal/diagonal directions
+ // Each sector covers exactly 60° with NO overlap
+ // Vision zones use "red" sprite, sound zones use "blurry" sprite
+
+ // FRONT CENTER (RED): -30° to +30° from facing (wraps around 0°)
+ create_sector(H, mob_dir, -30, 30, front_range, "#FF0000", front_alpha, "red")
+
+ // RIGHT PERIPHERAL (YELLOW): 30° to 90° from facing
+ create_sector(H, mob_dir, 30, 90, peripheral_range, "#FFCC00", peripheral_alpha, "red")
+
+ // RIGHT REAR (GRAY): 90° to 150° from facing - sound detection
+ create_sector(H, mob_dir, 90, 150, rear_peripheral_range, "#808080", rear_peripheral_alpha, "blurry")
+
+ // REAR CENTER (GRAY): 150° to 210° from facing (±30° from opposite) - sound detection
+ create_sector(H, mob_dir, 150, 210, rear_range, rear_color, rear_alpha, "blurry")
+
+ // LEFT REAR (GRAY): 210° to 270° from facing - sound detection
+ create_sector(H, mob_dir, 210, 270, rear_peripheral_range, "#808080", rear_peripheral_alpha, "blurry")
+
+ // LEFT PERIPHERAL (YELLOW): 270° to 330° from facing
+ create_sector(H, mob_dir, 270, 330, peripheral_range, "#FFCC00", peripheral_alpha, "red")
+
+// Create a sector between two angles (in degrees, clockwise from facing direction)
+// Angles can be negative (e.g., -30 to 30 for front-centered cone that wraps around 0°)
+/mob/living/carbon/human/proc/create_sector(mob/living/simple_animal/hostile/H, base_dir, start_angle, end_angle, sector_range, sector_color, sector_alpha, icon_state_name = "white")
+ if(sector_range <= 0)
+ return
+
+ var/obj/effect/overlay/vision_cone/cone = new /obj/effect/overlay/vision_cone(get_turf(H))
+ cone.viewer = src
+ cone.setup_sector(H, base_dir, start_angle, end_angle, sector_range, icon_state_name)
+ cone.alpha = sector_alpha
+
+ if(cone.generate_cone_image())
+ for(var/image/img in cone.cone_images)
+ img.color = sector_color
+ img.alpha = sector_alpha
+
+ visible_vision_cones += cone
+
+// Clear all vision cone overlays
+/mob/living/carbon/human/proc/clear_vision_cones()
+ for(var/obj/effect/overlay/vision_cone/cone in visible_vision_cones)
+ // Tiles are deleted when the cone is deleted
+ qdel(cone)
+ visible_vision_cones.Cut()
+
/mob/living/carbon/human/Topic(href, href_list)
if(usr.canUseTopic(src, BE_CLOSE, NO_DEXTERY))
if(href_list["embedded_object"])
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index 082318132e3..61770079b9e 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -1,3 +1,7 @@
+// Sneak mode movement slowdown - makes player move 2x slower
+/datum/movespeed_modifier/sneak_mode
+ multiplicative_slowdown = 1 // 2x slower (base movement + 1 = 2x the delay)
+
/mob/living/carbon/human/get_movespeed_modifiers()
var/list/considering = ..()
if(HAS_TRAIT(src, TRAIT_IGNORESLOWDOWN))
@@ -149,6 +153,26 @@
for(var/obj/item/I in held_items)
accident(I)
DefaultCombatKnockdown(80)
+
+ // Update vision cones and drain stamina when player moves in sneak mode
+ if(. && sneaking)
+ update_vision_cones() // Update vision cones when we move
+ // Drain sprint buffer while sneaking and moving
+ if(has_gravity(loc) && CHECK_ALL_MOBILITY(src, MOBILITY_MOVE|MOBILITY_STAND))
+ doSprintBufferRegen(FALSE) // Reset idle timer
+ sprint_idle_time = 0
+ var/sneak_cost = 0.5 // 0.5 tiles of sprint buffer per tile moved
+ sprint_buffer = max(0, sprint_buffer - sneak_cost)
+ update_hud_sprint_bar() // Update sprint bar display
+
+ // Force out of sneak mode if sprint buffer depleted
+ if(sprint_buffer <= 0)
+ force_exit_sneak_mode()
+ to_chat(src, span_warning("You're too exhausted to continue sneaking!"))
+ // Update assassination button availability (position changed)
+ for(var/datum/action/cooldown/assassinate/A in actions)
+ A.UpdateButtonIcon()
+
if(shoes)
if(!lying && !buckled)
if(loc == NewLoc)
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 2f290fb3006..d80f0ada26c 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -17,7 +17,14 @@
if(bprv & BODYPART_LIFE_UPDATE_HEALTH)
updatehealth()
update_stamina()
- doSprintBufferRegen()
+
+ // Don't regenerate sprint buffer while sneaking
+ if(ishuman(src))
+ var/mob/living/carbon/human/H = src
+ if(!H.sneaking)
+ doSprintBufferRegen()
+ else
+ doSprintBufferRegen()
if(stat != DEAD)
handle_brain_damage()
@@ -503,7 +510,12 @@ GLOBAL_LIST_INIT(ballmer_windows_me_msg, list("Yo man, what if, we like, uh, put
//this updates all special effects: stun, sleeping, knockdown, druggy, stuttering, etc..
/mob/living/carbon/handle_status_effects()
..()
- if(getStaminaLoss()) //CIT CHANGE - prevents stamina regen while combat mode is active (Stam regen is currently enabled)
+ // Don't regenerate stamina while sneaking
+ if(ishuman(src))
+ var/mob/living/carbon/human/H = src
+ if(!H.sneaking && getStaminaLoss())
+ adjustStaminaLoss(!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL)
+ else if(getStaminaLoss()) //CIT CHANGE - prevents stamina regen while combat mode is active (Stam regen is currently enabled)
adjustStaminaLoss(!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL)
if(!(combat_flags & COMBAT_FLAG_HARD_STAMCRIT) && incomingstammult != 1)
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index d197898f6db..5cd97dde54c 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -75,7 +75,10 @@
return
if(stat == DEAD)
- say_dead(original_message)
+ // Only route to dead chat if the mob has a player (ckey)
+ // NPCs should not broadcast to dead chat
+ if(ckey)
+ say_dead(original_message)
return
if(check_emote(original_message, just_runechat = just_chat) || !can_speak_basic(original_message, ignore_spam))
diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
index a94c95d381e..bd7db98c0fc 100644
--- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
+++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm
@@ -436,7 +436,7 @@
// Flee like the bucking code
spawn(0)
- for(var/i in 1 to rand(6,10))
+ for(var/i in 1 to rand(9,13))
if(stat)
break
var/flee_dir = pick(NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST)
@@ -1580,7 +1580,7 @@
ride_move_delay = 2.5
young_type = /mob/living/simple_animal/cow/brahmin/nightstalker
food_types = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/gecko,
+ /obj/item/reagent_containers/food/snacks/meat/slab,
/obj/item/reagent_containers/food/snacks/f13/canned/dog
)
milk_reagent = /datum/reagent/toxin
diff --git a/code/modules/mob/living/simple_animal/hostile/bear.dm b/code/modules/mob/living/simple_animal/hostile/bear.dm
index 1c3be25596b..85126bf9405 100644
--- a/code/modules/mob/living/simple_animal/hostile/bear.dm
+++ b/code/modules/mob/living/simple_animal/hostile/bear.dm
@@ -177,6 +177,9 @@ mob/living/simple_animal/hostile/bear/butter/AttackingTarget() //Makes some atta
wound_bonus = 5
bare_wound_bonus = 25
faction = list("yaoguai")
+
+ // Mutant bear with enhanced night vision
+ has_low_light_vision = TRUE
/*mob/living/simple_animal/hostile/bear/yaoguai/frozen
name = "frozen yao guai"
diff --git a/code/modules/mob/living/simple_animal/hostile/bees.dm b/code/modules/mob/living/simple_animal/hostile/bees.dm
index fa6dd34db6d..ea3a6542629 100644
--- a/code/modules/mob/living/simple_animal/hostile/bees.dm
+++ b/code/modules/mob/living/simple_animal/hostile/bees.dm
@@ -311,7 +311,7 @@
/mob/living/simple_animal/hostile/poison/bees/short/Initialize()
. = ..()
- addtimer(CALLBACK(src, PROC_REF(death)), 50 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(death)), 50 SECONDS, TIMER_DELETE_ME)
/mob/living/simple_animal/hostile/poison/bees/short/frenly //these bees need to be frenly or they'd murder everyone
faction = list("neutral")
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm b/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm
index cefca9ac114..77ab8d33a73 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm
@@ -1,9 +1,10 @@
-/* IN THIS FILE:
--Securitron
--Sentry Bot
-*/
+// In this document: Securitron, Sentry Bot, and variants
-//Securitron TV Head jackass
+/////////////////
+// SECURITRON //
+/////////////////
+
+// BASE SECURITRON - TV-head wheeled robot, ranged burst fire
/mob/living/simple_animal/hostile/securitron
name = "securitron"
desc = "A pre-War type of securitron.
Extremely dangerous machine."
@@ -11,62 +12,86 @@
icon_state = "securitron"
icon_living = "securitron"
icon_dead = "securitron_dead"
+
+ mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
mob_armor = ARMOR_VALUE_ROBOT_SECURITY
- maxHealth = 100
+
+ maxHealth = 100
health = 100
- sentience_type = SENTIENCE_BOSS
+ move_to_delay = 2.75
+ turns_per_move = 5
stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT
+ sentience_type = SENTIENCE_BOSS
+
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+ harm_intent_damage = 8
+
+ aggro_vision_range = 7
+ vision_range = 8
+ robust_searching = TRUE
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
+ attack_verb_simple = "punches"
+ attack_sound = 'sound/weapons/punch1.ogg'
+
+ speak = list("Stop Right There Criminal.")
+ speak_chance = 1
+ emote_hear = list("Beeps.")
+ emote_taunt = list("readies its arm gun")
+ taunt_chance = 30
+
emp_flags = list(
MOB_EMP_STUN,
MOB_EMP_BERSERK,
MOB_EMP_DAMAGE,
MOB_EMP_SCRAMBLE
- )
- speak_chance = 1
- turns_per_move = 5
- environment_smash = 0
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- robust_searching = TRUE
- blood_volume = 0
+ )
+
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ 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)
+
+ faction = list("wastebot")
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
del_on_death = TRUE
healable = FALSE
- faction = list("wastebot")
- mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
- move_to_delay = 3.25
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 2
- //how far they pull back
+ blood_volume = 0
- minimum_distance = 5
- // how close you can get before they try to pull back
-
- aggro_vision_range = 7
- //tiles within they start attacking, doesn't count the mobs tile
-
- vision_range = 8
- //tiles within they start making noise, does count the mobs tile
+ move_resist = MOVE_FORCE_OVERPOWERING
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ loot = list(
+ /obj/effect/decal/cleanable/robot_debris,
+ /obj/item/stack/crafting/electronicparts/three
+ )
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement - wheels, no ladders
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
- emote_hear = list("Beeps.")
- speak = list("Stop Right There Criminal.")
- harm_intent_damage = 8
- melee_damage_lower = 5
- melee_damage_upper = 10
- extra_projectiles = 2
- auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure ranged - burst fire pistol
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
ranged_ignores_vision = TRUE
- attack_verb_simple = "punches"
- attack_sound = "punch"
- a_intent = "harm"
- 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)
+ auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW
+ extra_projectiles = 2
projectiletype = /obj/item/projectile/bullet/c9mm/simple
projectilesound = 'sound/f13weapons/varmint_rifle.ogg'
- emote_taunt = list("readies its arm gun")
- check_friendly_fire = TRUE
- ranged = TRUE
- move_resist = MOVE_FORCE_OVERPOWERING
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_LIGHT_VOLUME),
@@ -78,24 +103,38 @@
SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
)
- can_z_move = TRUE
- z_move_delay = 30
+/mob/living/simple_animal/hostile/securitron/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
-/mob/living/simple_animal/hostile/securitron/nsb //NSB + Raider Bunker specific
- name = "Securitron"
- faction = list("raider")
- obj_damage = 300
- retreat_distance = 0 //perish, mortal
+/mob/living/simple_animal/hostile/securitron/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+// Friendly fire resistance + defensive grenade release
/mob/living/simple_animal/hostile/securitron/bullet_act(obj/item/projectile/Proj)
if(!Proj)
CRASH("[src] securitron invoked bullet_act() without a projectile")
+
+ if(Proj.firer && istype(Proj.firer, /mob/living/simple_animal/hostile/securitron))
+ var/original_damage = Proj.damage
+ Proj.damage *= 0.2
+ . = ..()
+ Proj.damage = original_damage
+ return
+
+ // Chance to release defensive grenade when shot
if(prob(5) && health > 1)
var/flashbang_turf = get_turf(src)
if(!flashbang_turf)
- return
+ return ..()
var/obj/item/grenade/S
- switch(rand(1,10))
+ switch(rand(1, 10))
if(1)
S = new /obj/item/grenade/flashbang/sentry(flashbang_turf)
if(2)
@@ -104,14 +143,16 @@
S = new /obj/item/grenade/smokebomb(flashbang_turf)
visible_message(span_danger("\The [src] releases a defensive [S]!"))
S.preprime(user = null)
- ..()
+
+ return ..()
+// Death sequence - beeps then explodes
/mob/living/simple_animal/hostile/securitron/proc/do_death_beep()
playsound(src, 'sound/machines/triple_beep.ogg', 75, TRUE)
visible_message(span_warning("You hear an ominous beep coming from [src]!"), span_warning("You hear an ominous beep!"))
/mob/living/simple_animal/hostile/securitron/proc/self_destruct()
- explosion(src,1,2,4,4)
+ explosion(src, 1, 2, 4, 4)
/mob/living/simple_animal/hostile/securitron/death()
do_sparks(3, TRUE, src)
@@ -120,32 +161,40 @@
addtimer(CALLBACK(src, PROC_REF(self_destruct)), 4 SECONDS)
return ..()
-/mob/living/simple_animal/hostile/securitron/Aggro()
- . = ..()
- summon_backup(15)
+// NSB SECURITRON - raider bunker variant, doesn't retreat
+/mob/living/simple_animal/hostile/securitron/nsb
+ name = "Securitron"
+ faction = list("raider")
+ obj_damage = 300
+ can_ghost_into = FALSE
+
+ retreat_distance = null // Perish, mortal
+
+/////////////////
+// SENTRY BOT //
+/////////////////
-//Sentry Bot
+// BASE SENTRY BOT - heavy gatling laser platform
/mob/living/simple_animal/hostile/securitron/sentrybot
name = "sentry bot"
desc = "A pre-war military robot armed with a deadly gatling laser and covered in thick armor plating."
icon_state = "sentrybot"
icon_living = "sentrybot"
icon_dead = "sentrybot_dead"
- mob_armor = ARMOR_VALUE_ROBOT_SECURITY
- maxHealth = 150
+
+ mob_armor = ARMOR_VALUE_ROBOT_MILITARY
+
+ maxHealth = 150
health = 150
stat_attack = UNCONSCIOUS
- del_on_death = FALSE
+ del_on_death = FALSE // Explodes instead
+
melee_damage_lower = 24
melee_damage_upper = 55
- extra_projectiles = 2 //5 projectiles
- ranged_cooldown_time = 40 //brrrrrrrrrrrrt
- retreat_distance = 5
- minimum_distance = 5 // SENTRY bot, not run up to your face and magdump you bot
attack_verb_simple = "pulverizes"
attack_sound = 'sound/weapons/punch1.ogg'
- projectilesound = 'sound/weapons/laser.ogg'
- projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak
+
+ emote_taunt = list("spins its barrels")
emote_taunt_sound = list(
'sound/f13npc/sentry/taunt1.ogg',
'sound/f13npc/sentry/taunt2.ogg',
@@ -153,27 +202,38 @@
'sound/f13npc/sentry/taunt4.ogg',
'sound/f13npc/sentry/taunt5.ogg',
'sound/f13npc/sentry/taunt6.ogg'
- )
- emote_taunt = list("spins its barrels")
+ )
aggrosound = list(
'sound/f13npc/sentry/aggro1.ogg',
'sound/f13npc/sentry/aggro2.ogg',
'sound/f13npc/sentry/aggro3.ogg',
'sound/f13npc/sentry/aggro4.ogg',
'sound/f13npc/sentry/aggro5.ogg'
- )
+ )
idlesound = list(
'sound/f13npc/sentry/idle1.ogg',
'sound/f13npc/sentry/idle2.ogg',
'sound/f13npc/sentry/idle3.ogg',
'sound/f13npc/sentry/idle4.ogg'
- )
- var/warned = FALSE
+ )
+
loot = list(
/obj/effect/decal/cleanable/robot_debris,
/obj/item/stack/crafting/electronicparts/five,
/obj/item/stock_parts/cell/ammo/mfc
- )
+ )
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+ // Pure ranged - gatling laser
+ combat_mode = COMBAT_MODE_RANGED
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 40 // Brrrrrrrt
+ extra_projectiles = 2 // 3 shots per burst
+ projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -184,63 +244,31 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
+
+ var/warned = FALSE // For low health warning sound
/mob/living/simple_animal/hostile/securitron/sentrybot/Life()
- ..()
- if (!warned)
- if (health <= 50)
- warned = TRUE
- playsound(src, 'sound/f13npc/sentry/systemfailure.ogg', 75, FALSE)
-
-// Lil chew-chew
-/mob/living/simple_animal/hostile/securitron/sentrybot/chew
- name = "lil' chew-chew"
- desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser and covered in thick, oddly blue armor plating, the name Lil' Chew-Chew scratched onto it's front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity of a sentrie's back, a chute at the bottom of it - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!"
- extra_projectiles = 6
- health = 1000
- maxHealth = 1000 //CHONK
- obj_damage = 300
- retreat_distance = 0
- environment_smash = ENVIRONMENT_SMASH_RWALLS //wall-obliterator. perish.
- color = "#75FFE2"
- aggro_vision_range = 15
- flags_1 = PREVENT_CONTENTS_EXPLOSION_1 //cannot self-harm with it's explosion spam
-
-/mob/living/simple_animal/hostile/securitron/sentrybot/chew/bullet_act(obj/item/projectile/Proj)
- if(!Proj)
- CRASH("[src] sentrybot invoked bullet_act() without a projectile")
- if(prob(10) && health > 1)
- visible_message(span_danger("\The [src] releases a defensive explosive!"))
- explosion(get_turf(src),-1,-1,2, flame_range = 4) //perish, mortal - explosion size identical to craftable IED
- ..()
-
-/mob/living/simple_animal/hostile/securitron/sentrybot/chew/strong //For use as a main bunker boss. Essentially a mildly edited OverSEER port
- name = "big chew-chew"
- desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser firing high-penetration experimental lasers and covered in thick, dark blue armor plating, the name Big Chew-Chew scratched onto it's front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity of a sentrie's back, a chute at the bottom of it - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!"
- extra_projectiles = 4 //Fires a bit less
- health = 1500 //more HP than its smaller brother
- maxHealth = 1500
- armour_penetration = 1 //Punches harder
- move_to_delay = 3.0 //Is deceptively quick
- retreat_distance = 0 //Is going to punch you
- rapid_melee = 2 //Punches faster
- color = "#3444C8" //dark blue
- emp_flags = list() //no emp instakill for you
- projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak/chew/strong
+ . = ..()
+ if(!warned && health <= 50)
+ warned = TRUE
+ playsound(src, 'sound/f13npc/sentry/systemfailure.ogg', 75, FALSE)
-//Raider friendly Sentry bot
+// NSB SENTRY BOT - raider faction
/mob/living/simple_animal/hostile/securitron/sentrybot/nsb
name = "sentry bot"
obj_damage = 300
+ can_ghost_into = FALSE
-//Raider friendly Sentry bot with non-lethals
-/mob/living/simple_animal/hostile/securitron/sentrybot/nsb/riot //NSB + Raider Bunker specific.
+// NSB RIOT SENTRY - non-lethal breacher
+/mob/living/simple_animal/hostile/securitron/sentrybot/nsb/riot
name = "riot-control sentry bot"
desc = "A pre-war military robot armed with a modified breacher shotgun and covered in thick armor plating."
- projectilesound = 'sound/f13weapons/riot_shotgun.ogg'
- projectiletype = /obj/item/projectile/bullet/shotgun_beanbag
- retreat_distance = 0
+
+ retreat_distance = null // Gets right in your face
extra_projectiles = 0
+
+ projectiletype = /obj/item/projectile/bullet/shotgun_beanbag
+ projectilesound = 'sound/f13weapons/riot_shotgun.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(SHOTGUN_VOLUME),
@@ -252,36 +280,87 @@
SP_DISTANT_RANGE(SHOTGUN_RANGE_DISTANT)
)
-//Playable Sentrybot
+// PLAYABLE SENTRY BOT
/mob/living/simple_animal/hostile/securitron/sentrybot/playable
- health = 50 //El Beef
maxHealth = 50
+ health = 50
speed = 1
attack_verb_simple = "clamps"
+ see_in_dark = 8
+ environment_smash = ENVIRONMENT_SMASH_RWALLS
+ wander = FALSE
+ force_threshold = 15
+ anchored = FALSE
+ del_on_death = FALSE
+ ranged = FALSE
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
- ranged = FALSE
- see_in_dark = 8
- environment_smash = 2 //can smash walls
- wander = 0
- force_threshold = 15
-/mob/living/simple_animal/hostile/securitron/sentrybot/playable/death()
+// LIL' CHEW-CHEW - unique named boss
+/mob/living/simple_animal/hostile/securitron/sentrybot/chew
+ name = "lil' chew-chew"
+ desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser and covered in thick, oddly blue armor plating, the name Lil' Chew-Chew scratched onto its front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity's back with a chute at the bottom - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!"
+
+ mob_armor = ARMOR_VALUE_ROBOT_MILITARY_HEAVY
+
+ maxHealth = 1000
+ health = 1000
+ obj_damage = 300
+ extra_projectiles = 6
+
+ retreat_distance = null // Perish, mortal
+ environment_smash = ENVIRONMENT_SMASH_RWALLS
+
+ color = "#75FFE2"
+ aggro_vision_range = 15
+ can_ghost_into = FALSE
+
+ flags_1 = PREVENT_CONTENTS_EXPLOSION_1 // Can't self-harm with explosion spam
+
+/mob/living/simple_animal/hostile/securitron/sentrybot/chew/bullet_act(obj/item/projectile/Proj)
+ if(!Proj)
+ CRASH("[src] sentrybot invoked bullet_act() without a projectile")
+ if(prob(10) && health > 1)
+ visible_message(span_danger("\The [src] releases a defensive explosive!"))
+ explosion(get_turf(src), -1, -1, 2, flame_range = 4)
return ..()
-//Junkers
+// BIG CHEW-CHEW - bunker boss variant
+/mob/living/simple_animal/hostile/securitron/sentrybot/chew/strong
+ name = "big chew-chew"
+ desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser firing high-penetration experimental lasers and covered in thick, dark blue armor plating, the name Big Chew-Chew scratched onto its front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity's back with a chute at the bottom - it's safe to assume this robot is capable of explosions. Better watch out!"
+
+ maxHealth = 1500
+ health = 1500
+ extra_projectiles = 4 // Fires a bit less than lil chew-chew
+ armour_penetration = 1
+ move_to_delay = 2.75
+ rapid_melee = 2
+
+ retreat_distance = null // Is going to punch you
+
+ color = "#3444C8" // Dark blue
+
+ emp_flags = list() // No EMP instakill
+ projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak/chew/strong
+
+// SUICIDE SENTRY - rushes in and explodes
/mob/living/simple_animal/hostile/securitron/sentrybot/suicide
name = "explosive sentry bot"
desc = "A pre-war military robot armed with a deadly gatling laser and covered in thick armor plating. Don't get too close to this one, it looks like it's rigged to blow!"
+
maxHealth = 160
health = 160
color = "#B85C00"
+
+ // Rushes target
retreat_distance = null
minimum_distance = 1
/mob/living/simple_animal/hostile/securitron/sentrybot/suicide/AttackingTarget()
+ . = ..()
if(ishuman(target))
addtimer(CALLBACK(src, PROC_REF(do_death_beep)), 1 SECONDS)
addtimer(CALLBACK(src, PROC_REF(self_destruct)), 2 SECONDS)
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm b/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm
index 2e296230117..bcad5e9cd15 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm
@@ -1,8 +1,10 @@
-// In this document: Freaks, the centaur, abomination and horror.
+// In this document: Freaks - Centaur, Abomination, Horror
-// -------------------------------------
-// CENTAUR
+//////////////
+// CENTAUR //
+//////////////
+// BASE CENTAUR - FEV mutation, ranged poison spitter
/mob/living/simple_animal/hostile/centaur
name = "centaur"
desc = "The result of infection by FEV gone horribly wrong."
@@ -11,121 +13,228 @@
icon_living = "centaur"
icon_dead = "centaur_dead"
icon_gib = "centaur_g"
- tastes = list("sadness" = 1, "nastyness" = 1)
- can_ghost_into = TRUE
+
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ mob_armor = ARMOR_VALUE_CENTAUR
+
maxHealth = 80
- sentience_type = SENTIENCE_BOSS
health = 80
speed = 2
+ move_to_delay = 4 // Slower than average
+ turns_per_move = 5
+
+ melee_damage_lower = 4
+ melee_damage_upper = 20
harm_intent_damage = 8
- melee_damage_lower = 4 // damage range is punch min, average is 15 when in melee
- melee_damage_upper = 20
- ranged = TRUE
wound_bonus = 0
- footstep_type = FOOTSTEP_MOB_CRAWL
-
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+
+ aggro_vision_range = 7
+ vision_range = 7
robust_searching = TRUE
- move_to_delay = 6 //slower than average, but not a lot. //Needs to be slower than a protectron
- // m2d 4 = standard, less is fast, more is slower.
-
- retreat_distance = 0 // Mob doesn't retreat
- //how far they pull back
+ stat_attack = CONSCIOUS
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/human/centaur = 3,
+ /obj/item/stack/sheet/animalhide/human = 2,
+ /obj/item/stack/sheet/bone = 2
+ )
+
+ footstep_type = FOOTSTEP_MOB_CRAWL
+ tastes = list("sadness" = 1, "nastyness" = 1)
- minimum_distance = 0 //Mob pushes up to melee, leading with its ranged attacks to soften up player.
- // how close you can get before they try to pull back
-
- aggro_vision_range = 7 //Will start attacking within player sight, but gives wiggle room to avoid if moving slow and carefully
- //tiles within they start attacking
-
- vision_range = 7 //will start attacking within player sight, but like aggro gives wiggle room. So they just don't see players outside of 7 tiles and start screeching.
- //tiles within they start making noise
- turns_per_move = 5
speak_emote = list("growls")
emote_see = list("screeches", "screams", "howls", "bellows", "flails", "fidgets", "festers")
- a_intent = INTENT_HARM
attack_verb_simple = list("whipped", "whacked", "whomped", "wailed on", "smacked", "smashed", "bapped")
+ attack_sound = 'sound/f13npc/centaur/lash.ogg'
+
+ emote_taunt = list("grunts", "gurgles", "wheezes", "flops", "scrabbles")
+ emote_taunt_sound = list('sound/f13npc/centaur/taunt.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/f13npc/centaur/aggro1.ogg')
+ idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg')
+ death_sound = 'sound/f13npc/centaur/centaur_death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 20
- stat_attack = CONSCIOUS
- gold_core_spawnable = HOSTILE_SPAWN
+
faction = list("hostile", "supermutant")
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human/centaur = 3,
- /obj/item/stack/sheet/animalhide/human = 2,
- /obj/item/stack/sheet/bone = 2)
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+ sentience_type = SENTIENCE_BOSS
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Mixed combat - poison spit + melee
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 3
+ minimum_distance = 1
+
+ ranged_message = "spits poison"
+ ranged_cooldown_time = 3 SECONDS
projectiletype = /obj/item/projectile/neurotox
projectilesound = 'sound/f13npc/centaur/spit.ogg'
- emote_taunt_sound = list('sound/f13npc/centaur/taunt.ogg')
- emote_taunt = list("grunts", "gurgles", "wheezes", "flops", "scrabbles")
- taunt_chance = 30
- aggrosound = list('sound/f13npc/centaur/aggro1.ogg', )
- idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg')
- death_sound = 'sound/f13npc/centaur/centaur_death.ogg'
- attack_sound = 'sound/f13npc/centaur/lash.ogg'
+/mob/living/simple_animal/hostile/centaur/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/centaur/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - centaurs fight together
+/mob/living/simple_animal/hostile/centaur/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/centaur))
+ // Friendly fire from another centaur - take only 20% damage
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// NEUROTOXIN PROJECTILE
/obj/item/projectile/neurotox
- name = "spitball"
+ name = "poison spit"
damage = 25
icon_state = "toxin"
-/mob/living/simple_animal/hostile/centaur/strong // Mostly for FEV mutation
+/obj/item/projectile/neurotox/on_hit(atom/target)
+ . = ..()
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.reagents.add_reagent(/datum/reagent/toxin, 3)
+
+// STRONG CENTAUR - for FEV mutation event
+/mob/living/simple_animal/hostile/centaur/strong
maxHealth = 400
health = 400
stat_attack = UNCONSCIOUS
+
melee_damage_lower = 35
melee_damage_upper = 35
armour_penetration = 0.1
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
-/mob/living/simple_animal/pet/dog/centaur //Cutie
+// DOUG - friendly pet centaur
+/mob/living/simple_animal/pet/dog/centaur
name = "Doug"
- desc = "A docile centaur. Was brought here along with the warband, Isn't he adorable?"
+ desc = "A docile centaur. Was brought here along with the warband. Isn't he adorable?"
icon = 'icons/fallout/mobs/monsters/freaks.dmi'
icon_state = "centaur"
icon_living = "centaur"
icon_dead = "centaur_dead"
icon_gib = "centaur_g"
+
maxHealth = 200
health = 200
turns_per_move = 5
+
speak_emote = list("growls")
emote_see = list("screeches", "screams", "howls", "bellows", "flails", "fidgets", "festers")
+
idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg')
death_sound = 'sound/f13npc/centaur/centaur_death.ogg'
+
response_help_simple = "pet"
response_disarm_simple = "push"
response_harm_simple = "punch"
+/////////////////
+// ABOMINATION //
+/////////////////
-// -----------------------------------
-// ABOMINATION
-
+// BASE ABOMINATION - FEV nightmare, wall smasher
/mob/living/simple_animal/hostile/abomination
name = "abomination"
- desc = "A horrible fusion of man, animal, and something entirely different. It quakes and shudders, looking to be in an immense amount of pain. Blood and other fluids ooze from various gashes and lacerations on its body, punctuated by mouths that gnash and scream."
+ desc = "A horrible fusion of man, animal, and something entirely different. It quakes and shudders, looking to be in an immense amount of pain. Blood and other fluids oo ze from various gashes and lacerations on its body, punctuated by mouths that gnash and scream."
icon = 'icons/fallout/mobs/monsters/freaks.dmi'
icon_state = "abomination"
icon_living = "abomination"
icon_dead = "abomination_dead"
-
+
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ mob_armor = ARMOR_VALUE_ABOMINATION
+
maxHealth = 1000
health = 1000
+ speed = -0.5 // Fast
stat_attack = UNCONSCIOUS
- speed = -0.5
- harm_intent_damage = 8
+
melee_damage_lower = 30
melee_damage_upper = 40
+ harm_intent_damage = 8
armour_penetration = 0.1
-
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+
environment_smash = ENVIRONMENT_SMASH_RWALLS
robust_searching = TRUE
+
+ speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes")
attack_verb_simple = "eviscerates"
attack_sound = 'sound/weapons/bladeslice.ogg'
- speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes")
- var/static/list/abom_sounds
- deathmessage = "wails as its form shudders and violently comes to a stop."
death_sound = 'sound/voice/abomburning.ogg'
- despawns_when_lonely = FALSE // too ANGRY to despawn
+ deathmessage = "wails as its form shudders and violently comes to a stop."
+
+ faction = list("hostile")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE // Too ANGRY to despawn
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - wall smashing tank
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+ // Sound system
+ var/static/list/abom_sounds
+
+/mob/living/simple_animal/hostile/abomination/Initialize()
+ . = ..()
+ abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg')
+
+/mob/living/simple_animal/hostile/abomination/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(12)
+
+/mob/living/simple_animal/hostile/abomination/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/abomination/AttackingTarget()
. = ..()
@@ -134,89 +243,134 @@
var/choice = pick(1, 1, 2, 2, 3, 4)
H.reagents.add_reagent(/datum/reagent/toxin/FEV_solution, choice)
-/mob/living/simple_animal/hostile/abomination/Initialize()
+/mob/living/simple_animal/hostile/abomination/say(message, datum/language/language = null, list/spans = list(), sanitize, ignore_spam, forced = null, just_chat)
. = ..()
- abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg')
-
-/mob/living/simple_animal/hostile/abomination/say(message, datum/language/language = null, list/spans = list(), language, sanitize, ignore_spam, forced = null, just_chat)
- ..()
if(stat)
return
var/chosen_sound = pick(abom_sounds)
playsound(src, chosen_sound, 50, TRUE)
/mob/living/simple_animal/hostile/abomination/Life()
- ..()
+ . = ..()
if(stat)
return
if(prob(10))
var/chosen_sound = pick(abom_sounds)
playsound(src, chosen_sound, 70, TRUE)
-/mob/living/simple_animal/hostile/abomination/weak // For FEV mutation.
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES // So you don't break walls
+// WEAK ABOMINATION - for FEV mutation event
+/mob/living/simple_animal/hostile/abomination/weak
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES // Can't break walls
+
maxHealth = 500
health = 500
- harm_intent_damage = 8
+ speed = 2 // Slower
+
melee_damage_lower = 20
melee_damage_upper = 30
- speed = 2
-
+
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
-// ------------------------------------------
-// HORROR
+////////////
+// HORROR //
+////////////
+// BASE HORROR - weaker FEV fusion
/mob/living/simple_animal/hostile/abomhorror
name = "failed experiment"
desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain."
- speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes")
icon = 'icons/fallout/mobs/monsters/freaks.dmi'
icon_state = "horror"
icon_living = "horror"
icon_dead = "horror_dead"
-
- speed = -0.5
+
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ mob_armor = ARMOR_VALUE_HORROR
+
maxHealth = 700
health = 700
+ speed = -0.5 // Fast
stat_attack = UNCONSCIOUS
- harm_intent_damage = 8
+
melee_damage_lower = 30
melee_damage_upper = 40
-
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+ harm_intent_damage = 8
+
robust_searching = TRUE
+
+ speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes")
attack_verb_simple = "eviscerates"
attack_sound = 'sound/weapons/punch1.ogg'
- var/static/list/abom_sounds
deathmessage = "wails as its form shudders and violently comes to a stop."
+
+ faction = list("hostile")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
-// NSB variant, some sort of bulletsponge
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+ var/static/list/abom_sounds
+
+/mob/living/simple_animal/hostile/abomhorror/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/abomhorror/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// NSB HORROR - bunker variant, bulletsponge
/mob/living/simple_animal/hostile/abomhorror/nsb
+ desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain, constantly shuddering violently and seeming relatively docile to the robots and raiders of the bunker. Huh."
+
maxHealth = 1000
health = 1000
- desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain, constantly shuddering violently and seeming relatively docile to the robots and raiders of the bunker. Huh."
- harm_intent_damage = 8
+ speed = -1 // Even faster
+
melee_damage_lower = 40
melee_damage_upper = 50
obj_damage = 300
- faction = list("raider")
wound_bonus = 20
- speed = -1
- deathmessage = "wails as its form shudders and violently comes to a stop."
+
+ faction = list("raider")
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE
/mob/living/simple_animal/hostile/abomhorror/nsb/Initialize()
. = ..()
abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg')
-/mob/living/simple_animal/hostile/abomhorror/nsb/say(message, datum/language/language = null, list/spans = list(), language, sanitize, ignore_spam, forced = null, just_chat)
- ..()
+/mob/living/simple_animal/hostile/abomhorror/nsb/say(message, datum/language/language = null, list/spans = list(), sanitize, ignore_spam, forced = null, just_chat)
+ . = ..()
if(stat)
return
var/chosen_sound = pick(abom_sounds)
playsound(src, chosen_sound, 50, TRUE)
/mob/living/simple_animal/hostile/abomhorror/nsb/Life()
- ..()
+ . = ..()
if(stat)
return
if(prob(10))
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm b/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm
index e4583879917..9e20bfcff9f 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm
@@ -1,3 +1,10 @@
+// In this document: Chinese Remnant Soldiers
+
+/////////////////////////////
+// CHINESE REMNANT SOLDIERS //
+/////////////////////////////
+
+// BASE - melee ghoul soldier
/mob/living/simple_animal/hostile/chinese
name = "chinese remnant soldier"
desc = "Chinese soldiers who survived the Great War via ghoulification, and now shoot anything that isn't their own on sight."
@@ -5,49 +12,108 @@
icon_state = "chinesesoldier"
icon_living = "chinesesoldier"
icon_gib = "syndicate_gib"
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- speak_chance = 0
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
+ mob_armor = ARMOR_VALUE_CHINESE_REMNANT
+
maxHealth = 80
health = 80
- harm_intent_damage = 8
+ speed = 1
+ move_to_delay = 3
+ turns_per_move = 5
+
melee_damage_lower = 20
melee_damage_upper = 38
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "punches"
attack_sound = 'sound/weapons/bladeslice.ogg'
- a_intent = INTENT_HARM
- loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant, /obj/item/melee/onehanded/knife/survival)
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
+
+ tastes = list("people" = 1, "dust" = 2)
+
faction = list("china")
- check_friendly_fire = 1
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
- del_on_death = 1
- tastes = list("people" = 1, "dust" = 2)
+ del_on_death = TRUE
+ speak_chance = 0
+
+ loot = list(
+ /obj/effect/mob_spawn/human/corpse/chineseremnant,
+ /obj/item/melee/onehanded/knife/survival
+ )
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
/mob/living/simple_animal/hostile/chinese/Aggro()
. = ..()
if(.)
return
- summon_backup(15)
+ summon_backup(10)
if(!ckey)
- say(pick("操你祖宗十八代", "乡巴佬", "傻逼" , "妈你个", "操你大爷", "祝你生孩子没屁眼", "扯鸡巴蛋", "狗改不了吃屎", "爆你菊花" ))
+ say(pick("操你祖宗十八代", "乡巴佬", "傻逼", "妈你个", "操你大爷", "祝你生孩子没屁眼", "扯鸡巴蛋", "狗改不了吃屎", "爆你菊花"))
+/mob/living/simple_animal/hostile/chinese/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - chinese soldiers fight in squads
+/mob/living/simple_animal/hostile/chinese/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/chinese))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// PISTOL SOLDIER - ranged variant
/mob/living/simple_animal/hostile/chinese/ranged
+ name = "chinese remnant soldier"
icon_state = "chinesepistol"
icon_living = "chinesepistol"
- loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant/pistol, /obj/item/gun/ballistic/automatic/pistol/type17)
- ranged = 1
+
maxHealth = 110
health = 110
- retreat_distance = 4
- minimum_distance = 6
+
+ loot = list(
+ /obj/effect/mob_spawn/human/corpse/chineseremnant/pistol,
+ /obj/item/gun/ballistic/automatic/pistol/type17
+ )
+
+ // Pure ranged - pistol
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/bullet/c9mm/simple
- projectilesound = 'sound/f13weapons/ninemil.ogg'
+ projectilesound = 'sound/f13weapons/ninemil.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_LIGHT_VOLUME),
@@ -59,14 +125,24 @@
SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
)
+// ASSAULT SOLDIER - burst fire rifle
/mob/living/simple_animal/hostile/chinese/ranged/assault
name = "chinese remnant assault soldier"
icon_state = "chineseassault"
icon_living = "chineseassault"
+
maxHealth = 160
health = 160
extra_projectiles = 2
- loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant/assault, /obj/item/gun/ballistic/automatic/type93, /obj/item/ammo_box/magazine/m556/rifle/assault)
+
+ loot = list(
+ /obj/effect/mob_spawn/human/corpse/chineseremnant/assault,
+ /obj/item/gun/ballistic/automatic/type93,
+ /obj/item/ammo_box/magazine/m556/rifle/assault
+ )
+
+ // Pure ranged - assault rifle
+ ranged_cooldown_time = 15
projectiletype = /obj/item/projectile/bullet/a556/simple
projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg'
projectile_sound_properties = list(
@@ -81,5 +157,7 @@
)
/mob/living/simple_animal/hostile/chinese/ranged/assault/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm b/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm
index 3ed988bff08..f66c4edd2c9 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm
@@ -13,8 +13,8 @@
icon_gib = "deathclaw_gib"
mob_armor = ARMOR_VALUE_DEATHCLAW_COMMON
sentience_type = SENTIENCE_BOSS
- maxHealth = 600
- health = 600
+ maxHealth = 500 // Reduced from 600
+ health = 500
stat_attack = UNCONSCIOUS
reach = 2
speed = 1
@@ -22,28 +22,38 @@
melee_damage_lower = 30
melee_damage_upper = 40
footstep_type = FOOTSTEP_MOB_HEAVY
- move_to_delay = 2.75 //hahahahahahahaaaaa
+ move_to_delay = 2.75
gender = MALE
- a_intent = INTENT_HARM //So we can not move past them.
+ a_intent = INTENT_HARM
mob_biotypes = MOB_ORGANIC|MOB_BEAST
robust_searching = TRUE
+
+ // Apex predator with excellent vision in low light
+ has_low_light_vision = TRUE
+ low_light_bonus = 4
+
speak = list("ROAR!","Rawr!","GRRAAGH!","Growl!")
speak_emote = list("growls", "roars")
emote_hear = list("grumbles.","grawls.")
emote_taunt = list("stares ferociously", "stomps")
speak_chance = 10
taunt_chance = 25
+
tastes = list("a bad time" = 5, "dirt" = 1)
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS | ENVIRONMENT_SMASH_RWALLS //can smash walls
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES // Only structures, not walls normally
var/color_mad = "#ffc5c5"
see_in_dark = 8
decompose = FALSE
- wound_bonus = 0 //This might be a TERRIBLE idea
+ wound_bonus = 0
bare_wound_bonus = 0
sharpness = SHARP_EDGED
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 4,
- /obj/item/stack/sheet/animalhide/deathclaw = 2,
- /obj/item/stack/sheet/bone = 4)
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 4,
+ /obj/item/stack/sheet/animalhide/deathclaw = 2,
+ /obj/item/stack/sheet/bone = 4
+ )
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
@@ -51,23 +61,41 @@
attack_sound = 'sound/weapons/bladeslice.ogg'
faction = list("deathclaw")
gold_core_spawnable = HOSTILE_SPAWN
- //var/charging = FALSE
move_resist = MOVE_FORCE_OVERPOWERING
+
emote_taunt_sound = list('sound/f13npc/deathclaw/taunt.ogg')
- aggrosound = list('sound/f13npc/deathclaw/aggro1.ogg', 'sound/f13npc/deathclaw/aggro2.ogg', )
- idlesound = list('sound/f13npc/deathclaw/idle.ogg',)
+ aggrosound = list('sound/f13npc/deathclaw/aggro1.ogg', 'sound/f13npc/deathclaw/aggro2.ogg')
+ idlesound = list('sound/f13npc/deathclaw/idle.ogg')
death_sound = 'sound/f13npc/deathclaw/death.ogg'
+
low_health_threshold = 0.5
+ despawns_when_lonely = FALSE
+
+ // Charge mechanic vars
+ var/charging = FALSE
+ var/charge_cooldown = 0
+ var/charge_cooldown_time = 10 SECONDS
+
variation_list = list(
MOB_RETREAT_DISTANCE_LIST(0, 0, 0, 3, 3),
MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(65),
MOB_MINIMUM_DISTANCE_LIST(0, 0, 0, 1),
MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(30),
)
- despawns_when_lonely = FALSE
+ // Z-movement
can_z_move = TRUE
- z_move_delay = 20 // 2 seconds - fast pursuit
+ can_climb_ladders = FALSE // Too big!
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 20 // Fast pursuit
+
+ can_open_doors = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
/mob/living/simple_animal/hostile/deathclaw/playable
emote_taunt_sound = null
@@ -77,39 +105,46 @@
see_in_dark = 8
wander = FALSE
-/// Override this with what should happen when going from high health to low health
-/mob/living/simple_animal/hostile/deathclaw/make_low_health()
- visible_message(span_danger("[src] lets out a vicious roar!!!"))
- playsound(src, 'sound/f13npc/deathclaw/aggro2.ogg', 100, 1, SOUND_DISTANCE(20))
- color = color_mad
- reach += 1
- speed *= 0.8
- obj_damage += 200
- melee_damage_lower *= 1.5
- melee_damage_upper *= 1.4
- see_in_dark += 8
- environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS | ENVIRONMENT_SMASH_RWALLS //can smash walls
- wound_bonus += 25
- bare_wound_bonus += 50
- sound_pitch = -50
- alternate_attack_prob = 75
- is_low_health = TRUE
+// Override to ignore unconscious/dead targets
+/mob/living/simple_animal/hostile/deathclaw/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
-/// Override this with what should happen when going from low health to high health
-/mob/living/simple_animal/hostile/deathclaw/make_high_health()
- visible_message(span_danger("[src] calms down."))
- color = initial(color)
- reach = initial(reach)
- speed = initial(speed)
- obj_damage = initial(obj_damage)
- melee_damage_lower = initial(melee_damage_lower)
- melee_damage_upper = initial(melee_damage_upper)
- see_in_dark = initial(see_in_dark)
- environment_smash = initial(environment_smash)
- wound_bonus = initial(wound_bonus)
- bare_wound_bonus = initial(bare_wound_bonus)
- alternate_attack_prob = initial(alternate_attack_prob)
- is_low_health = FALSE
+/// RAGE MODE - when going from high health to low health
+/mob/living/simple_animal/hostile/deathclaw/mother/make_low_health()
+ if(!target)
+ return
+ ..()
+
+/// Calming down when going from low health to high health
+/mob/living/simple_animal/hostile/deathclaw/mother/make_high_health()
+ if(!target)
+ // If we somehow have rage active without a target, clean it up
+ if(is_low_health)
+ color = initial(color)
+ reach = initial(reach)
+ speed = initial(speed)
+ obj_damage = initial(obj_damage)
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ see_in_dark = initial(see_in_dark)
+ environment_smash = initial(environment_smash)
+ wound_bonus = initial(wound_bonus)
+ bare_wound_bonus = initial(bare_wound_bonus)
+ alternate_attack_prob = initial(alternate_attack_prob)
+ sound_pitch = initial(sound_pitch)
+ is_low_health = FALSE
+ return
+ ..()
+
+// Deactivate rage when target is lost
+/mob/living/simple_animal/hostile/deathclaw/mother/LoseTarget()
+ ..()
+ if(is_low_health)
+ make_high_health()
/mob/living/simple_animal/hostile/deathclaw/AlternateAttackingTarget(atom/the_target)
if(!ismovable(the_target))
@@ -119,43 +154,116 @@
return
var/atom/throw_target = get_ranged_target_turf(throwee, get_dir(src, the_target), rand(2,10), 4)
throwee.safe_throw_at(throw_target, 10, 1, src, TRUE)
- playsound(get_turf(throwee), 'sound/effects/Flesh_Break_1.ogg')
+ playsound(get_turf(throwee), 'sound/effects/Flesh_Break_1.ogg', 50, 1)
+ visible_message(span_danger("[src] hurls [the_target] across the room!"))
+
+// CHARGE MECHANIC - trigger on getting shot
+/mob/living/simple_animal/hostile/deathclaw/bullet_act(obj/item/projectile/Proj)
+ if(!Proj)
+ return
+
+ // Chance to charge when shot, if not on cooldown
+ if(!charging && world.time > charge_cooldown && prob(30))
+ visible_message(span_danger("\The [src] roars in rage!"))
+ addtimer(CALLBACK(src, PROC_REF(Charge)), 0.3 SECONDS)
+
+ return ..()
+
+/mob/living/simple_animal/hostile/deathclaw/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect)
+ if(!charging)
+ ..()
+
+/mob/living/simple_animal/hostile/deathclaw/AttackingTarget()
+ if(!charging)
+ return ..()
+
+/mob/living/simple_animal/hostile/deathclaw/Goto(target, delay, minimum_distance)
+ if(!charging)
+ ..()
/mob/living/simple_animal/hostile/deathclaw/Move()
if(is_low_health && health > 0)
new /obj/effect/temp_visual/decoy/fading(loc,src)
DestroySurroundings()
. = ..()
+ if(charging)
+ new /obj/effect/temp_visual/decoy/fading(loc,src)
+ DestroySurroundings()
+
+/mob/living/simple_animal/hostile/deathclaw/proc/Charge()
+ if(!target)
+ return
+
+ var/turf/T = get_turf(target)
+ if(!T || T == loc)
+ return
+
+ charging = TRUE
+ charge_cooldown = world.time + charge_cooldown_time
+
+ visible_message(span_danger("[src] charges with terrifying speed!"))
+ DestroySurroundings()
+ walk(src, 0)
+ setDir(get_dir(src, T))
+
+ var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src)
+ animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 0.1 SECONDS)
+
+ throw_at(T, get_dist(src, T), 2, src, FALSE, callback = CALLBACK(src, PROC_REF(charge_end)))
+
+/mob/living/simple_animal/hostile/deathclaw/proc/charge_end(list/effects_to_destroy)
+ charging = FALSE
+ if(target)
+ Goto(target, move_to_delay, minimum_distance)
/mob/living/simple_animal/hostile/deathclaw/Bump(atom/A)
- if(is_low_health)
+ if(charging)
if((isturf(A) || isobj(A)) && A.density)
A.ex_act(EXPLODE_HEAVY)
playsound(src, 'sound/effects/meteorimpact.ogg', 100, 1)
- if(stat || health <= 0)
+
+ // Chance to die on wall impact if low health
+ if(is_low_health && health < (maxHealth * 0.2) && prob(25))
playsound(get_turf(src), 'sound/effects/Flesh_Break_2.ogg', 100, 1, ignore_walls = TRUE)
- visible_message(span_danger("[src] smashes into \the [A] and explodes in a violent spray of gore![prob(25) ? " Holy shit!" : ""]"))
+ visible_message(span_danger("[src] smashes into \the [A] with such force that it explodes in a violent spray of gore! Holy shit!"))
gib()
return
DestroySurroundings()
..()
-// Mother death claw
+/mob/living/simple_animal/hostile/deathclaw/throw_impact(atom/A)
+ if(!charging)
+ return ..()
+
+ if(isliving(A))
+ var/mob/living/L = A
+ L.visible_message(span_danger("[src] slams into [L] with incredible force!"), span_userdanger("[src] slams into you!"))
+ L.apply_damage(melee_damage_upper, BRUTE)
+ playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1)
+ shake_camera(L, 4, 3)
+ shake_camera(src, 2, 3)
+ var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src)))
+ L.throw_at(throwtarget, 3, 2)
+
+ charging = FALSE
+
+// Mother deathclaw
/mob/living/simple_animal/hostile/deathclaw/mother
name = "mother deathclaw"
desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. This one is an angry mother."
gender = FEMALE
mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER
- maxHealth = 1500
- health = 1500
+ maxHealth = 1000 // Reduced from 1500
+ health = 1000
stat_attack = CONSCIOUS
melee_damage_lower = 25
melee_damage_upper = 55
- footstep_type = FOOTSTEP_MOB_HEAVY
color = rgb(95,104,94)
color_mad = rgb(113, 105, 100)
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 6,
- /obj/item/stack/sheet/animalhide/deathclaw = 3)
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 6,
+ /obj/item/stack/sheet/animalhide/deathclaw = 3
+ )
/mob/living/simple_animal/hostile/deathclaw/butter
name = "butterclaw"
@@ -164,22 +272,23 @@
icon_living = "deathclaw_butter"
icon_dead = "deathclaw_butter_dead"
color_mad = rgb(133, 98, 87)
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/butter = 10,
- /obj/item/stack/sheet/animalhide/deathclaw = 3)
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/butter = 10,
+ /obj/item/stack/sheet/animalhide/deathclaw = 3
+ )
//Legendary Deathclaw
/mob/living/simple_animal/hostile/deathclaw/legendary
name = "legendary deathclaw"
desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. This one is a legendary enemy."
mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER
- maxHealth = 2400
- health = 2400
+ maxHealth = 1800 // Reduced from 2400
+ health = 1800
color = "#FFFF00"
color_mad = rgb(133, 98, 87)
stat_attack = CONSCIOUS
melee_damage_lower = 25
melee_damage_upper = 55
- footstep_type = FOOTSTEP_MOB_HEAVY
/mob/living/simple_animal/hostile/deathclaw/legendary/death(gibbed)
var/turf/T = get_turf(src)
@@ -187,83 +296,16 @@
new /obj/item/melee/unarmed/deathclawgauntlet(T)
. = ..()
-//Power Armor Deathclaw the tankest and the scariest deathclaw in the West. One mistake will end you. May the choice be with you.
+//Power Armor Deathclaw - the tankiest deathclaw
/mob/living/simple_animal/hostile/deathclaw/power_armor
name = "power armored deathclaw"
desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. Someone had managed to put power armor on him."
icon_state = "combatclaw"
icon_living = "combatclaw"
icon_dead = "combatclaw_dead"
- mob_armor = ARMOR_VALUE_DEATHCLAW_PA // ha get fucked
- maxHealth = 3000 // ha get turbofucked
- health = 3000
+ mob_armor = ARMOR_VALUE_DEATHCLAW_PA
+ maxHealth = 2000 // Reduced from 3000
+ health = 2000
stat_attack = CONSCIOUS
melee_damage_lower = 40
melee_damage_upper = 60
- footstep_type = FOOTSTEP_MOB_HEAVY
-
-
-/// Code for deathclaw charging. It barely works
-/* /mob/living/simple_animal/hostile/deathclaw/bullet_act(obj/item/projectile/Proj)
- if(!Proj)
- return
- if(!charging)
- visible_message(span_danger("\The [src] growls, enraged!"))
- addtimer(CALLBACK(src, PROC_REF(Charge)), 3)
- . = ..() // I swear I looked at this like 10 times before, never once noticed this wasnt here, fmdakm
-
-/mob/living/simple_animal/hostile/deathclaw/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect)
- if(!charging)
- ..()
-
-/mob/living/simple_animal/hostile/deathclaw/AttackingTarget()
- if(!charging)
- return ..()
-
-/mob/living/simple_animal/hostile/deathclaw/Goto(target, delay, minimum_distance)
- if(!charging)
- ..()
-
-/mob/living/simple_animal/hostile/deathclaw/proc/Charge()
- var/turf/T = get_turf(target)
- if(!T || T == loc)
- return
- charging = TRUE
- visible_message(span_danger(">[src] charges!"))
- DestroySurroundings()
- walk(src, 0)
- setDir(get_dir(src, T))
- var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src)
- animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 1)
- throw_at(T, get_dist(src, T), 1, src, 0, callback = CALLBACK(src, PROC_REF(charge_end)))
-
-/mob/living/simple_animal/hostile/deathclaw/proc/charge_end(list/effects_to_destroy)
- charging = FALSE
- if(target)
- Goto(target, move_to_delay, minimum_distance)
-
-/mob/living/simple_animal/hostile/deathclaw/Bump(atom/A)
- if(charging)
- if(isturf(A) || isobj(A) && A.density)
- A.ex_act(EXPLODE_HEAVY)
- DestroySurroundings()
- ..()
-
-/mob/living/simple_animal/hostile/deathclaw/throw_impact(atom/A)
- if(!charging)
- return ..()
-
- else if(isliving(A))
- var/mob/living/L = A
- L.visible_message(span_danger("[src] slams into [L]!"), span_userdanger("[src] slams into you!"))
- L.apply_damage(melee_damage_lower/2, BRUTE)
- playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1)
- shake_camera(L, 4, 3)
- shake_camera(src, 2, 3)
- var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src)))
- L.throw_at(throwtarget, 3)
-
-
- charging = FALSE
- charging = FALSE
- */
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm b/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm
index ef81f572511..c836c558aaf 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm
@@ -1,7 +1,10 @@
+// In this document: Eyebots, Floating Eyes, Propaganda Eyebot
+
/////////////
// EYEBOTS //
/////////////
+// BASE EYEBOT - hovering laser recon drone
/mob/living/simple_animal/hostile/eyebot
name = "eyebot"
desc = "A hovering, propaganda-spewing reconnaissance and surveillance robot with radio antennas pointing out its back and loudspeakers blaring out the front."
@@ -9,59 +12,79 @@
icon_state = "eyebot"
icon_living = "eyebot"
icon_dead = "eyebot_d"
- can_ghost_into = TRUE
- speak_chance = 0
+
+ mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
+ mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
+
+ maxHealth = 40
+ health = 40
+ move_to_delay = 2.75
turns_per_move = 6
- environment_smash = 0
+ stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT
+
+ melee_damage_lower = 2
+ melee_damage_upper = 3
+ harm_intent_damage = 8
+
+ aggro_vision_range = 7
+ vision_range = 7
+ robust_searching = TRUE
+
response_help_simple = "touches"
response_disarm_simple = "shoves"
response_harm_simple = "hits"
- move_to_delay = 3
- robust_searching = 1
- mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
- maxHealth = 40
- health = 40
- stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT
+ attack_verb_simple = "punches"
+ attack_sound = 'sound/weapons/punch1.ogg'
+
+ speak_emote = list("states")
+ speak_chance = 0
+ aggrosound = list('sound/f13npc/eyebot/aggro.ogg')
+ idlesound = list('sound/f13npc/eyebot/idle1.ogg', 'sound/f13npc/eyebot/idle2.ogg')
+ death_sound = 'sound/f13npc/eyebot/robo_death.ogg'
+
emp_flags = list(
MOB_EMP_STUN,
MOB_EMP_BERSERK,
MOB_EMP_DAMAGE,
MOB_EMP_SCRAMBLE
- )
- healable = 0
- mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
- blood_volume = 0
- faction = list(
- "hostile",
- "enclave",
- "wastebot",
- "ghoul",
- "cazador",
- "supermutant",
- "bighorner"
- )
- harm_intent_damage = 8
- melee_damage_lower = 2
- melee_damage_upper = 3
- minimum_distance = 6
- retreat_distance = 14
- attack_verb_simple = "punches"
- attack_sound = "punch"
- a_intent = "harm"
- tastes = list("metal" = 1, "glass" = 1)
+ )
+
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
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)
- unsuitable_atmos_damage = 15
- status_flags = CANPUSH
+
tastes = list("metal" = 1, "glass" = 1)
- vision_range = 7 //reduced from 13 to 7 because who needs that kind of shit in their life
- aggro_vision_range = 7 //as above
- ranged = 1
+
+ faction = list("hostile", "enclave", "wastebot", "ghoul", "cazador", "supermutant", "bighorner")
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
+ healable = FALSE
+ blood_volume = 0
+ status_flags = CANPUSH
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ can_ghost_into = TRUE
+ desc_short = "A flying metal meatball with lasers."
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Z-movement - flies
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ can_open_doors = FALSE
+ can_open_airlocks = TRUE // Small enough to slip through
+
+ // Pure ranged - kites at extreme distance
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 14
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/beam/laser/pistol/wattz
projectilesound = 'sound/weapons/resonator_fire.ogg'
- aggrosound = list('sound/f13npc/eyebot/aggro.ogg')
- idlesound = list('sound/f13npc/eyebot/idle1.ogg', 'sound/f13npc/eyebot/idle2.ogg')
- death_sound = 'sound/f13npc/eyebot/robo_death.ogg'
- speak_emote = list("states")
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -72,47 +95,100 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
- desc_short = "A flying metal meatball with lasers."
+
var/obj/machinery/camera/portable/builtInCamera
-
/mob/living/simple_animal/hostile/eyebot/New()
..()
name = "ED-[rand(1,99)]"
+/mob/living/simple_animal/hostile/eyebot/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - eyebots are networked
+/mob/living/simple_animal/hostile/eyebot/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/eyebot))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
/mob/living/simple_animal/hostile/eyebot/become_the_mob(mob/user)
send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/robot
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/robot
. = ..()
+// PLAYABLE EYEBOT
/mob/living/simple_animal/hostile/eyebot/playable
- ranged = FALSE
- health = 30
maxHealth = 30
+ health = 30
+ speed = -1
attack_verb_simple = "zaps"
- emote_taunt_sound = null
- emote_taunt = null
- aggrosound = null
- idlesound = null
see_in_dark = 8
- wander = 0
+ wander = FALSE
force_threshold = 10
anchored = FALSE
del_on_death = FALSE
dextrous = TRUE
+ ranged = FALSE
+ emote_taunt_sound = null
+ emote_taunt = null
+ aggrosound = null
+ idlesound = null
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
- speed = -1
+// REINFORCED EYEBOT - tougher raider variant
+/mob/living/simple_animal/hostile/eyebot/reinforced
+ name = "reinforced eyebot"
+ desc = "An eyebot with beefier protection, and extra electronic aggression."
+ color = "#B85C00"
+
+ mob_armor = ARMOR_VALUE_ROBOT_SECURITY
+
+ maxHealth = 100
+ health = 100
+
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+
+ faction = list("raider", "wastebot")
+
+ // Pure ranged - closer than standard
+ retreat_distance = 6
+ minimum_distance = 1
+
+ extra_projectiles = 1
+ auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER
+
+/mob/living/simple_animal/hostile/eyebot/reinforced/become_the_mob(mob/user)
+ send_mobs = null
+ call_backup = null
+ . = ..()
+
+///////////////////
+// FLOATING EYES //
+///////////////////
+
+// FLOATING EYE - taser variant, BOS faction
/mob/living/simple_animal/hostile/eyebot/floatingeye
name = "floating eyebot"
desc = "A quick-observation robot commonly found in pre-War military installations.
The floating eyebot uses a powerful taser to keep intruders in line."
icon_state = "floatingeye"
icon_living = "floatingeye"
icon_dead = "floatingeye_d"
-
- retreat_distance = 4
+
faction = list("hostile", "bs")
-
+
+ // Pure ranged - closer engagement than standard eyebot
+ retreat_distance = 6
+ minimum_distance = 1
+
projectiletype = /obj/item/projectile/energy/electrode
projectilesound = 'sound/weapons/resonator_blast.ogg'
@@ -125,7 +201,12 @@
call_backup = null
. = ..()
-/mob/living/simple_animal/pet/dog/eyebot //It's a propaganda eyebot, not a dog, but...
+//////////////////////
+// PROPAGANDA EYEBOT //
+//////////////////////
+
+// PROPAGANDA EYEBOT - unarmed pet variant
+/mob/living/simple_animal/pet/dog/eyebot
name = "propaganda eyebot"
desc = "This eyebot's weapons module has been removed and replaced with a loudspeaker. It appears to be shouting Pre-War propaganda."
icon = 'icons/fallout/mobs/robots/eyebots.dmi'
@@ -133,22 +214,28 @@
icon_living = "eyebot"
icon_dead = "eyebot_d"
icon_gib = "eyebot_d"
+
+ mob_biotypes = MOB_ROBOTIC
+
maxHealth = 60
health = 60
speak_chance = 8
gender = NEUTER
- mob_biotypes = MOB_ROBOTIC
+ blood_volume = 0
+
faction = list("hostile", "enclave", "wastebot", "ghoul", "cazador", "supermutant", "bighorner")
+
speak = list()
speak_emote = list("states")
emote_hear = list()
- emote_see = list("buzzes.","pings.","floats in place")
- response_help_simple = "shakes the radio of"
+ emote_see = list("buzzes.", "pings.", "floats in place")
+
+ response_help_simple = "shakes the radio of"
response_disarm_simple = "pushes"
- response_harm_simple = "punches"
+ response_harm_simple = "punches"
attack_sound = 'sound/voice/liveagain.ogg'
+
butcher_results = list(/obj/effect/gibspawner/robot = 1)
- blood_volume = 0
/mob/living/simple_animal/pet/dog/eyebot/ComponentInitialize()
. = ..()
@@ -158,47 +245,27 @@
. = ..()
if(. & EMP_PROTECT_SELF)
return
- var/emp_damage = round((maxHealth * 0.1) * (severity * 0.1)) // 10% of max HP * 10% of severity(Usually around 20-40)
+ var/emp_damage = round((maxHealth * 0.1) * (severity * 0.1))
adjustBruteLoss(emp_damage)
+// PLAYABLE PROPAGANDA EYEBOT
/mob/living/simple_animal/pet/dog/eyebot/playable
- health = 200
maxHealth = 200
+ health = 200
+ speed = 1
attack_verb_simple = "zaps"
- aggrosound = null
- speak_chance = 0
- idlesound = null
see_in_dark = 8
- wander = 0
+ speak_chance = 0
+ wander = FALSE
force_threshold = 10
anchored = FALSE
del_on_death = FALSE
dextrous = TRUE
+ aggrosound = null
+ idlesound = null
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
- speed = 1
/mob/living/simple_animal/pet/dog/eyebot/playable/become_the_mob(mob/user)
send_mobs = null
call_backup = null
. = ..()
-
-//Junkers
-/mob/living/simple_animal/hostile/eyebot/reinforced
- name = "reinforced eyebot"
- desc = "An eyebot with beefier protection, and extra electronic aggression."
- color = "#B85C00"
- mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
- maxHealth = 100
- health = 100
- faction = list("raider", "wastebot")
- extra_projectiles = 1
- auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER
- melee_damage_lower = 5
- melee_damage_upper = 10
- minimum_distance = 4
- retreat_distance = 6
-
-/mob/living/simple_animal/hostile/eyebot/reinforced/become_the_mob(mob/user)
- send_mobs = null
- call_backup = null
- . = ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm b/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm
index 0c6851f7956..ae1376f9198 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm
@@ -1,55 +1,98 @@
+// In this document: Vault Dwellers, Enclave, Brotherhood, NCR, Legion, Tribals
+
///////////////
// VAULT NPC //
///////////////
+// BASE VAULT DWELLER
/mob/living/simple_animal/hostile/vault
name = "Vault Dweller"
- desc = "Just a Vault Dweller"
+ desc = "Just a Vault Dweller."
icon = 'icons/fallout/mobs/humans/fallout_npc.dmi'
icon_state = "vault_dweller"
icon_living = "vault_dweller"
icon_dead = "vault_dweller"
- turns_per_move = 5
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
- stat_attack = 1
- robust_searching = 1
+
maxHealth = 100
health = 100
- harm_intent_damage = 8
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 5
melee_damage_upper = 10
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "punches"
attack_sound = 'sound/weapons/punch1.ogg'
- a_intent = INTENT_HARM
- loot = list(/obj/effect/mob_spawn/human/corpse/vault)
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
+
faction = list("vault", "city")
- check_friendly_fire = 1
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
del_on_death = TRUE
speak_chance = 1
despawns_when_lonely = FALSE
-
-/obj/effect/mob_spawn/human/corpse/vault
- name = "Vault Dweller"
- gloves = /obj/item/pda
- uniform = /obj/item/clothing/under/f13/vault/v13
- shoes = /obj/item/clothing/shoes/jackboots
-
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/vault)
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/vault/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - vault dwellers work together
+/mob/living/simple_animal/hostile/vault/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/vault))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// VAULT DWELLER VARIANTS - flee when attacked
/mob/living/simple_animal/hostile/vault/dweller
- minimum_distance = 10
+ // Cowardly melee - flees when attacked
+ combat_mode = COMBAT_MODE_MELEE
retreat_distance = 10
+ minimum_distance = 1
+
obj_damage = 0
- environment_smash = 0
+ environment_smash = ENVIRONMENT_SMASH_NONE
/mob/living/simple_animal/hostile/vault/dweller/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
say("HELP!!")
/mob/living/simple_animal/hostile/vault/dweller/dweller1
@@ -77,22 +120,29 @@
icon_living = "vault_dweller5"
icon_dead = "vault_dweller5"
+// VAULT SECURITY - armed guards
/mob/living/simple_animal/hostile/vault/security
name = "Vault Security"
- desc = "Just a Vault Security"
+ desc = "Just a Vault Security officer."
icon_state = "vault_dweller_sec"
icon_living = "vault_dweller_sec"
icon_dead = "vault_dweller_sec"
+
maxHealth = 160
health = 160
- retreat_distance = 5
- minimum_distance = 5
+
loot = list(/obj/effect/mob_spawn/human/corpse/vault/security)
- healable = 1
- ranged = 1
+ healable = TRUE
+
+ // Pure ranged - laser pistol
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/beam
projectilesound = 'sound/weapons/resonator_fire.ogg'
- speak_chance = 1
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -104,10 +154,18 @@
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
-
/mob/living/simple_animal/hostile/vault/security/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+// VAULT CORPSES
+/obj/effect/mob_spawn/human/corpse/vault
+ name = "Vault Dweller"
+ gloves = /obj/item/pda
+ uniform = /obj/item/clothing/under/f13/vault/v13
+ shoes = /obj/item/clothing/shoes/jackboots
/obj/effect/mob_spawn/human/corpse/vault/security
name = "Vault Security"
@@ -122,48 +180,70 @@
// ENCLAVE NPC //
/////////////////
-// Enclave specialist, basic fighter
+// BASE ENCLAVE
/mob/living/simple_animal/hostile/enclave
name = "enclave specialist"
- desc = "A Enclave soldier with combat armor and a G-11 rifle."
+ desc = "An Enclave soldier with combat armor and a G-11 rifle."
icon = 'icons/fallout/mobs/humans/fallout_npc.dmi'
icon_state = "enclave_specialist"
icon_living = "enclave_specialist"
- del_on_death = TRUE
+ icon_dead = "enclave_specialist"
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- speak_chance = 0
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- retreat_distance = 6
- minimum_distance = 6
- speed = 0
- ranged_cooldown_time = 22
- extra_projectiles = 2
- stat_attack = 1
- ranged = TRUE
- robust_searching = TRUE
- healable = TRUE
+ mob_armor = ARMOR_VALUE_ENCLAVE
+
maxHealth = 200
health = 200
+ speed = 0
+ turns_per_move = 5
+
melee_damage_lower = 15
melee_damage_upper = 35
harm_intent_damage = 8
-
- projectiletype = /obj/item/projectile/bullet/c46x30mm
- projectilesound = 'sound/weapons/gunshot_smg.ogg'
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+ healable = TRUE
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "pistol-whips"
-
attack_sound = 'sound/weapons/punch1.ogg'
- a_intent = INTENT_HARM
+
+ speak = list("For the Enclave!", "Stars and Stripes!", "Liberty or death!")
+ speak_emote = list("pulls out a weapon", "shouts")
+ speak_chance = 0
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
+
faction = list("enclave")
- check_friendly_fire = 1
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
- speak = list("For the Enclave!", "Stars and Stripes!", "Liberty or death!")
- speak_emote = list("pulls out a weapon", "shouts")
+ del_on_death = TRUE
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure ranged - G-11
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 6
+ minimum_distance = 1
+
+ ranged_cooldown_time = 22
+ extra_projectiles = 2
+ projectiletype = /obj/item/projectile/bullet/c46x30mm
+ projectilesound = 'sound/weapons/gunshot_smg.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_LIGHT_VOLUME),
@@ -175,26 +255,57 @@
SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
)
-// Enclave Scientist
+/mob/living/simple_animal/hostile/enclave/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/enclave/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - enclave soldiers are well-trained
+/mob/living/simple_animal/hostile/enclave/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/enclave))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// ENCLAVE SCIENTIST - cowardly ranged
/mob/living/simple_animal/hostile/enclave/scientist
name = "enclave scientist"
desc = "An Enclave Scientist wearing an advanced radiation suit. While they may run from you, that does not exempt them from the evil they have committed."
icon_state = "enclave_scientist"
icon_living = "enclave_scientist"
+ icon_dead = "enclave_scientist"
+
maxHealth = 120
health = 120
- minimum_distance = 10
- retreat_distance = 10
- obj_damage = 0
- environment_smash = 0
- loot = list(/obj/effect/mob_spawn/human/corpse/enclavescientist)
+
melee_damage_lower = 5
melee_damage_upper = 15
+ obj_damage = 0
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/enclavescientist)
+
+ // Pure ranged - flees when threatened
+ combat_mode = COMBAT_MODE_RANGED
+ retreat_distance = 10
+ minimum_distance = 1
+
ranged_cooldown_time = 30
- projectiletype = /obj/item/projectile/f13plasma/pistol/adam
- projectilesound = 'sound/weapons/wave.ogg'
extra_projectiles = 0
attack_verb_simple = "thrusts"
+ projectiletype = /obj/item/projectile/f13plasma/pistol/adam
+ projectilesound = 'sound/weapons/wave.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PLASMA_VOLUME),
@@ -206,23 +317,32 @@
SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT)
)
-// Enclave Armored Infantry
+// ENCLAVE ARMORED INFANTRY - heavy trooper
/mob/living/simple_animal/hostile/enclave/soldier
name = "enclave armored infantry"
desc = "An Enclave Soldier wearing Advanced Power Armor and a plasma multi-caster. Play time's over, mutie."
icon_state = "enclave_armored"
icon_living = "enclave_armored"
+ icon_dead = "enclave_armored"
+
+ mob_armor = ARMOR_VALUE_ENCLAVE_APA
+
maxHealth = 560
- health = 650
+ health = 560
stat_attack = UNCONSCIOUS
+
melee_damage_lower = 20
melee_damage_upper = 47
- extra_projectiles = 2
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/enclave/soldier)
+
+ // Pure ranged - aggressive
+ combat_mode = COMBAT_MODE_RANGED
retreat_distance = 3
- minimum_distance = 5
+ minimum_distance = 1
+
ranged_cooldown_time = 12
- loot = list(/obj/effect/mob_spawn/human/corpse/enclave/soldier)
- healable = 1
+ extra_projectiles = 2
attack_verb_simple = "power-fists"
projectiletype = /obj/item/projectile/f13plasma/repeater
projectilesound = 'sound/f13weapons/plasmarifle.ogg'
@@ -237,7 +357,7 @@
SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT)
)
-// Enclave corpses
+// ENCLAVE CORPSES
/obj/effect/mob_spawn/human/corpse/enclavescientist
name = "enclave scientist"
uniform = /obj/item/clothing/under/f13/enclave/science
@@ -261,66 +381,107 @@
gloves = /obj/item/clothing/gloves/f13/military
mask = /obj/item/clothing/mask/gas/enclave
-
/////////////////////
// BROTHERHOOD NPC //
/////////////////////
+// BASE BROTHERHOOD
/mob/living/simple_animal/hostile/bs
- name = "BS"
+ name = "Brotherhood Knight"
desc = "The brotherhood never fails."
icon = 'icons/fallout/mobs/humans/fallout_npc.dmi'
icon_state = "bs_knight"
icon_living = "bs_knight"
icon_dead = "bs_knight"
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- faction = list("BOS")
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
- stat_attack = 1
- robust_searching = 1
+ mob_armor = ARMOR_VALUE_BOS
+
maxHealth = 200
health = 200
- harm_intent_damage = 8
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 7
melee_damage_upper = 15
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "pistol-whips"
attack_sound = 'sound/weapons/punch1.ogg'
- a_intent = INTENT_HARM
- loot = list(/obj/effect/mob_spawn/human/corpse/bs)
+
+ speak = list("Semper Invicta!")
+ speak_emote = list("rushes")
+ speak_chance = 1
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
- check_friendly_fire = 1
+
+ faction = list("BOS")
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
del_on_death = TRUE
- speak = list("Semper Invicta!")
- speak_emote = list("rushes")
- speak_chance = 1
-
-/obj/effect/mob_spawn/human/corpse/bs
- name = "Brotherhood Knight"
- uniform = /obj/item/clothing/under/syndicate/brotherhood
- suit = /obj/item/clothing/suit/armor/medium/combat/brotherhood
- shoes = /obj/item/clothing/shoes/combat/swat
- gloves = /obj/item/clothing/gloves/combat
- belt = /obj/item/storage/belt/army/assault
- mask = /obj/item/clothing/mask/gas/sechailer
- head = /obj/item/clothing/head/helmet/f13/combat/brotherhood
-
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/bs)
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/bs/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/bs/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - brotherhood members are well-trained
+/mob/living/simple_animal/hostile/bs/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/bs))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// BROTHERHOOD KNIGHT - laser pistol
/mob/living/simple_animal/hostile/bs/knight
name = "Brotherhood Knight"
desc = "A Brotherhood Knight wielding a laser pistol and older issue Brotherhood combat armor."
- icon_state = "bs_knight"
- icon_living = "bs_knight"
- icon_dead = "bs_knight"
+
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/bs)
- healable = 1
- ranged = 1
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/beam/laser/pistol/hitscan
projectilesound = 'sound/f13weapons/aep7fire.ogg'
projectile_sound_properties = list(
@@ -334,20 +495,30 @@
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
+// BROTHERHOOD PALADIN - laser rifle + power armor
/mob/living/simple_animal/hostile/bs/paladin
name = "Brotherhood Paladin"
desc = "A Paladin equipped with an AER9 and T-51b power armor. The Brotherhood has arrived."
icon_state = "bs_paladin"
icon_living = "bs_paladin"
icon_dead = "bs_paladin"
- retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/bs/paladin)
+
+ mob_armor = ARMOR_VALUE_BOS_PALADIN
+
maxHealth = 480
health = 480
stat_attack = UNCONSCIOUS
- healable = 1
- ranged = 1
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/bs/paladin)
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/beam/laser/lasgun/hitscan
projectilesound = 'sound/f13weapons/aer9fire.ogg'
projectile_sound_properties = list(
@@ -361,74 +532,128 @@
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
+// BROTHERHOOD CORPSES
+/obj/effect/mob_spawn/human/corpse/bs
+ name = "Brotherhood Knight"
+ uniform = /obj/item/clothing/under/syndicate/brotherhood
+ suit = /obj/item/clothing/suit/armor/medium/combat/brotherhood
+ shoes = /obj/item/clothing/shoes/combat/swat
+ gloves = /obj/item/clothing/gloves/combat
+ belt = /obj/item/storage/belt/army/assault
+ mask = /obj/item/clothing/mask/gas/sechailer
+ head = /obj/item/clothing/head/helmet/f13/combat/brotherhood
+
/obj/effect/mob_spawn/human/corpse/bs/paladin
name = "Brotherhood Paladin"
uniform = /obj/item/clothing/under/f13/recon
suit = /obj/item/clothing/suit/armor/power_armor/t51b/bos
shoes = /obj/item/clothing/shoes/combat/swat
gloves = /obj/item/clothing/gloves/combat
- belt = /obj/item/storage/belt/army/assault
+ belt = /obj/item/storage/belt/army/assault
mask = /obj/item/clothing/mask/gas/sechailer
head = /obj/item/clothing/head/helmet/f13/power_armor/t51b/bos
-
///////////////
// NCR = NPC //
///////////////
+// BASE NCR
/mob/living/simple_animal/hostile/ncr
- name = "NCR"
+ name = "NCR Trooper"
desc = "For the Republic!"
icon = 'icons/fallout/mobs/humans/fallout_npc.dmi'
icon_state = "ncr_trooper"
icon_living = "ncr_trooper"
icon_dead = "ncr_trooper"
- faction = list("NCR")
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
- stat_attack = 1
- robust_searching = 1
+ mob_armor = ARMOR_VALUE_NCR
+
maxHealth = 120
health = 120
- harm_intent_damage = 8
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 8
melee_damage_upper = 15
- attack_verb_simple = "áüåò"
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
+ attack_verb_simple = "attacks"
attack_sound = 'sound/weapons/punch1.ogg'
- a_intent = INTENT_HARM
- loot = list(/obj/effect/mob_spawn/human/corpse/ncr)
+
+ speak = list("Patrolling the Mojave almost makes you wish for a nuclear winter.", "When I got this assignment I was hoping there would be more gambling.", "It's been a long tour, all I can think about now is going back home.", "You know, if you were serving, you'd probably be halfway to general by now.", "You oughtta think about enlisting. We need you here.")
+ speak_emote = list("says")
+ speak_chance = 1
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
- check_friendly_fire = 1
+
+ faction = list("NCR")
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
del_on_death = TRUE
- speak = list("Patrolling the Mojave almost makes you wish for a nuclear winter.", "When I got this assignment I was hoping there would be more gambling.", "It's been a long tour, all I can think about now is going back home.", "You know, if you were serving, you'd probably be halfway to general by now.", "You oughtta think about enlisting. We need you here.")
- speak_emote = list("says")
- speak_chance = 1
-
-/obj/effect/mob_spawn/human/corpse/ncr
- name = "NCR Trooper"
- uniform = /obj/item/clothing/under/f13/ncr
- suit = /obj/item/clothing/suit/armor/ncrarmor
- belt = /obj/item/storage/belt/army/assault/ncr
- shoes = /obj/item/clothing/shoes/f13/military/ncr
- head = /obj/item/clothing/head/f13/ncr
-
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/ncr)
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/ncr/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/ncr/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - NCR troopers are trained soldiers
+/mob/living/simple_animal/hostile/ncr/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/ncr))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// NCR TROOPER - service rifle
/mob/living/simple_animal/hostile/ncr/trooper
name = "NCR Trooper"
desc = "A standard NCR Trooper wielding a service rifle and equipped with a patrol vest."
- icon_state = "ncr_trooper"
- icon_living = "ncr_trooper"
- icon_dead = "ncr_trooper"
+
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/ncr)
- healable = 1
- ranged = 1
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/bullet/a556/simple
projectilesound = 'sound/f13weapons/varmint_rifle.ogg'
casingtype = /obj/item/ammo_casing/a556
@@ -443,19 +668,29 @@
SP_DISTANT_RANGE(RIFLE_LIGHT_RANGE_DISTANT)
)
+// NCR RANGER - revolver
/mob/living/simple_animal/hostile/ncr/ranger
name = "NCR Ranger"
desc = "A Ranger of the NCRA, wielding a big iron on his hip and equipped with a ranger patrol vest."
icon_state = "ncr_sergeant"
icon_living = "ncr_sergeant"
icon_dead = "ncr_sergeant"
- retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/ncr/ranger)
+
+ mob_armor = ARMOR_VALUE_NCR_RANGER
+
maxHealth = 160
health = 160
- healable = 1
- ranged = 1
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/ncr/ranger)
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/bullet/m44/simple
projectilesound = 'sound/f13weapons/44mag.ogg'
casingtype = /obj/item/ammo_casing/m44
@@ -469,6 +704,16 @@
SP_DISTANT_SOUND(PISTOL_HEAVY_DISTANT_SOUND),
SP_DISTANT_RANGE(PISTOL_HEAVY_RANGE_DISTANT)
)
+
+// NCR CORPSES
+/obj/effect/mob_spawn/human/corpse/ncr
+ name = "NCR Trooper"
+ uniform = /obj/item/clothing/under/f13/ncr
+ suit = /obj/item/clothing/suit/armor/ncrarmor
+ belt = /obj/item/storage/belt/army/assault/ncr
+ shoes = /obj/item/clothing/shoes/f13/military/ncr
+ head = /obj/item/clothing/head/f13/ncr
+
/obj/effect/mob_spawn/human/corpse/ncr/ranger
name = "NCR Ranger"
uniform = /obj/item/clothing/under/f13/ranger/patrol
@@ -481,59 +726,103 @@
// LEGION NPC //
////////////////
+// BASE LEGION
/mob/living/simple_animal/hostile/legion
- name = "Legion"
+ name = "Legion Prime"
desc = "True to Caesar."
icon = 'icons/fallout/mobs/humans/fallout_npc.dmi'
icon_state = "legion_prime"
icon_living = "legion_prime"
icon_dead = "legion_prime"
- faction = list("Legion")
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
- stat_attack = 1
- robust_searching = 1
+ mob_armor = ARMOR_VALUE_LEGION
+
maxHealth = 120
health = 120
- harm_intent_damage = 8
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 8
melee_damage_upper = 15
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "attacks"
attack_sound = 'sound/weapons/punch1.ogg'
- a_intent = INTENT_HARM
- loot = list(/obj/effect/mob_spawn/human/corpse/legion)
+
+ speak = list("Ave, true to Caesar.", "True to Caesar.", "Ave, Amicus.", "The new slave girls are quite beautiful.", "Give me cause, Profligate.", "Degenerates like you belong on a cross.")
+ speak_emote = list("says")
+ speak_chance = 1
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 15
- check_friendly_fire = 1
+
+ faction = list("Legion")
+ a_intent = INTENT_HARM
+ check_friendly_fire = TRUE
status_flags = CANPUSH
del_on_death = TRUE
- speak = list("Ave, true to Caesar.", "True to Caesar.", "Ave, Amicus.", "The new slave girls are quite beautiful.", "Give me cause, Profligate.", "Degenerates like you belong on a cross.")
- speak_emote = list("says")
- speak_chance = 1
-
-/obj/effect/mob_spawn/human/corpse/legion
- name = "Legion Prime"
- uniform = /obj/item/clothing/under/f13/legskirt
- suit = /obj/item/clothing/suit/armor/legion/prime
- shoes = /obj/item/clothing/shoes/f13/military/legion
- head = /obj/item/clothing/head/helmet/f13/legion/prime
-
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/legion)
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/legion/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/legion/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - legion soldiers fight in formation
+/mob/living/simple_animal/hostile/legion/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/legion))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// LEGION PRIME - hunting rifle
/mob/living/simple_animal/hostile/legion/prime
name = "Legion Prime"
desc = "A Prime Legionary, equipped with a hunting rifle."
- icon_state = "legion_prime"
- icon_living = "legion_prime"
- icon_dead = "legion_prime"
- icon_gib = "legion_prime"
+
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/legion)
- healable = 1
- ranged = 1
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/bullet/a762/sport/simple
projectilesound = 'sound/f13weapons/hunting_rifle.ogg'
casingtype = /obj/item/ammo_casing/a308
@@ -548,6 +837,7 @@
SP_DISTANT_RANGE(RIFLE_MEDIUM_RANGE_DISTANT)
)
+// LEGION DECANUS - veteran officer
/mob/living/simple_animal/hostile/legion/decan
name = "Legion Decanus"
desc = "A Prime Decanus, equipped with a hunting rifle."
@@ -555,13 +845,22 @@
icon_living = "legion_decan"
icon_dead = "legion_decan"
icon_gib = "legion_decan"
- retreat_distance = 5
- minimum_distance = 5
- loot = list(/obj/effect/mob_spawn/human/corpse/legion/decan)
+
+ mob_armor = ARMOR_VALUE_LEGION_VETERAN
+
maxHealth = 180
health = 180
- healable = 1
- ranged = 1
+
+ loot = list(/obj/effect/mob_spawn/human/corpse/legion/decan)
+ healable = TRUE
+
+ // Pure ranged
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
projectiletype = /obj/item/projectile/bullet/a762/sport/simple
projectilesound = 'sound/f13weapons/hunting_rifle.ogg'
casingtype = /obj/item/ammo_casing/a308
@@ -576,7 +875,13 @@
SP_DISTANT_RANGE(RIFLE_MEDIUM_RANGE_DISTANT)
)
-/mob/living/simple_animal/hostile/legion/decan
+// LEGION CORPSES
+/obj/effect/mob_spawn/human/corpse/legion
+ name = "Legion Prime"
+ uniform = /obj/item/clothing/under/f13/legskirt
+ suit = /obj/item/clothing/suit/armor/legion/prime
+ shoes = /obj/item/clothing/shoes/f13/military/legion
+ head = /obj/item/clothing/head/helmet/f13/legion/prime
/obj/effect/mob_spawn/human/corpse/legion/decan
name = "Legion Decanus"
@@ -597,26 +902,63 @@
icon_state = "tribal_raider"
icon_living = "tribal_raider"
icon_dead = "tribal_raider_dead"
- faction = list("Tribe")
+
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- turns_per_move = 5
- response_help_simple = "pokes"
- response_disarm_simple = "shoves"
- response_harm_simple = "hits"
- speed = 1
- stat_attack = 1
- robust_searching = 1
+ mob_armor = ARMOR_VALUE_TRIBAL
+
maxHealth = 160
health = 160
- harm_intent_damage = 8
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 22
melee_damage_upper = 47
+ harm_intent_damage = 8
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ response_help_simple = "pokes"
+ response_disarm_simple = "shoves"
+ response_harm_simple = "hits"
attack_verb_simple = "attacks"
attack_sound = 'sound/weapons/bladeslice.ogg'
- a_intent = INTENT_HARM
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 15
- status_flags = CANPUSH
+
speak = list("For our kin!", "This will be a good hunt.", "The gods look upon me today.")
speak_emote = list("says")
speak_chance = 1
+
+ atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+ unsuitable_atmos_damage = 15
+
+ faction = list("Tribe")
+ a_intent = INTENT_HARM
+ status_flags = CANPUSH
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee - glaive fighter
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/tribe/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/tribe/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm b/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm
index ef8f558945f..db1c135675f 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm
@@ -1,8 +1,11 @@
/* IN THIS FILE
--Ghouls
+- Ghouls
*/
-//Base Ghoul
+// ============================================================
+// BASE GHOUL
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul
name = "feral ghoul"
desc = "A ghoul that has lost its mind and become aggressive."
@@ -13,38 +16,27 @@
can_ghost_into = TRUE
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
mob_armor = ARMOR_VALUE_GHOUL_NAKED
- maxHealth = 40
+ maxHealth = 40
health = 40
robust_searching = 1
- move_to_delay = 2.0
+ move_to_delay = 3
turns_per_move = 5
waddle_amount = 2
waddle_up_time = 1
waddle_side_time = 1
- speak_emote = list(
- "growls",
- )
- emote_see = list(
- "sniffs the air",
- "growls",
- "foams at the mouth",
- )
+
+ speak_emote = list("growls")
+ emote_see = list("sniffs the air", "growls", "foams at the mouth")
+
a_intent = INTENT_HARM
speed = 1
harm_intent_damage = 8
melee_damage_lower = 6
melee_damage_upper = 12
- attack_verb_simple = list(
- "claws",
- "maims",
- "bites",
- "mauls",
- "slashes",
- "thrashes",
- "bashes",
- "glomps"
- )
+
+ attack_verb_simple = list("claws", "maims", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps")
attack_sound = 'sound/hallucinations/growl1.ogg'
+
atmos_requirements = list(
"min_oxy" = 5,
"max_oxy" = 0,
@@ -54,49 +46,38 @@
"max_co2" = 5,
"min_n2" = 0,
"max_n2" = 0
- )
-
+ )
+
unsuitable_atmos_damage = 20
gold_core_spawnable = HOSTILE_SPAWN
faction = list("hostile", "ghoul")
decompose = TRUE
- sharpness = SHARP_EDGED //They need to cut their finger nails
+ sharpness = SHARP_EDGED
+
guaranteed_butcher_results = list(
/obj/item/reagent_containers/food/snacks/meat/slab/human/ghoul = 2,
/obj/item/stack/sheet/animalhide/human = 1,
/obj/item/stack/sheet/bone = 1
- )
+ )
emote_taunt_sound = list('sound/f13npc/ghoul/taunt.ogg')
- emote_taunt = list(
- "gurgles",
- "stares",
- "foams at the mouth",
- "groans",
- "growls",
- "jibbers",
- "howls madly",
- "screeches",
- "charges"
- )
+ emote_taunt = list("gurgles", "stares", "foams at the mouth", "groans", "growls", "jibbers", "howls madly", "screeches", "charges")
tastes = list("decay" = 1, "mud" = 1)
taunt_chance = 30
aggrosound = list('sound/f13npc/ghoul/aggro1.ogg', 'sound/f13npc/ghoul/aggro2.ogg')
idlesound = list('sound/f13npc/ghoul/idle.ogg', 'sound/effects/scrungy.ogg')
death_sound = 'sound/f13npc/ghoul/ghoul_death.ogg'
+
loot = list(/obj/item/stack/f13Cash/random/low/lowchance)
- /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list
loot_drop_amount = 1
- /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items
loot_amount_random = TRUE
- /// slots in a list of trash loot
var/random_trash_loot = TRUE
+
footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = TRUE
desc_short = "A flimsy creature that may or may not be a reanimated corpse."
pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
-
+
variation_list = list(
MOB_COLOR_VARIATION(150, 150, 150, 255, 255, 255),
MOB_SPEED_LIST(2.3, 2.5, 2.8, 2.9, 3.0),
@@ -109,30 +90,55 @@
)
can_z_move = TRUE
- z_move_delay = 50 // 5 seconds - slower
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ can_open_doors = TRUE
+ can_open_airlocks = FALSE
+
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
/mob/living/simple_animal/hostile/ghoul/Initialize()
. = ..()
if(random_trash_loot)
loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment
-
/mob/living/simple_animal/hostile/ghoul/Aggro()
. = ..()
if(.)
return
- summon_backup(15)
+ summon_backup(10)
if(!ckey)
say(pick("*scrungy", "*mbark"))
+// Friendly fire resistance - ghouls are tough and fight in packs
+/mob/living/simple_animal/hostile/ghoul/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/ghoul))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// The parent /hostile already handles stat_attack = CONSCIOUS correctly.
+// The override was also incorrectly blocking legendary ghoul's stat_attack = UNCONSCIOUS
+// from working, since it ran before the parent check.
/mob/living/simple_animal/hostile/ghoul/become_the_mob(mob/user)
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/ghoul
send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/ghoul
. = ..()
+// ============================================================
+// GHOUL REAVER
+// Armored, throws rocks at range, stronger than base ghoul
+// ============================================================
-// Ghoul Reaver
/mob/living/simple_animal/hostile/ghoul/reaver
name = "feral ghoul reaver"
desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger."
@@ -144,21 +150,16 @@
maxHealth = 50
health = 50
rapid_melee = 2
- retreat_distance = 3
- minimum_distance = 1
- ranged = TRUE
- ranged_message = "throws a rock"
- ranged_cooldown_time = 3 SECONDS
- projectiletype = /obj/item/projectile/bullet/ghoul_rock
- projectilesound = 'sound/weapons/punchmiss.ogg'
+ move_to_delay = 2.5
+
harm_intent_damage = 8
melee_damage_lower = 8
melee_damage_upper = 14
+
loot = list(/obj/item/stack/f13Cash/random/low/medchance)
loot_drop_amount = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = TRUE
pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ desc_short = "A beefy creature that may or may not be a reanimated corpse."
variation_list = list(
MOB_COLOR_VARIATION(200, 200, 200, 255, 255, 255),
@@ -175,35 +176,52 @@
MOB_PROJECTILE_ENTRY(/obj/item/projectile/bullet/ghoul_rock/jagged_scrap, 1)\
)
)
- desc_short = "A beefy creature that may or may not be a reanimated corpse."
-/mob/living/simple_animal/hostile/ghoul/reaver/Initialize()
- . = ..()
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 3
+ minimum_distance = 1
+ ranged_message = "throws a rock"
+ ranged_cooldown_time = 3 SECONDS
+ projectiletype = /obj/item/projectile/bullet/ghoul_rock
+ projectilesound = 'sound/weapons/punchmiss.ogg'
/mob/living/simple_animal/hostile/ghoul/reaver/Aggro()
- ..()
- summon_backup(10)
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+// Military ghoul variants - former US Army, can't be ghost-inhabited
/mob/living/simple_animal/hostile/ghoul/reaver/ncr
name = "feral ghoul soldier"
desc = "A former US Army combatant, now ghoulified and insane. The armor that failed it in life still packs some good defense."
maxHealth = 60
+ health = 60
can_ghost_into = FALSE
/mob/living/simple_animal/hostile/ghoul/reaver/ncr_helmet
name = "plated feral ghoul soldier"
desc = "A former US Army combatant, now ghoulified and insane. The armor that failed it in life still packs some good defense."
maxHealth = 60
+ health = 60
can_ghost_into = FALSE
/mob/living/simple_animal/hostile/ghoul/reaver/ncr_officer
name = "feral ghoul officer"
desc = "A former US Army officer, now ghoulified and insane. The armor that failed it in life still packs some good defense."
maxHealth = 60
+ health = 60
speed = 3
can_ghost_into = FALSE
-//Cold Feral Ghoul
+// ============================================================
+// COLD / FROZEN GHOULS
+// FIX: frozenreaver now inherits from coldferal to avoid duplication
+// NOTE: Neither has atmos_requirements override - they still need oxygen.
+// Add vacuum-safe atmos list here if they're meant for cold/airless maps.
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/coldferal
name = "cold ghoul feral"
desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger."
@@ -218,28 +236,21 @@
melee_damage_upper = 15
loot = list(/obj/item/stack/f13Cash/random/low/medchance)
loot_drop_amount = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
-//Frozen Feral Ghoul
-/mob/living/simple_animal/hostile/ghoul/frozenreaver
+/mob/living/simple_animal/hostile/ghoul/coldferal/frozenreaver
name = "frozen ghoul reaver"
desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger."
icon_state = "frozen_reaver"
icon_living = "frozen_reaver"
icon_dead = "frozen_reaver_dead"
- speed = 1.5
- maxHealth = 80
- health = 80
- harm_intent_damage = 8
- melee_damage_lower = 10
- melee_damage_upper = 15
- loot = list(/obj/item/stack/f13Cash/random/low/medchance)
loot_drop_amount = 4
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = FALSE
-//Legendary Ghoul
+// ============================================================
+// LEGENDARY GHOUL
+// Enrages at 30% HP - faster and more dangerous
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/legendary
name = "legendary ghoul"
desc = "A ghoul that has lost its mind and become aggressive. This one has exceptionally large, bulging muscles. It looks quite strong."
@@ -249,30 +260,68 @@
color = "#FFFF00"
mob_armor = ARMOR_VALUE_GHOUL_LEGEND
can_ghost_into = FALSE
- maxHealth = 200
- health = 200
- stat_attack = UNCONSCIOUS
+ maxHealth = 160
+ health = 160
+ stat_attack = UNCONSCIOUS // Can attack downed players
speed = 2.5
harm_intent_damage = 8
melee_damage_lower = 20
melee_damage_upper = 35
- mob_size = 5
+ mob_size = MOB_SIZE_HUGE // FIX: was raw 5, use the defined constant
wound_bonus = 0
bare_wound_bonus = 0
loot = list(/obj/item/stack/f13Cash/random/med)
loot_drop_amount = 5
loot_amount_random = FALSE
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = FALSE //heeeeeell no
pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
desc_short = "A deadly creature that may or may not be reanimated jerky."
+ low_health_threshold = 0.3 // Enrages at 30% HP
+ var/color_rage = "#ff6666"
+
/mob/living/simple_animal/hostile/ghoul/legendary/become_the_mob(mob/user)
call_backup = null
send_mobs = null
. = ..()
-//Glowing Ghoul
+/// RAGE MODE - legendary ghoul becomes faster and deadlier at low health
+/mob/living/simple_animal/hostile/ghoul/legendary/make_low_health()
+ if(!target)
+ return // Not in combat, don't rage
+ ..()
+ visible_message(span_danger("[src] howls with primal fury!!!"))
+ playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15))
+ color = color_rage
+ speed *= 0.7
+ melee_damage_lower = round(melee_damage_lower * 1.4)
+ melee_damage_upper = round(melee_damage_upper * 1.4)
+ wound_bonus += 15
+ bare_wound_bonus += 30
+ is_low_health = TRUE
+
+/// Calming down from rage (health recovered above threshold)
+/mob/living/simple_animal/hostile/ghoul/legendary/make_high_health()
+ visible_message(span_notice("[src]'s fury subsides."))
+ color = initial(color)
+ speed = initial(speed)
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ wound_bonus = initial(wound_bonus)
+ bare_wound_bonus = initial(bare_wound_bonus)
+ is_low_health = FALSE
+
+/mob/living/simple_animal/hostile/ghoul/legendary/Aggro()
+ . = ..()
+ if(is_low_health)
+ return
+ if(health < (maxHealth * low_health_threshold))
+ make_low_health()
+
+// ============================================================
+// GLOWING GHOUL
+// Ranged radiation attacker, heals nearby ghouls passively
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/glowing
name = "glowing feral ghoul"
desc = "A feral ghoul that has absorbed massive amounts of radiation, causing them to glow in the dark and radiate constantly."
@@ -280,22 +329,14 @@
icon_living = "glowinghoul"
icon_dead = "glowinghoul_dead"
mob_armor = ARMOR_VALUE_GHOUL_GLOWING
- maxHealth = 40
+ maxHealth = 40
health = 40
speed = 2
- retreat_distance = 4
- minimum_distance = 4
- ranged_message = "emits radiation"
- ranged = TRUE
- projectiletype = /obj/item/projectile/radiation_thing
- projectilesound = 'sound/weapons/etherealhit.ogg'
harm_intent_damage = 8
melee_damage_lower = 10
melee_damage_upper = 22
light_system = MOVABLE_LIGHT
light_range = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = TRUE
pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
desc_short = "A glowing creature that may or may not be a reanimated corpse."
loot_drop_amount = 2
@@ -311,23 +352,25 @@
MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50)
)
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 4
+ ranged_message = "emits radiation"
+ ranged_cooldown_time = 2 SECONDS
+ projectiletype = /obj/item/projectile/radiation_thing
+ projectilesound = 'sound/weapons/etherealhit.ogg'
+
/mob/living/simple_animal/hostile/ghoul/glowing/Initialize(mapload)
. = ..()
- // we only heal BRUTELOSS because each type directly heals a simplemob's health
- // therefore setting it to BRUTELOSS | FIRELOSS | TOXLOSS | OXYLOSS would mean healing 4x as much
- // aka 40% of max life every tick, which is basically unkillable
- // TODO: refactor this if simple_animals ever get damage types
+ // restrict_faction = null means ALL nearby ghouls benefit from the aura
AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/ghoul, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS)
-/obj/item/projectile/radiation_thing
- name = "radiation"
- damage = 0
- irradiate = 20
- icon_state = "declone"
-
/mob/living/simple_animal/hostile/ghoul/glowing/Aggro()
- ..()
- summon_backup(10)
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
/mob/living/simple_animal/hostile/ghoul/glowing/AttackingTarget()
. = ..()
@@ -335,16 +378,20 @@
var/mob/living/carbon/human/H = target
H.apply_effect(20, EFFECT_IRRADIATE, 0)
-/mob/living/simple_animal/hostile/ghoul/glowing/strong // FEV mutation
- maxHealth = 256
- health = 256
- speed = 1.4 // Nyooom
+// FEV-mutated glowing ghoul - much tougher
+/mob/living/simple_animal/hostile/ghoul/glowing/strong
+ maxHealth = 180
+ health = 180
+ speed = 1.4
can_ghost_into = FALSE
melee_damage_lower = 25
melee_damage_upper = 30
armour_penetration = 0.1
-//Alive Ghoul
+// ============================================================
+// SOLDIER GHOULS (living/allied)
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/soldier
name = "ghoul soldier"
desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants."
@@ -352,15 +399,12 @@
icon_living = "soldier_ghoul"
icon_dead = "soldier_ghoul_d"
icon_gib = "syndicate_gib"
- mob_armor = ARMOR_VALUE_GHOUL_NAKED
- maxHealth = 60
+ maxHealth = 60
health = 60
loot = list(/obj/item/stack/f13Cash/random/low/medchance)
loot_drop_amount = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
-//Alive Ghoul
/mob/living/simple_animal/hostile/ghoul/soldier/armored
name = "armored ghoul soldier"
desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants."
@@ -368,14 +412,15 @@
icon_living = "soldier_ghoul_a"
icon_dead = "soldier_ghoul_a_d"
icon_gib = "syndicate_gib"
- mob_armor = ARMOR_VALUE_GHOUL_NAKED
- maxHealth = 80
+ maxHealth = 80
health = 80
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = FALSE
loot_drop_amount = 3
-//Alive Ghoul
+// ============================================================
+// SCORCHED GHOULS
+// Faction "scorched" - separate from base ghoul faction
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/scorched
name = "scorched ghoul soldier"
desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants."
@@ -384,98 +429,88 @@
icon_dead = "scorched_m_d"
icon_gib = "syndicate_gib"
speak_chance = 1
- environment_smash = 0
+ environment_smash = 0 // Scorched don't smash furniture unlike feral ghouls
response_help_simple = "hugs"
response_disarm_simple = "pushes aside"
response_harm_simple = "growl"
- move_to_delay = 3.0
+ move_to_delay = 3
faction = list("scorched", "hostile")
death_sound = null
melee_damage_upper = 20
aggro_vision_range = 10
attack_verb_simple = "punches"
attack_sound = "punch"
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
loot_drop_amount = 4
-//Alive Ghoul Ranged
/mob/living/simple_animal/hostile/ghoul/scorched/ranged
- name = "Ranged Ghoul Solder"
+ name = "ranged ghoul soldier"
desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants."
icon_state = "scorched_r"
icon_living = "scorched_r"
icon_dead = "scorched_r_d"
icon_gib = "syndicate_gib"
- speak_chance = 1
turns_per_move = 5
- environment_smash = 0
- response_help_simple = "hugs"
- response_disarm_simple = "pushes aside"
- response_harm_simple = "ow"
- move_to_delay = 3.0
- ranged = TRUE
- ranged_cooldown_time = 200
- projectiletype = /obj/item/projectile/bullet/c9mm/simple
- projectilesound = 'sound/f13weapons/hunting_rifle.ogg'
- faction = list("scorched", "hostile")
melee_damage_lower = 15
melee_damage_upper = 20
- aggro_vision_range = 10
attack_verb_simple = "shoots"
- attack_sound = "punch"
- footstep_type = FOOTSTEP_MOB_BAREFOOT
- can_ghost_into = FALSE
loot_drop_amount = 5
-//Sunset mob of some sort?
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+ ranged_cooldown_time = 2 SECONDS
+ projectiletype = /obj/item/projectile/bullet/c9mm/simple
+ projectilesound = 'sound/f13weapons/hunting_rifle.ogg'
+
+// ============================================================
+// WYOMING GHOST SOLDIER
+// NOTE: faction includes "supermutant" intentionally - this mob is
+// allied with both supermutants and ghouls as a Sunset event enemy.
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/wyomingghost
name = "ghost soldier"
desc = "A figure clad in armor that stands silent except for the slight wheezing coming from them, a dark orange and black liquid pumps through a clear tube into the gas mask. The armor they wear seems to be sealed to their skin."
icon_state = "wyomingghost"
icon_living = "wyomingghost"
icon_dead = "wyomingghost_dead"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
robust_searching = 1
turns_per_move = 5
speak_emote = list("wheezes")
emote_see = list("stares")
- a_intent = INTENT_HARM
- maxHealth = 150
- health = 150
+ maxHealth = 140
+ health = 140
speed = 2
harm_intent_damage = 8
melee_damage_lower = 15
melee_damage_upper = 15
attack_verb_simple = "attacks"
- attack_sound = 'sound/hallucinations/growl1.ogg'
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 20
- gold_core_spawnable = HOSTILE_SPAWN
- faction = list("supermutant","ghoul")
+ faction = list("supermutant", "ghoul") // Allied with both factions - intentional for Sunset event
decompose = FALSE
- sharpness = SHARP_EDGED //They need to cut their finger nails
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
loot_drop_amount = 5
-//Halloween Event Ghouls
+// ============================================================
+// ZOMBIE GHOULS (Halloween event)
+// Infect humans on hit - spreads ghoulification
+// ============================================================
+
/mob/living/simple_animal/hostile/ghoul/zombie
name = "ravenous feral ghoul"
desc = "A ferocious feral ghoul, hungry for human meat."
faction = list("ghoul")
stat_attack = CONSCIOUS
- can_ghost_into = FALSE
- maxHealth = 200
- health = 200
- footstep_type = FOOTSTEP_MOB_BAREFOOT
+ maxHealth = 170
+ health = 170
can_ghost_into = FALSE
/mob/living/simple_animal/hostile/ghoul/zombie/AttackingTarget()
. = ..()
if(. && ishuman(target))
var/mob/living/carbon/human/H = target
- try_to_ghoul_zombie_infect(H)
+ try_to_ghoul_zombie_infect(H) // Defined in disease/infection code
/mob/living/simple_animal/hostile/ghoul/zombie/reaver
name = "ravenous feral ghoul reaver"
@@ -484,44 +519,56 @@
icon_living = "ghoulreaver"
icon_dead = "ghoulreaver_dead"
speed = 2
- maxHealth = 216
- health = 216
- can_ghost_into = FALSE
+ maxHealth = 130
+ health = 130
harm_intent_damage = 8
melee_damage_lower = 30
melee_damage_upper = 30
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 3
+ minimum_distance = 1
+ ranged_message = "throws a rock"
+ ranged_cooldown_time = 3 SECONDS
+ projectiletype = /obj/item/projectile/bullet/ghoul_rock
+ projectilesound = 'sound/weapons/punchmiss.ogg'
+
/mob/living/simple_animal/hostile/ghoul/zombie/glowing
name = "ravenous glowing feral ghoul"
desc = "A ferocious feral ghoul, hungry for human meat. This one has absorbed massive amounts of radiation, causing them to glow in the dark and radiate constantly."
icon_state = "glowinghoul"
icon_living = "glowinghoul"
icon_dead = "glowinghoul_dead"
- maxHealth = 192
- health = 192
+ maxHealth = 120
+ health = 120
speed = 2
- can_ghost_into = FALSE
harm_intent_damage = 8
melee_damage_lower = 30
melee_damage_upper = 30
light_system = MOVABLE_LIGHT
light_range = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 4
+ ranged_message = "emits radiation"
+ ranged_cooldown_time = 2 SECONDS
+ projectiletype = /obj/item/projectile/radiation_thing
+ projectilesound = 'sound/weapons/etherealhit.ogg'
+
/mob/living/simple_animal/hostile/ghoul/zombie/glowing/Initialize(mapload)
. = ..()
- // we only heal BRUTELOSS because each type directly heals a simplemob's health
- // therefore setting it to BRUTELOSS | FIRELOSS | TOXLOSS | OXYLOSS would mean healing 4x as much
- // aka 40% of max life every tick, which is basically unkillable
- // TODO: refactor this if simple_animals ever get damage types
AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/ghoul, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS)
/mob/living/simple_animal/hostile/ghoul/zombie/glowing/Aggro()
- ..()
- summon_backup(10)
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
/mob/living/simple_animal/hostile/ghoul/zombie/glowing/AttackingTarget()
. = ..()
@@ -536,16 +583,13 @@
icon_living = "glowinghoul"
icon_dead = "glowinghoul_dead"
color = "#FFFF00"
- maxHealth = 200
- health = 200
- can_ghost_into = FALSE
+ maxHealth = 170
+ health = 170
speed = 2.5
harm_intent_damage = 8
melee_damage_lower = 30
melee_damage_upper = 35
- mob_size = 5
+ mob_size = MOB_SIZE_HUGE
wound_bonus = 0
bare_wound_bonus = 0
- footstep_type = FOOTSTEP_MOB_BAREFOOT
can_ghost_into = FALSE
-
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/insects.dm b/code/modules/mob/living/simple_animal/hostile/f13/insects.dm
index da78c49ba2e..1345360a0e0 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/insects.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/insects.dm
@@ -1,124 +1,191 @@
// INSECTS - In this document: Giant ant, Radscorpion, Cazador, Radroach, Bloatfly, cazador venom
-
///////////////
// GIANT ANT //
///////////////
+// BASE GIANT ANT
/mob/living/simple_animal/hostile/giantant
name = "giant ant"
- desc = "A giant ant with twitching, darting antennae. Its outsides are a mixture of crusted, unrotting rock and chitin that bounce off bullets and melee weapons. Hardened insides compact once valueless sand and dirt to gemstones. Many a fool in their search for wealth have become part of the gemstones. Can be butchered down the thorax for minerals and shinies."
+ desc = "A giant ant with twitching, darting antennae. Its outsides are a mixture of crusted, unrotting rock and chitin that bounce off bullets and melee weapons. Hardened insides compact once valueless sand and dirt to gemstones. Can be butchered down the thorax for minerals and shinies."
icon = 'icons/fallout/mobs/animals/insects.dmi'
icon_state = "GiantAnt"
icon_living = "GiantAnt"
icon_dead = "GiantAnt_dead"
icon_gib = "GiantAnt_gib"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
mob_armor = ARMOR_VALUE_ANTS
- speak_chance = 0
+
+ maxHealth = 110
+ health = 110
+ speed = 1
move_to_delay = 3
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 0
- //how far they pull back
-
- minimum_distance = 0
- // how close you can get before they try to pull back
-
- aggro_vision_range = 4 //due to ants poor eyesight
- //tiles within they start attacking, doesn't count the mobs tile
-
- vision_range = 5
- //tiles within they start making noise, does count the mobs tile
-
- speak_emote = list("clacks", "chitters", "snips", "snaps")
- emote_see = list("waggles its antenna", "clicks its mandibles", "picks up your scent", "goes on the hunt")
- attack_verb_simple = list ("rips", "tears", "stings")
turns_per_move = 5
- guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 1, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 2, /obj/item/stack/sheet/animalhide/chitin = 1)
- butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1)
+
+ melee_damage_lower = 6
+ melee_damage_upper = 20
+ harm_intent_damage = 8
+ obj_damage = 20
+
+ aggro_vision_range = 4 // Poor eyesight
+ vision_range = 5
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 2,
+ /obj/item/stack/sheet/animalhide/chitin = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
butcher_difficulty = 1.5
+
+ waddle_amount = 2
+ waddle_up_time = 1
+ waddle_side_time = 1
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
+
+ speak_emote = list("skitters", "clacks", "chitters", "snips", "snaps")
+ emote_see = list("waggles its antenna", "clicks its mandibles", "picks up your scent", "goes on the hunt")
+ attack_verb_simple = list("rips", "tears", "stings")
+ attack_sound = 'sound/creatures/radroach_attack.ogg'
+
emote_taunt = list("chitters")
emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg'
taunt_chance = 30
- speed = 1
- waddle_amount = 2
- waddle_up_time = 1
- waddle_side_time = 1
- maxHealth = 110
- health = 110
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 6
- melee_damage_upper = 20
- attack_verb_simple = "stings"
- attack_sound = 'sound/creatures/radroach_attack.ogg'
- speak_emote = list("skitters")
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("ant")
- gold_core_spawnable = HOSTILE_SPAWN
a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
blood_volume = 0
decompose = FALSE
tastes = list("dirt" = 1, "sand" = 1, "metal?" = 1)
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ // Can open doors
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
-/mob/living/simple_animal/hostile/giantant/Initialize()
+/mob/living/simple_animal/hostile/giantant/Aggro()
. = ..()
+ if(.)
+ return
+ summon_backup(8)
-/mob/living/simple_animal/hostile/giantant/Aggro()
- ..()
- summon_backup(10)
+/mob/living/simple_animal/hostile/giantant/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
-// FIREANT
+// FIRE ANT - applies burning reagent
/mob/living/simple_animal/hostile/fireant
- name = "fireant"
- desc = "A large reddish ant. The furnace it holds inside itself blasts intruders and the dirt it chews with flaming heat. Its insides contain more gemstones than its unremarkable kin, accessible by butchering them straight down the thorax."
+ name = "fire ant"
+ desc = "A large reddish ant. The furnace it holds inside itself blasts intruders and the dirt it chews with flaming heat. Its insides contain more gemstones than its unremarkable kin."
icon = 'icons/fallout/mobs/animals/insects.dmi'
icon_state = "FireAnt"
icon_living = "FireAnt"
icon_dead = "FireAnt_dead"
icon_gib = "FireAnt_gib"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
+ mob_armor = ARMOR_VALUE_ANTS
+
+ maxHealth = 90
+ health = 90
+ speed = 1
turns_per_move = 5
+
+ melee_damage_lower = 8
+ melee_damage_upper = 16
+ harm_intent_damage = 8
+ obj_damage = 20
+
+ aggro_vision_range = 4
+ vision_range = 5
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/reagent_containers/food/snacks/meat/slab/fireant_meat = 2,
+ /obj/item/reagent_containers/food/snacks/rawantbrain = 1,
+ /obj/item/stack/sheet/animalhide/chitin = 2,
+ /obj/item/stack/sheet/bone = 1
+ )
+ butcher_difficulty = 1.5
+
waddle_amount = 2
waddle_up_time = 1
waddle_side_time = 1
- guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 1, /obj/item/reagent_containers/food/snacks/meat/slab/fireant_meat = 2, /obj/item/reagent_containers/food/snacks/rawantbrain = 1, /obj/item/stack/sheet/animalhide/chitin = 2)
- butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 2)
- butcher_difficulty = 1.5
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
+
+ speak_emote = list("skitters")
+ attack_verb_simple = "stings"
+ attack_sound = 'sound/creatures/radroach_attack.ogg'
+
emote_taunt = list("chitters")
emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg'
taunt_chance = 30
- speed = 1
- maxHealth = 90
- health = 90
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 8
- melee_damage_upper = 16
- attack_verb_simple = "stings"
- attack_sound = 'sound/creatures/radroach_attack.ogg'
- speak_emote = list("skitters")
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("ant")
+ a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
decompose = FALSE
- a_intent = INTENT_HARM
blood_volume = 0
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
-/mob/living/simple_animal/hostile/fireant/Initialize()
+/mob/living/simple_animal/hostile/fireant/Aggro()
. = ..()
+ if(.)
+ return
+ summon_backup(8)
-/mob/living/simple_animal/hostile/fireant/Aggro()
- ..()
- summon_backup(10)
+/mob/living/simple_animal/hostile/fireant/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/fireant/AttackingTarget()
. = ..()
@@ -126,60 +193,102 @@
var/mob/living/carbon/human/H = target
H.reagents.add_reagent(/datum/reagent/hellwater, 1)
-// ANT QUEEN
+// ANT QUEEN - boss variant, spawns ants
/mob/living/simple_animal/hostile/giantantqueen
name = "giant ant queen"
- desc = "The queen of a giant ant colony. Butchering it seems like a good way to a pretty penny."
+ desc = "The queen of a giant ant colony. Butchering it seems like a good way to make a pretty penny."
icon = 'icons/fallout/mobs/animals/antqueen.dmi'
icon_state = "antqueen"
icon_living = "antqueen"
icon_dead = "antqueen_dead"
icon_gib = "GiantAnt_gib"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
+ mob_armor = ARMOR_VALUE_ANTS_QUEEN
+
+ maxHealth = 350 // Reduced from 560
+ health = 350
+ speed = 5 // Slow
turns_per_move = 5
- guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 3, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 6, /obj/item/stack/sheet/animalhide/chitin = 6, /obj/item/reagent_containers/food/snacks/rawantbrain = 1, /obj/item/stack/sheet/animalhide/chitin = 5)
- butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 6, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 3)
+ stat_attack = UNCONSCIOUS
+
+ melee_damage_lower = 15
+ melee_damage_upper = 25 // Increased from 15
+ harm_intent_damage = 8
+ obj_damage = 20
+
+ aggro_vision_range = 5
+ vision_range = 6
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/sinew = 3,
+ /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 6,
+ /obj/item/stack/sheet/animalhide/chitin = 8,
+ /obj/item/reagent_containers/food/snacks/rawantbrain = 1,
+ /obj/item/stack/sheet/bone = 3
+ )
butcher_difficulty = 1.5
+
loot = list(/obj/item/reagent_containers/food/snacks/f13/giantantegg = 10)
+ loot_drop_amount = 10
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
+
+ speak_emote = list("skitters")
+ attack_verb_simple = "stings"
+ attack_sound = 'sound/creatures/radroach_attack.ogg'
+
emote_taunt = list("chitters")
emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg'
taunt_chance = 30
- speed = 5
- maxHealth = 560
- health = 560
- stat_attack = UNCONSCIOUS
- ranged = 1
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 15
- melee_damage_upper = 15
- attack_verb_simple = "stings"
- attack_sound = 'sound/creatures/radroach_attack.ogg'
- projectiletype = /obj/item/projectile/bile
- projectilesound = 'sound/f13npc/centaur/spit.ogg'
- extra_projectiles = 2
- speak_emote = list("skitters")
- retreat_distance = 5
- minimum_distance = 7
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("ant")
+ a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
decompose = FALSE
- a_intent = INTENT_HARM
+ blood_volume = 0
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 60
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Mixed combat - bile spit + melee
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
+ ranged_message = "spits bile"
+ ranged_cooldown_time = 4 SECONDS
+ projectiletype = /obj/item/projectile/bile
+ projectilesound = 'sound/f13npc/centaur/spit.ogg'
+ extra_projectiles = 2
+
+ // Spawner vars
var/max_mobs = 2
var/mob_types = list(/mob/living/simple_animal/hostile/giantant)
var/spawn_time = 30 SECONDS
var/spawn_text = "hatches from"
- blood_volume = 0
-
/mob/living/simple_animal/hostile/giantantqueen/Initialize()
. = ..()
AddComponent(/datum/component/spawner, mob_types, spawn_time, faction, spawn_text, max_mobs, _range = 7)
+ resize = 1.2
+ update_transform()
/mob/living/simple_animal/hostile/giantantqueen/death()
RemoveComponentByType(/datum/component/spawner)
@@ -190,18 +299,35 @@
. = ..()
/mob/living/simple_animal/hostile/giantantqueen/Aggro()
- ..()
+ . = ..()
+ if(.)
+ return
summon_backup(10)
+/mob/living/simple_animal/hostile/giantantqueen/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// BILE PROJECTILE
/obj/item/projectile/bile
- name = "spit"
+ name = "bile spit"
damage = 20
icon_state = "toxin"
+/obj/item/projectile/bile/on_hit(atom/target)
+ . = ..()
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.reagents.add_reagent(/datum/reagent/toxin, 2)
+
/////////////////
// RADSCORPION //
/////////////////
+// BASE RADSCORPION
/mob/living/simple_animal/hostile/radscorpion
name = "giant radscorpion"
desc = "A mutated arthropod with an armored carapace and a powerful sting."
@@ -209,63 +335,81 @@
icon_state = "radscorpion"
icon_living = "radscorpion"
icon_dead = "radscorpion_dead"
-
- speed = 1.25
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ mob_armor = ARMOR_VALUE_RADSCORPION
+
maxHealth = 120
health = 120
- harm_intent_damage = 8
- obj_damage = 20
+ speed = 1.25
+ move_to_delay = 3
+ turns_per_move = 5
+
melee_damage_lower = 15
melee_damage_upper = 28
+ harm_intent_damage = 8
+ obj_damage = 20
+
+ aggro_vision_range = 4 // Poor eyesight
+ vision_range = 5
+
+ // Nocturnal hunter, but limited by poor eyesight
+ has_low_light_vision = TRUE
+ low_light_bonus = 2
+
+ butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/radscorpion_meat = 2,
+ /obj/item/stack/sheet/bone = 1
+ )
+
waddle_amount = 3
waddle_up_time = 1
waddle_side_time = 1
- move_to_delay = 3
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 0
- //how far they pull back
-
- minimum_distance = 0
- // how close you can get before they try to pull back
-
- aggro_vision_range = 4 //due to scorpions poor eyesight
- //tiles within they start attacking, doesn't count the mobs tile
-
- vision_range = 5
- //tiles within they start making noise, does count the mobs tile
-
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/radscorpion_meat = 2)
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
- taunt_chance = 30
- a_intent = INTENT_HARM
+
+ speak_emote = list("hisses")
attack_verb_simple = "stings"
attack_sound = 'sound/creatures/radscorpion_attack.ogg'
- speak_emote = list("hisses")
+
+ emote_taunt = list("snips")
+ emote_taunt_sound = list('sound/f13npc/scorpion/taunt1.ogg', 'sound/f13npc/scorpion/taunt2.ogg', 'sound/f13npc/scorpion/taunt3.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/f13npc/scorpion/aggro.ogg')
+ idlesound = list('sound/creatures/radscorpion_snip.ogg')
+ death_sound = 'sound/f13npc/scorpion/death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("radscorpion")
+ a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
- var/scorpion_color = "radscorpion" //holder for icon set
- var/list/icon_sets = list("radscorpion", "radscorpion_blue", "radscorpion_black")
blood_volume = 0
- emote_taunt = list("snips")
-
- emote_taunt_sound = list('sound/f13npc/scorpion/taunt1.ogg', 'sound/f13npc/scorpion/taunt2.ogg', 'sound/f13npc/scorpion/taunt3.ogg')
- aggrosound = list('sound/f13npc/scorpion/aggro.ogg', )
- idlesound = list('sound/creatures/radscorpion_snip.ogg', )
- death_sound = 'sound/f13npc/scorpion/death.ogg'
-
-
-/mob/living/simple_animal/hostile/radscorpion/AttackingTarget()
- . = ..()
- if(. && ishuman(target))
- var/mob/living/carbon/human/H = target
- H.reagents.add_reagent(/datum/reagent/toxin, 5)
+ footstep_type = FOOTSTEP_MOB_CLAW
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - venomous sting
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+ // Color randomization
+ var/scorpion_color = "radscorpion"
+ var/list/icon_sets = list("radscorpion", "radscorpion_blue", "radscorpion_black")
/mob/living/simple_animal/hostile/radscorpion/Initialize()
. = ..()
@@ -278,40 +422,60 @@
icon_living = "[scorpion_color]"
icon_dead = "[scorpion_color]_dead"
-// BLACK RADSCORPION - a little tougher and slower
+/mob/living/simple_animal/hostile/radscorpion/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/radscorpion/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+/mob/living/simple_animal/hostile/radscorpion/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin, 5)
+
+// BLACK RADSCORPION - tougher, slower
/mob/living/simple_animal/hostile/radscorpion/black
- name = "giant rad scorpion"
- desc = "A giant irradiated scorpion with a black exoskeleton. Its appearance makes you shudder in fear.
This one has giant pincers."
+ name = "black radscorpion"
+ desc = "A giant irradiated scorpion with a black exoskeleton. Its appearance makes you shudder in fear. This one has giant pincers."
icon_state = "radscorpion_black"
icon_living = "radscorpion_black"
icon_dead = "radscorpion_black_d"
- speed = 1.2
+
+ mob_armor = ARMOR_VALUE_RADSCORPION_BLACK
+
maxHealth = 160
health = 160
+ speed = 1.2
+
melee_damage_lower = 10
melee_damage_upper = 28
- move_to_delay = 4
- footstep_type = FOOTSTEP_MOB_CLAW
-
-// BLUE RADSCORPION - a little weaker and faster
+// BLUE RADSCORPION - weaker, faster
/mob/living/simple_animal/hostile/radscorpion/blue
- name = "giant rad scorpion"
- desc = "A giant irradiated scorpion with a bluish exoskeleton. Slighly smaller and faster than its reddish cousin."
+ name = "blue radscorpion"
+ desc = "A giant irradiated scorpion with a bluish exoskeleton. Slightly smaller and faster than its reddish cousin."
icon_state = "radscorpion_blue"
icon_living = "radscorpion_blue"
icon_dead = "radscorpion_blue_d"
icon_gib = "radscorpion_blue_gib"
- speed = 1.35
+
maxHealth = 110
health = 110
- move_to_delay = 4
- footstep_type = FOOTSTEP_MOB_CLAW
+ speed = 1.35
/////////////
// CAZADOR //
/////////////
+// BASE CAZADOR - flying venomous terror
/mob/living/simple_animal/hostile/cazador
name = "cazador"
desc = "A mutated insect known for its fast speed, deadly sting, and being huge bastards."
@@ -319,57 +483,92 @@
icon_state = "cazador"
icon_living = "cazador"
icon_dead = "cazador_dead1"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
+ mob_armor = ARMOR_VALUE_CAZADOR
+
+ maxHealth = 24
+ health = 24
+ speed = 1
+ move_to_delay = 2.5
turns_per_move = 5
-
- move_to_delay = 2.0
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 3
- //how far they pull back
-
- minimum_distance = 1
- // how close you can get before they try to pull back
-
- aggro_vision_range = 7 //due to scorpions poor eyesight
- //tiles within they start attacking, doesn't count the mobs tile
-
- vision_range = 8
- //tiles within they start making noise, does count the mobs tile
+
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+ harm_intent_damage = 8
+ obj_damage = 20
rapid_melee = 2
-
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 2, /obj/item/stack/sheet/sinew = 2, /obj/item/stack/sheet/animalhide/chitin = 2)
- butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1)
+
+ aggro_vision_range = 7
+ vision_range = 8
+
+ // Hunting insect with compound eyes adapted for low light
+ has_low_light_vision = TRUE
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 2,
+ /obj/item/stack/sheet/sinew = 2,
+ /obj/item/stack/sheet/animalhide/chitin = 2
+ )
butcher_difficulty = 1.5
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
+
+ speak_emote = list("buzzes")
+ attack_verb_simple = "stings"
+ attack_sound = 'sound/creatures/cazador_attack.ogg'
+
emote_taunt = list("buzzes")
emote_taunt_sound = list('sound/f13npc/cazador/cazador_alert.ogg')
+ taunt_chance = 30
aggrosound = list('sound/f13npc/cazador/cazador_charge1.ogg', 'sound/f13npc/cazador/cazador_charge2.ogg', 'sound/f13npc/cazador/cazador_charge3.ogg')
idlesound = list('sound/creatures/cazador_buzz.ogg')
- stat_attack = CONSCIOUS
- robust_searching = TRUE
- taunt_chance = 30
- speed = 1
- maxHealth = 24
- health = 24
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 5
- melee_damage_upper = 10
- attack_verb_simple = "stings"
- attack_sound = 'sound/creatures/cazador_attack.ogg'
- speak_emote = list("buzzes")
+ death_sound = 'sound/f13npc/cazador/cazador_death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("cazador")
- movement_type = FLYING
a_intent = INTENT_HARM
- pass_flags = PASSTABLE | PASSMOB
+ stat_attack = CONSCIOUS
+ robust_searching = TRUE
gold_core_spawnable = HOSTILE_SPAWN
- death_sound = 'sound/f13npc/cazador/cazador_death.ogg'
blood_volume = 0
+
+ movement_type = FLYING
+ pass_flags = PASSTABLE | PASSMOB
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement - can fly
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 20 // Fast flier
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - flying ambush
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/cazador/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/cazador/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/cazador/AttackingTarget()
. = ..()
@@ -385,33 +584,40 @@
if(!Proj)
return
if(prob(50))
- return ..()
- else
visible_message(span_danger("[src] dodges [Proj]!"))
- return 0
-
+ return BULLET_ACT_FORCE_PIERCE
+ return ..()
+// YOUNG CAZADOR - smaller, weaker
/mob/living/simple_animal/hostile/cazador/young
name = "young cazador"
desc = "A mutated insect known for its fast speed, deadly sting, and being huge bastards. This one's little."
+
maxHealth = 20
health = 20
speed = 1
+
melee_damage_lower = 5
melee_damage_upper = 10
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1, /obj/item/stack/sheet/sinew = 1)
- butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1)
- butcher_difficulty = 1.5
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1,
+ /obj/item/stack/sheet/animalhide/chitin = 1,
+ /obj/item/stack/sheet/sinew = 1
+ )
+
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
/mob/living/simple_animal/hostile/cazador/young/Initialize()
. = ..()
resize = 0.8
update_transform()
+// CAZADOR VENOM
/datum/reagent/toxin/cazador_venom
name = "Cazador venom"
description = "A potent toxin resulting from cazador stings that quickly kills if too much remains in the body."
- color = "#801E28" // rgb: 128, 30, 40
+ color = "#801E28"
toxpwr = 1
taste_description = "pain"
taste_mult = 1.3
@@ -438,60 +644,101 @@
icon_living = "bloatfly"
icon_dead = "bloatfly_dead"
icon_gib = null
- ranged = TRUE
-
- speed = 1
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+
maxHealth = 20
health = 20
- harm_intent_damage = 8
- obj_damage = 15
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 4
melee_damage_upper = 7
+ harm_intent_damage = 8
+ obj_damage = 15
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/bloatfly_meat = 2,
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/stack/sheet/animalhide/chitin = 1
+ )
+ butcher_difficulty = 1.5
+
waddle_amount = 4
waddle_up_time = 3
waddle_side_time = 2
- can_ghost_into = TRUE
-
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/bloatfly_meat = 2, /obj/item/stack/sheet/sinew = 1)
- butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1)
- butcher_difficulty = 1.5
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "bites"
- emote_taunt = list("growls")
- taunt_chance = 30
+
+ speak_emote = list("chitters")
attack_verb_simple = "bites"
attack_sound = 'sound/creatures/bloatfly_attack.ogg'
- speak_emote = list("chitters")
+
+ emote_taunt = list("buzzes")
+ taunt_chance = 30
+ idlesound = list('sound/f13npc/bloatfly/fly.ogg')
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("hostile", "gecko", "critter-friend")
+ a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
+ blood_volume = 0
+
+ movement_type = FLYING
pass_flags = PASSTABLE | PASSMOB
density = FALSE
- a_intent = INTENT_HARM
- idlesound = list('sound/f13npc/bloatfly/fly.ogg')
- blood_volume = 0
+
+ can_ghost_into = TRUE
+ desc_short = "A gigantic fly that's more disgusting than actually threatening. Tends to dodge bullets."
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement - can fly
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 20
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure ranged - flies and shoots maggots
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_cooldown_time = 3 SECONDS
+ auto_fire_delay = GUN_BURSTFIRE_DELAY_NORMAL
casingtype = /obj/item/ammo_casing/shotgun/bloatfly
projectiletype = null
projectilesound = 'sound/f13npc/bloatfly/shoot2.ogg'
- //sound_after_shooting = 'sound/f13npc/bloatfly/afterfire1.ogg'
- //sound_after_shooting_delay = 1 SECONDS
extra_projectiles = 1
- auto_fire_delay = GUN_BURSTFIRE_DELAY_NORMAL
- ranged_cooldown_time = 3 SECONDS
+
variation_list = list(
MOB_COLOR_VARIATION(200, 200, 200, 255, 255, 255),
- MOB_CASING_LIST(\
- MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly, 4),\
- MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly/two, 3),\
- MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly/three, 3)\
+ "varied_projectile" = list(
+ /obj/item/ammo_casing/shotgun/bloatfly = 4,
+ /obj/item/ammo_casing/shotgun/bloatfly/two = 3,
+ /obj/item/ammo_casing/shotgun/bloatfly/three = 3
)
)
- desc_short = "A gigantic fly that's more disgusting than actually threatening. Tends to dodge bullets."
- pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+/mob/living/simple_animal/hostile/bloatfly/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(6)
+
+/mob/living/simple_animal/hostile/bloatfly/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/bloatfly/bullet_act(obj/item/projectile/Proj)
if(!Proj)
@@ -499,8 +746,7 @@
if(prob(50))
visible_message(span_danger("[src] dodges [Proj]!"))
return BULLET_ACT_FORCE_PIERCE
- else
- . = ..()
+ return ..()
/mob/living/simple_animal/hostile/bloatfly/become_the_mob(mob/user)
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter
@@ -519,59 +765,96 @@
icon_living = "radroach"
icon_dead = "radroach_dead"
icon_gib = "radroach_gib"
- can_ghost_into = TRUE
- waddle_amount = 1
- waddle_up_time = 1
- waddle_side_time = 1
-
- speed = 1
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+
maxHealth = 20
health = 20
- harm_intent_damage = 8
- obj_damage = 20
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 4
melee_damage_upper = 6
-
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/radroach_meat = 2, /obj/item/stack/sheet/sinew = 1)
- butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1)
+ harm_intent_damage = 8
+ obj_damage = 20
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/radroach_meat = 2,
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/stack/sheet/animalhide/chitin = 1
+ )
butcher_difficulty = 1.5
+
+ waddle_amount = 1
+ waddle_up_time = 1
+ waddle_side_time = 1
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
+
+ speak_emote = list("skitters")
attack_verb_simple = "nips"
attack_sound = 'sound/creatures/radroach_attack.ogg'
- speak_emote = list("skitters")
+
+ aggrosound = list('sound/creatures/radroach_chitter.ogg')
+ idlesound = list('sound/f13npc/roach/idle1.ogg', 'sound/f13npc/roach/idle2.ogg', 'sound/f13npc/roach/idle3.ogg')
+ death_sound = 'sound/f13npc/roach/roach_death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("gecko", "critter-friend")
a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
pass_flags = PASSTABLE | PASSMOB
density = FALSE
- gold_core_spawnable = HOSTILE_SPAWN
randpixel = 12
+
+ can_ghost_into = TRUE
+ desc_short = "One of countless bugs that move in gross hordes."
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
variation_list = list(
MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255),
MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(100),
- MOB_HEALTH_LIST(5, 10, 1),
- MOB_RETREAT_DISTANCE_LIST(0, 2, 3),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(0, 1, 1),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(5),
+ MOB_HEALTH_LIST(5, 10, 15, 20),
)
- aggrosound = list('sound/creatures/radroach_chitter.ogg',)
- idlesound = list('sound/f13npc/roach/idle1.ogg', 'sound/f13npc/roach/idle2.ogg', 'sound/f13npc/roach/idle3.ogg',)
- death_sound = 'sound/f13npc/roach/roach_death.ogg'
- desc_short = "One of countless bugs that move in gross hordes."
- pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+/mob/living/simple_animal/hostile/radroach/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(6)
+
+/mob/living/simple_animal/hostile/radroach/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/radroach/become_the_mob(mob/user)
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter
send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter
. = ..()
+// JUNGLE RADROACH - friendly variant
/mob/living/simple_animal/hostile/radroach/jungle
faction = list("gecko", "critter-friend", "jungle")
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm b/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm
index 5add294a382..56af2babfa9 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm
@@ -1,11 +1,10 @@
// In this document: Mirelurks
-
///////////////
// MIRELURKS //
///////////////
-// MIRELURK ADULT
+// BASE MIRELURK - shared properties
/mob/living/simple_animal/hostile/mirelurk
name = "mirelurk"
desc = "A giant mutated crustacean, with a hardened exo-skeleton."
@@ -13,80 +12,132 @@
icon_state = "mirelurk"
icon_living = "mirelurk"
icon_dead = "mirelurk_d"
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ mob_armor = ARMOR_VALUE_MIRELURK // Hard shell - resistant to bullets
+
+ maxHealth = 120
+ health = 120
speed = 1
- can_ghost_into = TRUE
move_to_delay = 3
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 0
- //how far they pull back
+ turns_per_move = 5
+
+ melee_damage_lower = 5
+ melee_damage_upper = 18
+ harm_intent_damage = 8
+ rapid_melee = 1
- minimum_distance = 0
- // how close you can get before they try to pull back
-
aggro_vision_range = 7
- //tiles within they start attacking, doesn't count the mobs tile
-
vision_range = 8
- //tiles within they start making noise, does count the mobs tile
-
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 2, /obj/item/stack/sheet/sinew = 1)
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 2,
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
+ butcher_results = list(/obj/item/stack/sheet/bone = 1)
+ butcher_difficulty = 2
+
speak_emote = list("foams", "clacks", "chitters", "snips", "snaps")
- emote_see = list("clack its claws", "foam at the mouth", "woobs", "extends its eyestalks")
- attack_verb_simple = list ("pinches", "rends", "snips", "snaps", "snibbity-snaps", "clonks", "disects")
- maxHealth = 120
- health = 120
- melee_damage_lower = 5
- melee_damage_upper = 18
+ emote_see = list("clacks its claws", "foams at the mouth", "woobs", "extends its eyestalks")
+ attack_verb_simple = list("pinches", "rends", "snips", "snaps", "snibbity-snaps", "clonks", "dissects")
+
waddle_amount = 2
waddle_up_time = 1
waddle_side_time = 1
+
+ faction = list("mirelurk")
gold_core_spawnable = HOSTILE_SPAWN
blood_volume = 0
footstep_type = FOOTSTEP_MOB_CLAW
+
+ can_ghost_into = TRUE
pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement - can climb stairs
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ // Can't open doors - but can smash them
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+
+ // Pure melee ambusher - hides and rushes
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+ // Sound effects - using existing crustacean/creature sounds
+ emote_taunt_sound = list('sound/creatures/radscorpion_snip.ogg')
+ emote_taunt = list("clacks menacingly", "snaps its claws")
+ taunt_chance = 30
+ aggrosound = list('sound/creatures/radscorpion_attack.ogg')
+ idlesound = list('sound/creatures/radroach_chitter.ogg')
+ death_sound = 'sound/creatures/rattle.ogg'
+ attack_sound = 'sound/creatures/radscorpion_snip.ogg'
+
variation_list = list(
MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255),
MOB_SPEED_LIST(3.3, 3.4, 3.5),
- MOB_SPEED_CHANGE_PER_TURN_CHANCE(100),
+ MOB_SPEED_CHANGE_PER_TURN_CHANCE(10),
MOB_HEALTH_LIST(110, 115, 120, 130),
- MOB_RETREAT_DISTANCE_LIST(0, 1, 2),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(70),
- MOB_MINIMUM_DISTANCE_LIST(0, 0, 1),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(70),
)
+/mob/living/simple_animal/hostile/mirelurk/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+// Override CanAttack to ignore unconscious/dead targets
+/mob/living/simple_animal/hostile/mirelurk/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
-// MIRELURK HUNTER MALES
+// MIRELURK HUNTER - bigger, meaner, faster
/mob/living/simple_animal/hostile/mirelurk/hunter
name = "mirelurk hunter"
- desc = "A giant mutated crustacean, with a hardened exoskeleton. Its appearance makes you shudder in fear. This one has giant, razor sharp claw pincers."
+ desc = "A giant mutated crustacean, with a hardened exoskeleton. Its appearance makes you shudder in fear. This one has giant, razor-sharp claw pincers."
icon_state = "mirelurkhunter"
icon_living = "mirelurkhunter"
- speed = 1
icon_dead = "mirelurkhunter_d"
icon_gib = "gib"
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 4, /obj/item/stack/sheet/sinew = 2)
+
+ mob_armor = ARMOR_VALUE_MIRELURK_HUNTER // Even tougher shell
+
maxHealth = 160
health = 160
- stat_attack = UNCONSCIOUS
+ speed = 1
+
melee_damage_lower = 15
melee_damage_upper = 28
- gold_core_spawnable = HOSTILE_SPAWN
- footstep_type = FOOTSTEP_MOB_CLAW
+ rapid_melee = 2 // Attacks faster
+
+ stat_attack = UNCONSCIOUS // Will attack downed targets
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 4,
+ /obj/item/stack/sheet/sinew = 2,
+ /obj/item/stack/sheet/bone = 2
+ )
+
pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
variation_list = list(
MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255),
- MOB_SPEED_LIST(3, 3.1, 3.2),
- MOB_SPEED_CHANGE_PER_TURN_CHANCE(100),
+ MOB_SPEED_LIST(3.0, 3.1, 3.2),
+ MOB_SPEED_CHANGE_PER_TURN_CHANCE(10),
MOB_HEALTH_LIST(140, 150, 160, 170),
- MOB_RETREAT_DISTANCE_LIST(0, 1, 2),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(70),
- MOB_MINIMUM_DISTANCE_LIST(0, 0, 1),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(70),
)
-// MIRELURK BABY
+// MIRELURK BABY - small, weak, calls for help
/mob/living/simple_animal/hostile/mirelurk/baby
name = "mirelurk baby"
desc = "A neophyte mirelurk baby, mostly harmless. Adults respond to their chittering if distressed."
@@ -94,42 +145,311 @@
icon_living = "mirelurkbaby"
icon_dead = "mirelurkbaby_d"
icon_gib = "gib"
- butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1)
- speed = 1
+
+ mob_armor = ARMOR_VALUE_MIRELURK_BABY // Softer shell
+
maxHealth = 40
health = 40
+ speed = 1
+
melee_damage_lower = 5
melee_damage_upper = 10
+ rapid_melee = 1
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1,
+ /obj/item/stack/sheet/sinew = 1
+ )
+ butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1)
+
waddle_amount = 3
- waddle_up_time = 1
- waddle_side_time = 1
- gold_core_spawnable = HOSTILE_SPAWN
- footstep_type = FOOTSTEP_MOB_CLAW
- pop_required_to_jump_into = 0
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Babies run away and call for help
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
variation_list = list(
MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255),
MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(100),
MOB_HEALTH_LIST(35, 39, 40, 41),
- MOB_RETREAT_DISTANCE_LIST(5, 6, 7),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(3, 4, 5),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
)
+/mob/living/simple_animal/hostile/mirelurk/baby/Aggro()
+ ..()
+ summon_backup(12) // Babies call louder for help
+ if(!ckey)
+ visible_message(span_warning("[src] chitters frantically for help!"))
+
+// MIRELURK QUEEN - rare, powerful, spawns babies
+/mob/living/simple_animal/hostile/mirelurk/queen
+ name = "mirelurk queen"
+ desc = "A massive mirelurk matriarch. Her hardened carapace glistens with toxic secretions, and she guards her brood fiercely."
+ icon_state = "mirelurk"
+ icon_living = "mirelurk"
+ icon_dead = "mirelurk_d"
+ color = "#88ccaa" // Greenish-teal coloration for breeding female
+
+ mob_armor = ARMOR_VALUE_MIRELURK_QUEEN // Nearly impenetrable
+
+ maxHealth = 400
+ health = 400
+ speed = 2.5 // Slower but tankier
+ stat_attack = UNCONSCIOUS
+
+ melee_damage_lower = 20
+ melee_damage_upper = 35
+ rapid_melee = 1
+ obj_damage = 150
+
+ environment_smash = ENVIRONMENT_SMASH_WALLS // Can break through walls
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 8,
+ /obj/item/stack/sheet/sinew = 4,
+ /obj/item/stack/sheet/bone = 4
+ )
+
+ loot = list(/obj/item/stack/f13Cash/random/high)
+ loot_drop_amount = 5
+
+ // Mixed combat - acid spit at range, claw up close
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 3
+ minimum_distance = 1
+
+ ranged_message = "spits acid"
+ ranged_cooldown_time = 4 SECONDS
+ projectiletype = /obj/item/projectile/mirelurk_acid
+ projectilesound = 'sound/effects/splat.ogg'
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE
+
+ // Spawner component
+ var/max_babies = 3
+ var/baby_types = list(/mob/living/simple_animal/hostile/mirelurk/baby)
+ var/spawn_time = 20 SECONDS
+ var/spawn_text = "emerges from"
+
+/mob/living/simple_animal/hostile/mirelurk/queen/Initialize()
+ . = ..()
+ AddComponent(/datum/component/spawner, baby_types, spawn_time, faction, spawn_text, max_babies, _range = 5)
+ resize = 1.3
+ update_transform()
-/mob/living/simple_animal/hostile/mirelurk/baby/Initialize()
+/mob/living/simple_animal/hostile/mirelurk/queen/death()
+ RemoveComponentByType(/datum/component/spawner)
. = ..()
-/mob/living/simple_animal/hostile/mirelurk/baby/Aggro()
- ..()
- summon_backup(10)
+/mob/living/simple_animal/hostile/mirelurk/queen/Destroy()
+ RemoveComponentByType(/datum/component/spawner)
+ . = ..()
-// OBSOLETE MARKED FOR DEATH, YOU HAVE 3 DAYS
+/mob/living/simple_animal/hostile/mirelurk/queen/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(15)
+ if(!ckey)
+ visible_message(span_danger("[src] screeches, calling the brood!"))
+
+/mob/living/simple_animal/hostile/mirelurk/queen/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin, 3)
+
+// SOFTSHELL MIRELURK - weaker variant, less armor
+/mob/living/simple_animal/hostile/mirelurk/softshell
+ name = "softshell mirelurk"
+ desc = "A mirelurk that hasn't fully hardened its shell yet. Still dangerous, but more vulnerable."
+ icon_state = "mirelurk"
+ icon_living = "mirelurk"
+ icon_dead = "mirelurk_d"
+ color = "#ffaa99" // Reddish-pink for vulnerable molting state
+
+ mob_armor = ARMOR_VALUE_MIRELURK_SOFT // Much weaker armor
+
+ maxHealth = 80
+ health = 80
+ speed = 0.8 // Faster to compensate
+
+ melee_damage_lower = 8
+ melee_damage_upper = 15
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 3,
+ /obj/item/stack/sheet/sinew = 2
+ )
+
+ variation_list = list(
+ MOB_COLOR_VARIATION(150, 150, 150, 200, 200, 200),
+ MOB_SPEED_LIST(2.8, 2.9, 3.0),
+ MOB_SPEED_CHANGE_PER_TURN_CHANCE(10),
+ MOB_HEALTH_LIST(70, 75, 80, 85),
+ )
+
+// KING MIRELURK - rare boss variant
+/mob/living/simple_animal/hostile/mirelurk/king
+ name = "mirelurk king"
+ desc = "A towering humanoid mirelurk with a distinctive crown-like crest. Highly intelligent and extremely dangerous."
+ icon_state = "mirelurkhunter"
+ icon_living = "mirelurkhunter"
+ icon_dead = "mirelurkhunter_d"
+ color = "#6633aa" // Dark purple for intimidating alpha male
+
+ mob_armor = ARMOR_VALUE_MIRELURK_KING
+
+ maxHealth = 350
+ health = 350
+ speed = 1.5
+ stat_attack = UNCONSCIOUS
+
+ melee_damage_lower = 25
+ melee_damage_upper = 40
+ rapid_melee = 2
+ obj_damage = 200
+
+ environment_smash = ENVIRONMENT_SMASH_WALLS
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 6,
+ /obj/item/stack/sheet/sinew = 3,
+ /obj/item/stack/sheet/bone = 3
+ )
+
+ loot = list(/obj/item/stack/f13Cash/random/high)
+ loot_drop_amount = 8
+
+ // Mixed combat - sonic scream at range
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_message = "unleashes a sonic scream"
+ ranged_cooldown_time = 5 SECONDS
+ projectiletype = /obj/item/projectile/mirelurk_sonic
+ projectilesound = 'sound/effects/supermatter.ogg' // Sonic scream sound
+ extra_projectiles = 2
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE
+
+ // Low health rage mode
+ low_health_threshold = 0.35
+ var/color_rage = "#FF4444"
+
+/mob/living/simple_animal/hostile/mirelurk/king/Initialize()
+ . = ..()
+ resize = 1.4
+ update_transform()
+
+/mob/living/simple_animal/hostile/mirelurk/king/make_low_health()
+ visible_message(span_danger("[src] roars with fury, its crest glowing red!"))
+ playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15))
+ color = color_rage
+ speed *= 0.75 // Faster
+ melee_damage_lower = round(melee_damage_lower * 1.3)
+ melee_damage_upper = round(melee_damage_upper * 1.3)
+ rapid_melee = 3
+ is_low_health = TRUE
+
+/mob/living/simple_animal/hostile/mirelurk/king/make_high_health()
+ visible_message(span_notice("[src]'s fury subsides."))
+ color = initial(color)
+ speed = initial(speed)
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ rapid_melee = initial(rapid_melee)
+ is_low_health = FALSE
+
+/mob/living/simple_animal/hostile/mirelurk/king/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(15)
+ if(!ckey)
+ visible_message(span_danger("[src] bellows a deafening challenge!"))
+
+/mob/living/simple_animal/hostile/mirelurk/king/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin, 5)
+ H.Knockdown(20) // Powerful hits knock down
+
+// PROJECTILES
+/obj/item/projectile/mirelurk_acid
+ name = "acid spit"
+ damage = 20
+ icon_state = "toxin"
+
+/obj/item/projectile/mirelurk_acid/on_hit(atom/target)
+ . = ..()
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.reagents.add_reagent(/datum/reagent/toxin, 3)
+ M.emote("scream")
+
+/obj/item/projectile/mirelurk_sonic
+ name = "sonic blast"
+ damage = 15
+ stamina = 30
+ icon_state = "sound"
+
+/obj/item/projectile/mirelurk_sonic/on_hit(atom/target)
+ . = ..()
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.Stun(20)
+ M.soundbang_act(1, 4, 10, 5)
+
+// LEGACY EGG STRUCTURE (marked for removal)
/obj/structure/mirelurkegg
name = "mirelurk eggs"
- desc = "A fresh clutch of mirelurk eggs."
+ desc = "A fresh clutch of mirelurk eggs. They pulse with life."
icon = 'icons/mob/wastemobsdrops.dmi'
icon_state = "mirelurkeggs"
density = 1
anchored = 0
+ max_integrity = 50
+
+/obj/structure/mirelurkegg/attack_hand(mob/user)
+ . = ..()
+ if(.)
+ return
+ to_chat(user, span_notice("You crack open [src]."))
+ new /obj/item/reagent_containers/food/snacks/mirelurk_egg(get_turf(src))
+ qdel(src)
+
+/obj/structure/mirelurkegg/attackby(obj/item/I, mob/user, params)
+ if(I.force >= 5)
+ visible_message(span_warning("[src] cracks open!"))
+ new /obj/item/reagent_containers/food/snacks/mirelurk_egg(get_turf(src))
+ qdel(src)
+ return
+ return ..()
+
+// MIRELURK EGG FOOD ITEM
+/obj/item/reagent_containers/food/snacks/mirelurk_egg
+ name = "mirelurk egg"
+ desc = "A large, protein-rich egg from a mirelurk."
+ icon = 'icons/obj/food/egg.dmi'
+ icon_state = "egg"
+ list_reagents = list(/datum/reagent/consumable/nutriment = 8, /datum/reagent/consumable/nutriment/vitamin = 2)
+ filling_color = "#FFCC99"
+ tastes = list("egg" = 1, "ocean" = 1)
+ foodtype = MEAT | RAW
+ cooked_type = /obj/item/reagent_containers/food/snacks/mirelurk_egg/cooked
+
+/obj/item/reagent_containers/food/snacks/mirelurk_egg/cooked
+ name = "cooked mirelurk egg"
+ desc = "A perfectly cooked mirelurk egg. Delicious and nutritious."
+ icon_state = "egg"
+ list_reagents = list(/datum/reagent/consumable/nutriment = 10, /datum/reagent/consumable/nutriment/vitamin = 3)
+ foodtype = MEAT
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/raider.dm b/code/modules/mob/living/simple_animal/hostile/f13/raider.dm
index 3b450234020..645385fdf53 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/raider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/raider.dm
@@ -1,10 +1,9 @@
-// IN THIS FILE: -All Raider Mobs
+// IN THIS FILE: All Raider Mobs
-///////////////////
-// BASIC RAIDERS //
-///////////////////
+// ============================================================
+// BASE RAIDER
+// ============================================================
-// BASIC MELEE RAIDER
/mob/living/simple_animal/hostile/raider
name = "Raider"
desc = "Another murderer churned out by the wastes."
@@ -13,7 +12,6 @@
icon_living = "raider_melee"
icon_dead = "raider_dead"
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- turns_per_move = 5
mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 100
health = 100
@@ -26,60 +24,77 @@
check_friendly_fire = TRUE
status_flags = CANPUSH
del_on_death = FALSE
+
loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/stack/f13Cash/random/med)
- /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list
loot_drop_amount = 2
- /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items
loot_amount_random = TRUE
- /// slots in a list of trash loot
var/random_trash_loot = TRUE
+
footstep_type = FOOTSTEP_MOB_SHOE
rapid_melee = 2
melee_queue_distance = 5
- move_to_delay = 3.0
+ move_to_delay = 3
+ turns_per_move = 5
waddle_amount = 2
waddle_up_time = 1
waddle_side_time = 1
- retreat_distance = 1 //mob retreats 1 tile when in min distance
- minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
- variation_list = list(
- MOB_NAME_FROM_GLOBAL_LIST(\
- MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 1)\
- ))
+
+ aggro_vision_range = 6
+ vision_range = 8
+
can_z_move = TRUE
- z_move_delay = 40 // 4 seconds - more cautious
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+ variation_list = list(
+ MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 1))
+ )
/mob/living/simple_animal/hostile/raider/Initialize()
. = ..()
if(random_trash_loot)
loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment
-/obj/effect/mob_spawn/human/corpse/raider
- name = "Raider"
- uniform = /obj/item/clothing/under/f13/rag
- suit = /obj/item/clothing/suit/armor/medium/raider/iconoclast
- shoes = /obj/item/clothing/shoes/f13/explorer
- gloves = /obj/item/clothing/gloves/f13/leather
- head = /obj/item/clothing/head/helmet/f13/firefighter
-
/mob/living/simple_animal/hostile/raider/Aggro()
. = ..()
if(.)
return
- summon_backup(15)
+ summon_backup(10)
if(!ckey)
- say(pick("*insult", "Fuck off!!", "Back off!!" , "Keep moving!!", "Get lost, asshole!!", "Call a doctor, we got a bleeder!!", "Fuck around and find out!!" ))
+ say(pick("*insult", "Fuck off!!", "Back off!!", "Keep moving!!", "Get lost, asshole!!", "Call a doctor, we got a bleeder!!", "Fuck around and find out!!"))
+
+// Friendly fire resistance - raiders are used to fighting in groups
+/mob/living/simple_animal/hostile/raider/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/raider))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// ============================================================
+// THIEF RAIDER
+// Steals items from downed players and runs
+// ============================================================
-// THIEF RAIDER - nabs stuff and runs
/mob/living/simple_animal/hostile/raider/thief
desc = "Another murderer churned out by the wastes. This one looks like they have sticky fingers..."
/mob/living/simple_animal/hostile/raider/thief/movement_delay()
- return -2
+ return ..() - 2
/mob/living/simple_animal/hostile/raider/thief/AttackingTarget()
+ . = ..()
if(ishuman(target))
var/mob/living/carbon/human/H = target
if(H.stat == SOFT_CRIT)
@@ -95,41 +110,40 @@
if(shoe_target)
H.dropItemToGround(shoe_target, TRUE)
src.transferItemToLoc(shoe_target, src, TRUE)
- retreat_distance = 50
- else
- . = ..()
+ retreat_distance = 50 // Run after stealing
/mob/living/simple_animal/hostile/raider/thief/death(gibbed)
for(var/obj/I in contents)
src.dropItemToGround(I)
. = ..()
-// BASIC RANGED RAIDER
+// ============================================================
+// RANGED RAIDER
+// ============================================================
+
/mob/living/simple_animal/hostile/raider/ranged
icon_state = "raider_ranged"
icon_living = "raider_ranged"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 85
health = 85
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 1 //mob retreats 1 tile when in min distance
- minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+
+ loot = list(/obj/effect/spawner/lootdrop/f13/npc_raider, /obj/item/stack/f13Cash/random/med)
+ loot_drop_amount = 3
+
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
ranged_cooldown_time = 2 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/c9mm/simple
projectilesound = 'sound/f13weapons/ninemil.ogg'
- loot = list(/obj/effect/spawner/lootdrop/f13/npc_raider, /obj/item/stack/f13Cash/random/med)
- loot_drop_amount = 3
- footstep_type = FOOTSTEP_MOB_SHOE
+
variation_list = list(
- MOB_NAME_FROM_GLOBAL_LIST(\
- MOB_RANDOM_NAME(MOB_NAME_RANDOM_FEMALE, 1)\
- ))
+ MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_FEMALE, 1))
+ )
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_LIGHT_VOLUME),
@@ -141,49 +155,48 @@
SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
)
+// ============================================================
+// LEGENDARY RAIDERS
+// stat_attack = UNCONSCIOUS lets them finish off downed targets
+// NOTE: This only works correctly now that the CanAttack override is removed
+// ============================================================
-// LEGENDARY MELEE RAIDER
/mob/living/simple_animal/hostile/raider/legendary
name = "Legendary Raider"
desc = "Another murderer churned out by the wastes - this one seems a bit faster than the average..."
color = "#FFFF00"
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
- maxHealth = 300
- health = 300
+ maxHealth = 225
+ health = 225
stat_attack = UNCONSCIOUS
speed = 1.2
obj_damage = 300
rapid_melee = 1
+
loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/reagent_containers/food/snacks/kebab/human, /obj/item/stack/f13Cash/random/high)
loot_drop_amount = MOB_LOOT_ALL
loot_amount_random = FALSE
random_trash_loot = FALSE
- footstep_type = FOOTSTEP_MOB_SHOE
-// LEGENDARY RANGED RAIDER
/mob/living/simple_animal/hostile/raider/ranged/legendary
name = "Legendary Raider"
desc = "Another murderer churned out by the wastes, wielding a decent pistol and looking very strong"
color = "#FFFF00"
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
- maxHealth = 240
- health = 240
+ maxHealth = 180
+ health = 180
stat_attack = UNCONSCIOUS
- retreat_distance = 1
- minimum_distance = 2
- rapid_melee = 1
+
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
sight_shoot_delay_time = 0 SECONDS
projectiletype = /obj/item/projectile/bullet/m44/simple
projectilesound = 'sound/f13weapons/44mag.ogg'
extra_projectiles = 1
obj_damage = 300
+
loot = list(/obj/item/gun/ballistic/revolver/m29, /obj/item/stack/f13Cash/random/high)
loot_drop_amount = MOB_LOOT_ALL
loot_amount_random = FALSE
random_trash_loot = FALSE
- footstep_type = FOOTSTEP_MOB_SHOE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_HEAVY_VOLUME),
@@ -195,7 +208,10 @@
SP_DISTANT_RANGE(PISTOL_HEAVY_RANGE_DISTANT)
)
+// ============================================================
// RAIDER BOSS
+// ============================================================
+
/mob/living/simple_animal/hostile/raider/ranged/boss
name = "Raider Boss"
icon_state = "raiderboss"
@@ -205,26 +221,24 @@
maxHealth = 150
health = 150
stat_attack = UNCONSCIOUS
+
extra_projectiles = 2
- rapid_melee = 1
waddle_amount = 4
waddle_up_time = 2
waddle_side_time = 1
+
ranged_cooldown_time = 2 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST
projectiletype = /obj/item/projectile/bullet/c10mm/improvised
+
loot = list(/obj/item/gun/ballistic/automatic/smg/smg10mm, /obj/item/clothing/head/helmet/f13/combat/mk2/raider, /obj/effect/spawner/lootdrop/f13/armor/random, /obj/item/clothing/under/f13/ravenharness, /obj/item/stack/f13Cash/random/high)
loot_drop_amount = MOB_LOOT_ALL
loot_amount_random = FALSE
random_trash_loot = FALSE
- footstep_type = FOOTSTEP_MOB_SHOE
+
sentience_type = SENTIENCE_BOSS
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
despawns_when_lonely = FALSE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_MEDIUM_VOLUME),
@@ -237,9 +251,9 @@
)
variation_list = list(
- MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 4),
+ MOB_RETREAT_DISTANCE_LIST(3, 4, 5),
MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50),
- MOB_MINIMUM_DISTANCE_LIST(0, 2, 4),
+ MOB_MINIMUM_DISTANCE_LIST(1, 2, 3),
MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(40),
)
@@ -247,61 +261,38 @@
. = ..()
if(.)
return
- summon_backup(15)
+ summon_backup(10)
if(!ckey)
say("KILL 'EM, FELLAS!")
+// ============================================================
+// NAMED BOSSES
+// ============================================================
+
/mob/living/simple_animal/hostile/raider/ranged/boss/mangomatt
name = "Mango Mathew and his Merry Meth Madlads"
desc = "Hi, Mango Mathew and his Merry Meth Madlads."
icon_state = "mango_matt"
icon_living = "mango_matt"
icon_dead = "mango_matt_dead"
- mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_BOSS
maxHealth = 165
health = 165
extra_projectiles = 2
ranged_cooldown_time = 1 SECONDS
sight_shoot_delay_time = 0 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST
- speak_emote = list(
- "growls",
- "murrs",
- "purrs",
- "mrowls",
- "yowls",
- "prowls"
- )
- emote_see = list(
- "laughs",
- "nyas",
- ""
- )
- attack_verb_simple = list(
- "claws",
- "maims",
- "bites",
- "mauls",
- "slashes",
- "thrashes",
- "bashes",
- "glomps",
- "beats their greasegun against the face of"
- )
- variation_list = list() // so he keeps his stupid name
+
+ speak_emote = list("growls", "murrs", "purrs", "mrowls", "yowls", "prowls")
+ emote_see = list("laughs", "nyas", "")
+ attack_verb_simple = list("claws", "maims", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps", "beats their greasegun against the face of")
+ variation_list = list() // Prevents random name override
/mob/living/simple_animal/hostile/raider/ranged/boss/mangomatt/Aggro()
- ..()
- summon_backup(15)
- say(pick(\
- "*nya",\
- "*mrowl",\
- "*lynx",\
- "*cougar",\
- "*growl",\
- "*come",\
- "Fuck em' up!"\
- ))
+ . = ..()
+ if(.)
+ return
+ if(!ckey)
+ say(pick("*nya", "*mrowl", "*lynx", "*cougar", "*growl", "*come", "Fuck em' up!"))
/mob/living/simple_animal/hostile/raider/ranged/boss/blueberrybates
name = "Blueberry Bates and his Bottom-Feeder Buys"
@@ -309,58 +300,36 @@
icon_state = "blueberry_bates"
icon_living = "blueberry_bates"
icon_dead = "blueberry_bates_dead"
- mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_BOSS
- move_to_delay = 3.0 //S L O W
+ maxHealth = 200
+ health = 200
+
sight_shoot_delay_time = 0 SECONDS
ranged_cooldown_time = 1 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/incendiary/shotgun
projectilesound = 'sound/f13weapons/shotgun.ogg'
- maxHealth = 200 //bit beefier since his arena is significantly shittier for him and he's more of an annoyance
- health = 200
extra_projectiles = 0
- retreat_distance = 3
- minimum_distance = 3
+
+ retreat_distance = 4
+
loot = list(/obj/item/stack/f13Cash/random/high, /obj/item/ammo_box/shotgun/incendiary, /obj/item/gun/ballistic/shotgun/police)
- speak_emote = list(
- "mutters",
- "counts his caps to himself",
- "yells at someone to pick up the pace",
- "barks",
- "grumbles",
- "grouches"
- )
- emote_see = list(
- "chitters",
- "idly gnaws on a hat",
- )
- attack_verb_simple = list(
- "bayonets",
- "smacks",
- "bites",
- "mauls",
- "slashes",
- "thrashes",
- "bashes",
- "glomps",
- "robusts on"
- )
- variation_list = list() // so he keeps his stupid name
+ speak_emote = list("mutters", "counts his caps to himself", "yells at someone to pick up the pace", "barks", "grumbles", "grouches")
+ emote_see = list("chitters", "idly gnaws on a hat")
+ attack_verb_simple = list("bayonets", "smacks", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps", "robusts on")
+ variation_list = list()
+// FIX: Same double-summon issue as mangomatt. Added proper return check.
/mob/living/simple_animal/hostile/raider/ranged/boss/blueberrybates/Aggro()
- ..()
- summon_backup(15)
- say(pick(\
- "TO ME, BOYS!",\
- "KICK THEIR ASS!",\
- "Fuck 'em up!",\
- "*chitter",\
- "*kyaa",\
- "*come",\
- "YOU'RE ABOUT TO GET A DISCOUNT ON A GRAVE, BUDDY!",\
- ))
-
-// RANGED RAIDER WITH ARMOR
+ . = ..()
+ if(.)
+ return
+ if(!ckey)
+ say(pick("TO ME, BOYS!", "KICK THEIR ASS!", "Fuck 'em up!", "*chitter", "*kyaa", "*come", "YOU'RE ABOUT TO GET A DISCOUNT ON A GRAVE, BUDDY!"))
+
+// ============================================================
+// SPECIALTY RAIDERS
+// ============================================================
+
/mob/living/simple_animal/hostile/raider/ranged/sulphiteranged
icon_state = "metal_raider"
icon_living = "metal_raider"
@@ -368,14 +337,14 @@
mob_armor = ARMOR_VALUE_RAIDER_METAL_ARMOR
maxHealth = 60
health = 60
- rapid_melee = 1
+
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/c45/simple
projectilesound = 'sound/weapons/gunshot.ogg'
+
loot = list(/obj/item/gun/ballistic/automatic/pistol/m1911/custom, /obj/item/clothing/suit/armor/heavy/metal/reinforced, /obj/item/clothing/head/helmet/f13/metalmask/mk2, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 5
- footstep_type = FOOTSTEP_MOB_SHOE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_MEDIUM_VOLUME),
@@ -387,7 +356,6 @@
SP_DISTANT_RANGE(PISTOL_MEDIUM_RANGE_DISTANT)
)
-// FIREFIGHTER RAIDER
/mob/living/simple_animal/hostile/raider/firefighter
icon_state = "firefighter_raider"
icon_living = "firefighter_raider"
@@ -395,12 +363,10 @@
mob_armor = ARMOR_VALUE_RAIDER_ARMOR
maxHealth = 100
health = 100
+ rapid_melee = 1
loot = list(/obj/item/twohanded/fireaxe, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 3
- footstep_type = FOOTSTEP_MOB_SHOE
- rapid_melee = 1
-// BIKER RAIDER
/mob/living/simple_animal/hostile/raider/ranged/biker
icon_state = "biker_raider"
icon_living = "biker_raider"
@@ -410,14 +376,14 @@
mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY
maxHealth = 125
health = 125
- rapid_melee = 1
+
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/a762/sport/simple
projectilesound = 'sound/f13weapons/magnum_fire.ogg'
+
loot = list(/obj/item/gun/ballistic/revolver/thatgun, /obj/item/clothing/suit/armor/medium/combat/rusted, /obj/item/clothing/head/helmet/f13/raidercombathelmet, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 5
- footstep_type = FOOTSTEP_MOB_SHOE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(RIFLE_LIGHT_VOLUME),
@@ -429,22 +395,11 @@
SP_DISTANT_RANGE(RIFLE_LIGHT_RANGE_DISTANT)
)
-/obj/effect/mob_spawn/human/corpse/raider/ranged/biker
- uniform = /obj/item/clothing/under/f13/ncrcf
- suit = /obj/item/clothing/suit/armor/medium/combat/rusted
- shoes = /obj/item/clothing/shoes/f13/explorer
- gloves = /obj/item/clothing/gloves/f13/leather/fingerless
- head = /obj/item/clothing/head/helmet/f13/raidercombathelmet
- neck = /obj/item/clothing/neck/mantle/brown
-
-// YANKEE RAIDER
-
+// Yankee Raider - baseball bat specialist
/mob/living/simple_animal/hostile/raider/baseball
icon_state = "baseball_raider"
icon_living = "baseball_raider"
icon_dead = "baseball_raider_dead"
- retreat_distance = 1
- minimum_distance = 1
melee_damage_lower = 15
melee_damage_upper = 33
mob_armor = ARMOR_VALUE_RAIDER_ARMOR
@@ -453,17 +408,10 @@
rapid_melee = 1
loot = list(/obj/item/twohanded/baseball, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 3
- footstep_type = FOOTSTEP_MOB_SHOE
-
-/obj/effect/mob_spawn/human/corpse/raider/baseball
- uniform = /obj/item/clothing/under/f13/mechanic
- suit = /obj/item/clothing/suit/armor/medium/raider/yankee
- shoes = /obj/item/clothing/shoes/f13/explorer
- gloves = /obj/item/clothing/gloves/f13/leather/fingerless
- head = /obj/item/clothing/head/helmet/f13/raider/yankee
-
-// TRIBAL RAIDER
+/mob/living/simple_animal/hostile/raider/baseball/outlaw
+ name = "Baseball Outlaw"
+ faction = list("raider", "wastelander")
/mob/living/simple_animal/hostile/raider/tribal
icon_state = "tribal_raider"
@@ -474,28 +422,30 @@
health = 125
melee_damage_lower = 12
melee_damage_upper = 37
+ rapid_melee = 1
loot = list(/obj/item/twohanded/spear)
loot_drop_amount = 3
- footstep_type = FOOTSTEP_MOB_SHOE
- rapid_melee = 1
-/obj/effect/mob_spawn/human/corpse/raider/tribal
- uniform = /obj/item/clothing/under/f13/raiderrags
- suit = /obj/item/clothing/suit/armor/light/tribal
- shoes = /obj/item/clothing/shoes/f13/rag
- mask = /obj/item/clothing/mask/facewrap
- head = /obj/item/clothing/head/helmet/f13/fiend
+// only chosen for its damage values - using base is cleaner and more intentional.
+/mob/living/simple_animal/hostile/raider/outlaw
+ name = "Bouncer"
+ faction = list("raider", "wastelander") // FIX: was list("Raiders","Wastelander") - wrong case, broke faction checks
+ icon_state = "badland_raider"
+ icon_living = "badland_raider"
+ icon_dead = "badland_raider_dead"
+ melee_damage_lower = 15 // Preserved from baseball parent
+ melee_damage_upper = 33
-//////////////
-// SULPHITE //
-//////////////
+// ============================================================
+// SULPHITE RAIDERS
+// ============================================================
/mob/living/simple_animal/hostile/raider/sulphite
name = "Sulphite Brawler"
desc = "A raider with low military grade armor and a shishkebab"
icon_state = "sulphite"
icon_living = "sulphite"
- icon_dead= "sulphite_dead"
+ icon_dead = "sulphite_dead"
mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY
maxHealth = 135
health = 135
@@ -504,11 +454,10 @@
melee_damage_upper = 37
loot = list(/obj/item/stack/f13Cash/random/med)
loot_drop_amount = 5
- footstep_type = FOOTSTEP_MOB_SHOE
-/////////////
-// JUNKERS //
-/////////////
+// ============================================================
+// JUNKER GANG
+// ============================================================
/mob/living/simple_animal/hostile/raider/junker
name = "Junker"
@@ -523,7 +472,6 @@
rapid_melee = 1
melee_damage_lower = 18
melee_damage_upper = 42
- footstep_type = FOOTSTEP_MOB_SHOE
/mob/living/simple_animal/hostile/raider/ranged/boss/junker
name = "Junker Footman"
@@ -539,7 +487,6 @@
rapid_melee = 1
melee_damage_lower = 20
melee_damage_upper = 38
- footstep_type = FOOTSTEP_MOB_SHOE
loot_drop_amount = 5
/mob/living/simple_animal/hostile/raider/junker/creator
@@ -551,23 +498,24 @@
mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY
maxHealth = 150
health = 150
+
+ combat_mode = COMBAT_MODE_RANGED
ranged = TRUE
retreat_distance = 6
- minimum_distance = 8
+ minimum_distance = 1
+
rapid_melee = 1
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/c45/op
projectilesound = 'sound/weapons/gunshot.ogg'
+
var/list/spawned_mobs = list()
var/max_mobs = 3
var/mob_types = list(/mob/living/simple_animal/hostile/eyebot/reinforced)
var/spawn_time = 15 SECONDS
var/spawn_text = "flies from"
- footstep_type = FOOTSTEP_MOB_SHOE
loot_drop_amount = 5
-
/mob/living/simple_animal/hostile/raider/junker/creator/Initialize()
. = ..()
AddComponent(/datum/component/spawner, mob_types, spawn_time, faction, spawn_text, max_mobs, _range = 7)
@@ -576,15 +524,11 @@
RemoveComponentByType(/datum/component/spawner)
. = ..()
-/mob/living/simple_animal/hostile/raider/junker/creator/Destroy()
- RemoveComponentByType(/datum/component/spawner)
- . = ..()
-
/mob/living/simple_animal/hostile/raider/junker/creator/Aggro()
. = ..()
if(.)
return
- summon_backup(10)
+ summon_backup(8)
/mob/living/simple_animal/hostile/raider/junker/boss
name = "Junker Boss"
@@ -596,92 +540,65 @@
maxHealth = 165
health = 165
stat_attack = UNCONSCIOUS
+
+ combat_mode = COMBAT_MODE_RANGED
ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
rapid_melee = 1
- retreat_distance = 4
- minimum_distance = 6
extra_projectiles = 2
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
projectiletype = /obj/item/projectile/bullet/shrapnel/simple
projectilesound = 'sound/f13weapons/auto5.ogg'
+
loot = list(/obj/item/stack/f13Cash/random/high)
- footstep_type = FOOTSTEP_MOB_SHOE
- loot_drop_amount = 10
+ loot_drop_amount = 10 // Intentional - boss drops a big pile of caps
loot_amount_random = FALSE
-
-//Outlaw Raiders
+// ============================================================
+// CULTIST RAIDERS
+// implicit empty type that won't inherit random_trash_loot, faction, etc. cleanly.
+// ============================================================
-/mob/living/simple_animal/hostile/raider/baseball/outlaw
- name = "Bouncer"
- faction = list("Raiders","Wastelander")
- icon_state = "badland_raider"
- icon_living = "badland_raider"
- icon_dead = "badland_raider_dead"
-
-// Cultist Stuff
+/mob/living/simple_animal/hostile/raider/cultist
+ faction = list("cultist") // Cultists are separate from the raider faction
+ random_trash_loot = FALSE // Cultists don't carry general wasteland trash
/mob/living/simple_animal/hostile/raider/cultist/melee
name = "Cultist Shredder"
desc = "A nightmare in a robe. Now with 100% less conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cult_axeghoul"
icon_living = "cult_axeghoul"
icon_dead = "cult_dead"
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- turns_per_move = 5
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 100
health = 100
melee_damage_lower = 10
melee_damage_upper = 24
- attack_verb_simple = "clobbers"
- attack_sound = 'sound/weapons/bladeslice.ogg'
- a_intent = INTENT_HARM
- faction = list("raider")
- check_friendly_fire = TRUE
- status_flags = CANPUSH
- del_on_death = FALSE
loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/stack/f13Cash/random/med)
- loot_drop_amount = 2
- footstep_type = FOOTSTEP_MOB_SHOE
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0
- waddle_amount = 2
- waddle_up_time = 1
- waddle_side_time = 1
- retreat_distance = 1 //mob retreats 1 tile when in min distance
- minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
/mob/living/simple_animal/hostile/raider/cultist/ranged
name = "Cultist Gunner"
desc = "A nightmare in a robe. Now with 100% less conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cultist_pistol"
icon_living = "cultist_pistol"
icon_dead = "cultist_dead"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 85
health = 85
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 5
+ minimum_distance = 1
+
ranged_cooldown_time = 2 SECONDS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL
+ // FIX: c10mm projectile was paired with ninemil.ogg sound. Using 10mm sound.
projectiletype = /obj/item/projectile/bullet/c10mm/simple
- projectilesound = 'sound/f13weapons/ninemil.ogg'
+ projectilesound = 'sound/f13weapons/10mm_fire_02.ogg'
+
loot = list(/obj/item/gun/ballistic/automatic/pistol/n99, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 3
- footstep_type = FOOTSTEP_MOB_SHOE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PISTOL_LIGHT_VOLUME),
@@ -695,31 +612,20 @@
/mob/living/simple_animal/hostile/raider/cultist/ranged/shotgun
name = "Cultist Crowd Controller"
- desc = "A nightmare in a robe. Now with 100% less conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cultist_shotgun"
icon_living = "cultist_shotgun"
- icon_dead = "cultist_dead"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
- maxHealth = 85
- health = 85
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+ icon_dead = "cultist_shotgun_dead" // FIX: was missing, fell back to wrong parent dead sprite
+
ranged_cooldown_time = 4 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW
projectiletype = /obj/item/projectile/bullet/pellet/shotgun_buckshot
projectilesound = 'sound/f13weapons/shotgun.ogg'
sound_after_shooting = 'sound/weapons/shotguninsert.ogg'
extra_projectiles = 1
+
loot = list(/obj/item/gun/ballistic/shotgun/trench, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 6
- footstep_type = FOOTSTEP_MOB_SHOE
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(SHOTGUN_VOLUME),
@@ -733,114 +639,87 @@
/mob/living/simple_animal/hostile/raider/cultist/ranged/smg
name = "Cultist Bulletmage"
- desc = "A nightmare in a robe. Now with 100% less conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cultist2_smg"
icon_living = "cultist2_smg"
icon_dead = "cultist2_dead"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
- maxHealth = 85
- health = 85
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+
ranged_cooldown_time = 1 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST
projectiletype = /obj/item/projectile/bullet/c22
projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg'
sound_after_shooting = 'sound/weapons/shotguninsert.ogg'
extra_projectiles = 2
+
loot = list(/obj/item/gun/ballistic/automatic/smg/mini_uzi/smg22, /obj/item/stack/f13Cash/random/med)
loot_drop_amount = 8
- footstep_type = FOOTSTEP_MOB_SHOE
- projectile_sound_properties = list(
- SP_VARY(FALSE),
- SP_VOLUME(PISTOL_LIGHT_VOLUME),
- SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER),
- SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE),
- SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE),
- SP_IGNORE_WALLS(TRUE),
- SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND),
- SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
- )
/mob/living/simple_animal/hostile/raider/cultist/ranged/tesla
name = "Cultist Lasermage"
- desc = "A nightmare in a robe. Now with 100% less conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cultist3_tesla"
icon_living = "cultist3_tesla"
icon_dead = "cultist3_dead"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 150
health = 150
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+
ranged_cooldown_time = 2 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST
projectiletype = /obj/item/projectile/energy/teslacannon/eastwood
projectilesound = 'sound/weapons/resonator_fire.ogg'
sound_after_shooting = 'sound/f13weapons/rcwfire.ogg'
extra_projectiles = 2
+
loot = list(/obj/item/gun/energy/laser/auto/eastwood, /obj/item/stack/f13Cash/random/high)
loot_drop_amount = 8
- footstep_type = FOOTSTEP_MOB_SHOE
- projectile_sound_properties = list(
- SP_VARY(FALSE),
- SP_VOLUME(PISTOL_LIGHT_VOLUME),
- SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER),
- SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE),
- SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE),
- SP_IGNORE_WALLS(TRUE),
- SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND),
- SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
- )
/mob/living/simple_animal/hostile/raider/cultist/ranged/radiation
name = "Cultist Converter"
desc = "A nightmare in a robe. Now with 69% more conversion!"
- icon = 'icons/fallout/mobs/humans/raider.dmi'
icon_state = "cultist3_tesla"
icon_living = "cultist3_tesla"
icon_dead = "cultist3_dead"
- ranged = TRUE
- mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET
maxHealth = 150
health = 150
- rapid_melee = 2
- melee_queue_distance = 5
- move_to_delay = 3.0 //faster than average, but not a lot
- retreat_distance = 4 //mob retreats 1 tile when in min distance
- minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack?
- aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first.
- vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer.
+
ranged_cooldown_time = 2 SECONDS
auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST
projectiletype = /obj/item/projectile/energy/nuclear_particle
projectilesound = 'sound/weapons/resonator_fire.ogg'
sound_after_shooting = 'sound/f13weapons/rcwfire.ogg'
extra_projectiles = 1
+
loot = list(/obj/item/gun/energy/gammagun, /obj/item/stack/f13Cash/random/high)
loot_drop_amount = 10
- footstep_type = FOOTSTEP_MOB_SHOE
- projectile_sound_properties = list(
- SP_VARY(FALSE),
- SP_VOLUME(PISTOL_LIGHT_VOLUME),
- SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER),
- SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE),
- SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE),
- SP_IGNORE_WALLS(TRUE),
- SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND),
- SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT)
- )
+
+// ============================================================
+// CORPSE SPAWNERS
+// ============================================================
+
+/obj/effect/mob_spawn/human/corpse/raider
+ name = "Raider"
+ uniform = /obj/item/clothing/under/f13/rag
+ suit = /obj/item/clothing/suit/armor/medium/raider/iconoclast
+ shoes = /obj/item/clothing/shoes/f13/explorer
+ gloves = /obj/item/clothing/gloves/f13/leather
+ head = /obj/item/clothing/head/helmet/f13/firefighter
+
+/obj/effect/mob_spawn/human/corpse/raider/ranged/biker
+ uniform = /obj/item/clothing/under/f13/ncrcf
+ suit = /obj/item/clothing/suit/armor/medium/combat/rusted
+ shoes = /obj/item/clothing/shoes/f13/explorer
+ gloves = /obj/item/clothing/gloves/f13/leather/fingerless
+ head = /obj/item/clothing/head/helmet/f13/raidercombathelmet
+ neck = /obj/item/clothing/neck/mantle/brown
+
+/obj/effect/mob_spawn/human/corpse/raider/baseball
+ uniform = /obj/item/clothing/under/f13/mechanic
+ suit = /obj/item/clothing/suit/armor/medium/raider/yankee
+ shoes = /obj/item/clothing/shoes/f13/explorer
+ gloves = /obj/item/clothing/gloves/f13/leather/fingerless
+ head = /obj/item/clothing/head/helmet/f13/raider/yankee
+
+/obj/effect/mob_spawn/human/corpse/raider/tribal
+ uniform = /obj/item/clothing/under/f13/raiderrags
+ suit = /obj/item/clothing/suit/armor/light/tribal
+ shoes = /obj/item/clothing/shoes/f13/rag
+ mask = /obj/item/clothing/mask/facewrap
+ head = /obj/item/clothing/head/helmet/f13/fiend
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm b/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm
index 4cbc3440100..f16a89988b8 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm
@@ -1,10 +1,10 @@
-/*IN THIS FILE:
--Rattler
-as per originally proposed concept: less powerful directly than deathclaws, but faster, and venom is rough. don't yell at me if this is OP
-using ant armor b/c it just kinda works here and i don't want it to be super beefy lol.
-*/
+// In this document: Texas Rattler
+// Less powerful directly than deathclaws, but faster and venom is rough.
+
+///////////////////
+// TEXAS RATTLER //
+///////////////////
-//Rattler
/mob/living/simple_animal/hostile/texas_rattler
name = "texas rattler"
desc = "Keratin gleams and articulates over its massive sixty-foot body. Distended venom glands behind its upper pterygoid shudder and pressure deadly venom into its victims. A coil of thick muscle allows it to pounce. In layman's terms: don't get bit."
@@ -12,34 +12,74 @@ using ant armor b/c it just kinda works here and i don't want it to be super bee
icon_state = "texasrattler"
icon_living = "texasrattler"
icon_dead = "texasrattler_dead"
- icon_gib = "texasrattler_gib" //TODO: this is terrible
- mob_armor = ARMOR_VALUE_ANTS
+ icon_gib = "texasrattler_gib"
+
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ mob_armor = ARMOR_VALUE_ANTS // Lightly armored - speed is the threat
+
maxHealth = 150
health = 150
+ speed = -1 // Very fast
+ move_to_delay = 2.5
stat_attack = UNCONSCIOUS
- reach = 2
- speed = -1
- move_to_delay = 2.1
- tastes = list("weird oil" = 5, "dirt" = 1)
-
- speak_emote = list("hisses", "shakes its rattle")
- emote_hear = list("flicks its tongue.")
- emote_taunt = list("flicks its tongue", "hisses and shakes its ratttle")
-
- speak_chance = 10
- taunt_chance = 25
-
+
+ reach = 2 // Long striking range
+ see_in_dark = 8
+
melee_damage_lower = 18
melee_damage_upper = 38
-
+
+ speak_emote = list("hisses", "shakes its rattle")
+ emote_hear = list("flicks its tongue")
+ emote_taunt = list("flicks its tongue", "hisses and shakes its rattle")
attack_verb_simple = "bites and constricts"
- see_in_dark = 8
-
- retreat_distance = 2
-
+
+ speak_chance = 10
+ taunt_chance = 25
+
+ tastes = list("weird oil" = 5, "dirt" = 1)
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab = 4,
+ /obj/item/stack/sheet/sinew = 3,
+ /obj/item/stack/sheet/animalhide = 2
+ )
+ butcher_difficulty = 2
+
faction = list("hostile", "wastebot", "ghoul", "cazador", "supermutant", "bighorner")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - ambush predator with reach
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/texas_rattler/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+/mob/living/simple_animal/hostile/texas_rattler/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
/mob/living/simple_animal/hostile/texas_rattler/AttackingTarget()
. = ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm b/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm
index 7062359c144..9440f2a485b 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm
@@ -1,4 +1,8 @@
-//Fallout 13 super mutants directory
+// Fallout 13 super mutants
+
+// ============================================================
+// BASE SUPER MUTANT
+// ============================================================
/mob/living/simple_animal/hostile/supermutant
name = "super mutant"
@@ -11,75 +15,148 @@
mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
mob_armor = ARMOR_VALUE_SUPERMUTANT_BASE
sentience_type = SENTIENCE_BOSS
- maxHealth = 130
- health = 130
- stat_attack = UNCONSCIOUS
- speak_chance = 10
- speak = list(
- "GRRRRRR!",
- "ARGH!",
- "NNNNNGH!",
- "HMPH!",
- "ARRRRR!"
- )
- speak_emote = list(
- "shouts",
- "yells"
- )
- move_to_delay = 5
+ maxHealth = 110
+ health = 110
stat_attack = CONSCIOUS
robust_searching = 1
- environment_smash = ENVIRONMENT_SMASH_WALLS
+ check_friendly_fire = FALSE
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+
+ speak_chance = 10
+ speak = list("GRRRRRR!", "ARGH!", "NNNNNGH!", "HMPH!", "ARRRRR!")
+ speak_emote = list("shouts", "yells")
+ emote_taunt = list("yells")
emote_taunt_sound = list(
'sound/f13npc/supermutant/attack1.ogg',
'sound/f13npc/supermutant/attack2.ogg',
'sound/f13npc/supermutant/attack3.ogg'
- )
- emote_taunt = list("yells")
+ )
taunt_chance = 30
+
turns_per_move = 5
+ move_to_delay = 4
response_help_simple = "touches"
response_disarm_simple = "pushes"
response_harm_simple = "hits"
- faction = list(
- "hostile",
- "supermutant"
- )
+
+ faction = list("hostile", "supermutant")
+
melee_damage_lower = 20
melee_damage_upper = 35
+
aggro_vision_range = 7
- //tiles within they start attacking, doesn't count the mobs tile
vision_range = 8
- //tiles within they start making noise, does count the mobs tile
+
mob_size = MOB_SIZE_LARGE
move_resist = MOVE_FORCE_OVERPOWERING
attack_verb_simple = "smashes"
attack_sound = "punch"
a_intent = INTENT_GRAB
+
idlesound = list(
'sound/f13npc/supermutant/idle1.ogg',
'sound/f13npc/supermutant/idle2.ogg',
'sound/f13npc/supermutant/idle3.ogg',
'sound/f13npc/supermutant/idle4.ogg'
- )
+ )
death_sound = list(
'sound/f13npc/supermutant/death1.ogg',
'sound/f13npc/supermutant/death2.ogg'
- )
+ )
aggrosound = list(
'sound/f13npc/supermutant/alert1.ogg',
'sound/f13npc/supermutant/alert2.ogg',
'sound/f13npc/supermutant/alert3.ogg',
'sound/f13npc/supermutant/alert4.ogg'
- )
+ )
+
wound_bonus = 0
bare_wound_bonus = 0
footstep_type = FOOTSTEP_MOB_HEAVY
+ // Rage mode vars
+ low_health_threshold = 0.4
+ var/color_rage = "#ff9999"
+
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+
+ can_open_doors = TRUE
+ can_open_airlocks = FALSE
+
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+// FIX: Consolidated death() icon swap here so all subtypes inherit it.
+// Previously every subtype copy-pasted: icon = dead_dmi, icon_state = icon_dead, anchored = FALSE.
+// Now they all inherit this single override.
+/mob/living/simple_animal/hostile/supermutant/death(gibbed)
+ icon = 'icons/fallout/mobs/supermutant_dead.dmi'
+ icon_state = icon_dead
+ . = ..()
+
+// FIX: Added if(.) return guard. Previously summon_backup fired unconditionally,
+// meaning player-controlled supermutants would also call for backup on aggro.
+/mob/living/simple_animal/hostile/supermutant/Aggro()
+ . = ..()
+ if(is_low_health)
+ return // Already raged, skip
+ if(health < (maxHealth * low_health_threshold))
+ make_low_health()
+
+// Friendly fire resistance
+/mob/living/simple_animal/hostile/supermutant/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/supermutant))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+/// RAGE MODE - faster, harder hitting, can smash through walls
+/mob/living/simple_animal/hostile/supermutant/make_low_health()
+ if(!target)
+ return // Not in combat, don't rage
+ ..()
+ visible_message(span_danger("[src] roars with primal fury!!!"))
+ playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15))
+ color = color_rage
+ speed *= 0.8
+ melee_damage_lower = round(melee_damage_lower * 1.3)
+ melee_damage_upper = round(melee_damage_upper * 1.3)
+ obj_damage *= 1.5
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS
+ sound_pitch = -30
+ is_low_health = TRUE
+
+/// Calming down from rage
+/mob/living/simple_animal/hostile/supermutant/make_high_health()
+ visible_message(span_notice("[src]'s rage subsides."))
+ color = initial(color)
+ speed = initial(speed)
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ obj_damage = initial(obj_damage)
+ environment_smash = initial(environment_smash)
+ sound_pitch = initial(sound_pitch)
+ is_low_health = FALSE
+
+/mob/living/simple_animal/hostile/supermutant/Move()
+ if(is_low_health && health > 0)
+ DestroySurroundings()
+ . = ..()
+
+// ============================================================
+// PLAYABLE SUPERMUTANT
+// ============================================================
+
/mob/living/simple_animal/hostile/supermutant/playable
- mob_armor = ARMOR_VALUE_SUPERMUTANT_BASE
- maxHealth = 130
- health = 130
+ maxHealth = 110
+ health = 110
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
@@ -90,17 +167,11 @@
dextrous = TRUE
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
-/mob/living/simple_animal/hostile/supermutant/Aggro()
- ..()
- summon_backup(15)
-
-/mob/living/simple_animal/hostile/supermutant/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
+// ============================================================
+// BRAH-MIN WATCHER
+// ============================================================
-/mob/living/simple_animal/pet/dog/mutant //This is a supermutant, totally not a dog, and he is friendly
+/mob/living/simple_animal/pet/dog/mutant
name = "Brah-Min"
desc = "A large, docile supermutant. Adopted by Shale's Army as a sort of watch dog for their brahmin herd."
icon = 'icons/fallout/mobs/supermutant.dmi'
@@ -109,68 +180,81 @@
icon_dead = "hulk_113_s"
maxHealth = 240
health = 240
- speak_chance = 7 //30 //Oh my god he never shuts up.
+ speak_chance = 7
move_resist = MOVE_FORCE_OVERPOWERING
mob_size = MOB_SIZE_LARGE
- speak = list("Hey! These my brahmins!", "And I say, HEY-YEY-AAEYAAA-EYAEYAA! HEY-YEY-AAEYAAA-EYAEYAA! I SAID HEY, what's going on?", "What do you want from my brahmins?!", "Me gonna clean brahmin poop again now!", "I love brahmins, brahmins are good, just poop much!", "Do not speak to my brahmins ever again, you hear?!", "Bad raiders come to steal my brahmins - I crush with shovel!", "Do not come to my brahmins! Do not touch my brahmins! Do not look at my brahmins!", "I'm watching you, and my brahmins watch too!", "Brahmins say moo, and I'm saying - hey, get your ugly face out of my way!", "I... I remember, before the fire... THERE WERE NO BRAHMINS!", "No! No wind brahmin here! Wind brahmin lie!")
+ speak = list(
+ "Hey! These my brahmins!",
+ "And I say, HEY-YEY-AAEYAAA-EYAEYAA! HEY-YEY-AAEYAAA-EYAEYAA! I SAID HEY, what's going on?",
+ "What do you want from my brahmins?!",
+ "Me gonna clean brahmin poop again now!",
+ "I love brahmins, brahmins are good, just poop much!",
+ "Do not speak to my brahmins ever again, you hear?!",
+ "Bad raiders come to steal my brahmins - I crush with shovel!",
+ "Do not come to my brahmins! Do not touch my brahmins! Do not look at my brahmins!",
+ "I'm watching you, and my brahmins watch too!",
+ "Brahmins say moo, and I'm saying - hey, get your ugly face out of my way!",
+ "I... I remember, before the fire... THERE WERE NO BRAHMINS!",
+ "No! No wind brahmin here! Wind brahmin lie!"
+ )
speak_emote = list("shouts", "yells")
- emote_hear = list("yawns", "mumbles","sighs")
+ emote_hear = list("yawns", "mumbles", "sighs")
emote_see = list("raises his shovel", "shovels some dirt away", "waves his shovel above his head angrily")
response_help_simple = "pat"
response_disarm_simple = "push"
response_harm_simple = "punch"
-// butcher_results = list(/obj/item/weapon/reagent_containers/food/snacks/bearsteak = 3)
/mob/living/simple_animal/pet/dog/mutant/death(gibbed)
icon = 'icons/fallout/mobs/supermutant_dead.dmi'
icon_state = icon_dead
- anchored = FALSE
if(!gibbed)
visible_message(span_danger("\the [src] shouts something incoherent about brahmins for the last time and stops moving..."))
- ..()
+ . = ..()
+
+// ============================================================
+// MELEE SUPERMUTANT
+// ============================================================
/mob/living/simple_animal/hostile/supermutant/meleemutant
name = "sledgehammer supermutant"
desc = "An enormous, green tank of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \
They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \
If that wasn't bad enough, this monstrous critter is wielding a sledgehammer. Lovely."
- icon = 'icons/fallout/mobs/supermutant.dmi'
icon_state = "hulk_melee_s"
icon_living = "hulk_melee_s"
icon_dead = "hulk_melee_s"
mob_armor = ARMOR_VALUE_SUPERMUTANT_MELEE
- maxHealth = 130
- health = 130
+ maxHealth = 110
+ health = 110
mob_armor_tokens = list(
ARMOR_MODIFIER_UP_MELEE_T1,
ARMOR_MODIFIER_DOWN_LASER_T2,
ARMOR_MODIFIER_UP_DT_T2
- )
+ )
melee_damage_lower = 18
melee_damage_upper = 44
attack_sound = "hit_swing"
- footstep_type = FOOTSTEP_MOB_HEAVY
-/mob/living/simple_animal/hostile/supermutant/meleemutant/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
+// ============================================================
+// RANGED SUPERMUTANT
+// ============================================================
/mob/living/simple_animal/hostile/supermutant/rangedmutant
desc = "An enormous green mass of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \
They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \
If that wasn't bad enough, this monstrous critter is wielding a crude shotgun. Lovely."
- icon = 'icons/fallout/mobs/supermutant.dmi'
icon_state = "hulk_ranged_s"
icon_living = "hulk_ranged_s"
icon_dead = "hulk_ranged_s"
- ranged = 1
mob_armor = ARMOR_VALUE_SUPERMUTANT_RANGER
- maxHealth = 130
- health = 130
- retreat_distance = 1
+ maxHealth = 110
+ health = 110
+
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 3
minimum_distance = 1
+
casingtype = /obj/item/ammo_casing/shotgun/improvised
projectiletype = null
projectilesound = 'sound/f13weapons/shotgun.ogg'
@@ -179,11 +263,12 @@
extra_projectiles = 1
auto_fire_delay = GUN_BURSTFIRE_DELAY_FAST
ranged_cooldown_time = 4 SECONDS
+
loot = list(
/obj/item/ammo_box/shotgun/improvised,
/obj/item/gun/ballistic/revolver/widowmaker
- )
- footstep_type = FOOTSTEP_MOB_HEAVY
+ )
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(SHOTGUN_VOLUME),
@@ -195,6 +280,7 @@
SP_DISTANT_RANGE(SHOTGUN_RANGE_DISTANT)
)
+// Varmint rifle variant
/mob/living/simple_animal/hostile/supermutant/rangedmutant/varmint
desc = "An enormous green mass of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \
They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \
@@ -203,93 +289,87 @@
projectiletype = /obj/item/projectile/bullet/a556/simple
projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg'
sound_after_shooting = null
- sound_after_shooting_delay = 1 SECONDS
extra_projectiles = 0
ranged_cooldown_time = 2 SECONDS
- loot = list(
- /obj/item/gun/ballistic/automatic/varmint
- )
+ loot = list(/obj/item/gun/ballistic/automatic/varmint)
-/mob/living/simple_animal/hostile/supermutant/rangedmutant/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
+// ============================================================
+// LEGENDARY SUPERMUTANT
+// ============================================================
/mob/living/simple_animal/hostile/supermutant/legendary
name = "legendary super mutant"
- desc = "A huge and ugly mutant humanoid.He has a faint yellow glow to him, scars adorn his body. This super mutant is a grizzled vetern of combat. Look out!"
+ desc = "A huge and ugly mutant humanoid. He has a faint yellow glow to him, scars adorn his body. This super mutant is a grizzled veteran of combat. Look out!"
color = "#FFFF00"
+ color_rage = "#ffcc66"
mob_armor = ARMOR_VALUE_SUPERMUTANT_LEGEND
- maxHealth = 150
- health = 150
+ maxHealth = 130
+ health = 130
icon_state = "hulk_113_s"
icon_living = "hulk_113_s"
icon_dead = "hulk_113_s"
melee_damage_lower = 27
melee_damage_upper = 57
- mob_size = 5
- footstep_type = FOOTSTEP_MOB_HEAVY
+ mob_size = MOB_SIZE_HUGE // FIX: was raw 5
-/mob/living/simple_animal/hostile/supermutant/legendary/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
+// ============================================================
+// NIGHTKIN
+// Semi-invisible supermutant variants
+// ============================================================
/mob/living/simple_animal/hostile/supermutant/nightkin
name = "nightkin"
- desc = "A blue variant of the standard Super Mutant, equiped with steathboys."
- icon = 'icons/fallout/mobs/supermutant.dmi'
+ desc = "A blue variant of the standard Super Mutant, equipped with stealth boys."
icon_state = "night_s"
icon_living = "night_s"
icon_dead = "night_s"
mob_armor = ARMOR_VALUE_SUPERMUTANT_MELEE
- maxHealth = 140
- health = 140
+ maxHealth = 120
+ health = 120
alpha = 80
force_threshold = 15
melee_damage_lower = 27
melee_damage_upper = 50
attack_verb_simple = "slashes"
attack_sound = "sound/weapons/bladeslice.ogg"
- footstep_type = FOOTSTEP_MOB_HEAVY
+ color_rage = "#9999ff"
+
+ // Nightkin have excellent night vision for stealth operations
+ has_low_light_vision = TRUE
/mob/living/simple_animal/hostile/supermutant/nightkin/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
alpha = 255
-/mob/living/simple_animal/hostile/supermutant/nightkin/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
-
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant
name = "nightkin veteran"
- desc = "A blue variant of the standard Super Mutant, equiped with steathboys. This one is holding an Assault Rifle."
- icon = 'icons/fallout/mobs/supermutant.dmi'
+ desc = "A blue variant of the standard Super Mutant, equipped with stealth boys. This one is holding an Assault Rifle."
icon_state = "night_ranged_s"
icon_living = "night_ranged_s"
icon_dead = "night_ranged_s"
mob_armor = ARMOR_VALUE_SUPERMUTANT_RANGER
- maxHealth = 140
- health = 140
- ranged = 1
+ maxHealth = 120
+ health = 120
alpha = 80
force_threshold = 15
melee_damage_lower = 25
melee_damage_upper = 37
attack_verb_simple = "smashes"
attack_sound = "punch"
- extra_projectiles = 1
+
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
retreat_distance = 4
- minimum_distance = 6
+ minimum_distance = 4
+
+ extra_projectiles = 1
projectiletype = /obj/item/projectile/bullet/a556/simple
projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg'
loot = list(/obj/item/ammo_box/magazine/m556/rifle)
- footstep_type = FOOTSTEP_MOB_HEAVY
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(RIFLE_LIGHT_VOLUME),
@@ -302,39 +382,37 @@
)
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
alpha = 255
-/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
-
/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant
name = "nightkin elite"
desc = "A blue variant of the standard Super Mutant, and a remnant of the Masters Army."
- icon = 'icons/fallout/mobs/supermutant.dmi'
icon_state = "night_boss_s"
icon_living = "night_boss_s"
icon_dead = "night_boss_s"
- ranged = 1
mob_armor = ARMOR_VALUE_SUPERMUTANT_LEGEND
- maxHealth = 130
- health = 130
+ maxHealth = 110
+ health = 110
alpha = 80
force_threshold = 15
melee_damage_lower = 20
melee_damage_upper = 47
attack_verb_simple = "smashes"
attack_sound = "punch"
+
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
retreat_distance = 5
- minimum_distance = 7
+ minimum_distance = 5
+
projectiletype = /obj/item/projectile/f13plasma/repeater
projectilesound = 'sound/f13weapons/plasma_rifle.ogg'
loot = list(/obj/item/stock_parts/cell/ammo/mfc)
- footstep_type = FOOTSTEP_MOB_HEAVY
+
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PLASMA_VOLUME),
@@ -347,148 +425,158 @@
)
/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.) // FIX: same guard as nightkin/Aggro
+ return
+ summon_backup(10)
alpha = 255
-/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/death(gibbed)
- icon = 'icons/fallout/mobs/supermutant_dead.dmi'
- icon_state = icon_dead
- anchored = FALSE
- ..()
+// ============================================================
+// CULT OF RAIN VARIANTS
+// ============================================================
-//Cult Of Rain
/mob/living/simple_animal/hostile/supermutant/meleemutant/rain
name = "super mutant rain cultist"
desc = "A super mutant covered in blue markings that has been indoctrinated into the Cult Of Rain. This one wields a sledgehammer blessed by the rain gods."
color = "#6B87C0"
+ color_rage = "#4488cc"
speak_chance = 10
speak = list("The rain cleanses!", "Sacrifices for the rain gods!", "The thunder guides my fury!", "I am become the storm, destroyer of all heretics!", "The priests will be pleased with my sacrifices!")
- maxHealth = 360
- health = 360
- damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ maxHealth = 140
+ health = 140
+ damage_coeff = list(BRUTE = 0.9, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 24
melee_damage_upper = 48
- footstep_type = FOOTSTEP_MOB_HEAVY
-
+
/mob/living/simple_animal/hostile/supermutant/rangedmutant/rain
name = "super mutant rain cultist"
desc = "A super mutant covered in blue markings that has been indoctrinated into the Cult Of Rain. This one wields a hunting rifle blessed by the rain gods."
color = "#6B87C0"
+ color_rage = "#4488cc"
speak_chance = 10
speak = list("The rain cleanses!", "Sacrifices for the rain gods!", "The thunder guides my fury!", "I am become the storm, destroyer of all heretics!", "The priests will be pleased with my sacrifices!")
- maxHealth = 360
- health = 360
- damage_coeff = list(BRUTE = 1, BURN = 0.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ maxHealth = 140
+ health = 140
+ damage_coeff = list(BRUTE = 1, BURN = 0.9, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 24
melee_damage_upper = 48
- footstep_type = FOOTSTEP_MOB_HEAVY
+// Berserker nightkin - charges when shot, leaves destruction in its wake
/mob/living/simple_animal/hostile/supermutant/nightkin/rain
name = "nightkin berserker rain priest"
desc = "A nightkin that spreads the word of the Cult Of Rain. They are covered in dark blue markings, indicating that they have been blessed by the rain god Odile."
color = "#6666FF"
+ color_rage = "#3333ff"
speak_chance = 10
speak = list("The rain speaks through me!", "Witness the gifts of rain!", "The great flood will come upon us! Do not fear it!", "My life for the rain gods!", "The rain gods can always use more sacrifices!")
- maxHealth = 360
- health = 360
- damage_coeff = list(BRUTE = -0.1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ maxHealth = 180
+ health = 180
+ damage_coeff = list(BRUTE = 0.8, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 24
melee_damage_upper = 48
var/charging = FALSE
- footstep_type = FOOTSTEP_MOB_HEAVY
+ var/charge_cooldown = 0
+ var/charge_cooldown_time = 8 SECONDS
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/bullet_act(obj/item/projectile/Proj)
if(!Proj)
return
- if(prob(30))
+
+ if(!charging && world.time > charge_cooldown && prob(20))
visible_message(span_danger("\The [src] lets out a vicious war cry!"))
- addtimer(3)
- Charge()
+ addtimer(CALLBACK(src, PROC_REF(Charge)), 0.3 SECONDS)
+
if(prob(85) || Proj.damage > 30)
return ..()
- else
- visible_message(span_danger("\The [Proj] is abosrbed by \the [src]'s thick skin, strengthening it!"))
- return 0
+ visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin!"))
+ return 0
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect)
if(!charging)
- ..()
+ . = ..()
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/AttackingTarget()
if(!charging)
- return ..()
+ . = ..()
+// Blocks pathfinding during charge throw so AI doesn't fight the physics
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/Goto(target, delay, minimum_distance)
if(!charging)
- ..()
+ . = ..()
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/Move()
- if(charging)
- new /obj/effect/temp_visual/decoy/fading(loc,src)
+ if(charging || (is_low_health && health > 0))
+ new /obj/effect/temp_visual/decoy/fading(loc, src)
DestroySurroundings()
. = ..()
- if(charging)
- DestroySurroundings()
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/Charge()
+ if(!target)
+ return
+
var/turf/T = get_turf(target)
if(!T || T == loc)
return
+
charging = TRUE
- visible_message(span_danger(">[src] charges!"))
+ charge_cooldown = world.time + charge_cooldown_time
+
+ visible_message(span_danger("[src] charges with terrifying fury!"))
DestroySurroundings()
walk(src, 0)
setDir(get_dir(src, T))
- var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src)
- animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 1)
- addtimer(3)
- throw_at(T, get_dist(src, T), 1, src, 0, callback = CALLBACK(src, PROC_REF(charge_end)))
-/mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/charge_end(list/effects_to_destroy)
+ var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc, src)
+ animate(D, alpha = 0, color = "#FF0000", transform = matrix() * 2, time = 0.1 SECONDS)
+
+ throw_at(T, get_dist(src, T), 2, src, FALSE, callback = CALLBACK(src, PROC_REF(charge_end)))
+
+/mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/charge_end(atom/hit_atom)
charging = FALSE
if(target)
Goto(target, move_to_delay, minimum_distance)
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/Bump(atom/A)
if(charging)
- if(isturf(A) || isobj(A) && A.density)
+ if((isturf(A) || isobj(A)) && A.density)
A.ex_act(EXPLODE_HEAVY)
+ playsound(src, 'sound/effects/meteorimpact.ogg', 100, 1)
DestroySurroundings()
- ..()
+ . = ..()
/mob/living/simple_animal/hostile/supermutant/nightkin/rain/throw_impact(atom/A)
if(!charging)
return ..()
- else if(isliving(A))
+ if(isliving(A))
var/mob/living/L = A
L.visible_message(span_danger("[src] slams into [L]!"), span_userdanger("[src] slams into you!"))
- L.apply_damage(melee_damage_lower/2, BRUTE)
+ L.apply_damage(melee_damage_upper, BRUTE)
playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1)
shake_camera(L, 4, 3)
shake_camera(src, 2, 3)
var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src)))
- L.throw_at(throwtarget, 3)
-
+ L.throw_at(throwtarget, 3, 2)
charging = FALSE
- charging = FALSE
-
+// Guardian nightkin - ranged fire-release aura when hit
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain
name = "nightkin guardian rain priest"
desc = "A nightkin that spreads the word of the Cult Of Rain. They are covered in dark blue markings, indicating that they have been blessed by the rain god Ignacio."
color = "#6666FF"
+ color_rage = "#3333ff"
speak_chance = 10
speak = list("The rain speaks through me!", "Witness the gifts of rain!", "The great flood will come upon us! Do not fear it!", "My life for the rain gods!", "The rain gods can always use more sacrifices!")
- maxHealth = 380
- health = 380
- damage_coeff = list(BRUTE = 1, BURN = -0.25, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ maxHealth = 160
+ health = 160
+ damage_coeff = list(BRUTE = 1, BURN = 0.8, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 35
melee_damage_upper = 60
extra_projectiles = 2
- retreat_distance = 2
+
+ combat_mode = COMBAT_MODE_RANGED
+ retreat_distance = 4
minimum_distance = 4
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/Initialize(mapload)
@@ -498,18 +586,16 @@
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/bullet_act(obj/item/projectile/Proj)
if(!Proj)
return
- if(prob(20))
+ if(prob(15))
visible_message(span_danger("\The [src] lets out a vicious war cry!"))
fire_release()
if(prob(85) || Proj.damage > 30)
return ..()
- else
- visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin, strengthening it!"))
- return 0
+ visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin!"))
+ return 0
/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/proc/fire_release()
- playsound(get_turf(src),'sound/magic/fireball.ogg', 200, 1)
-
+ playsound(get_turf(src), 'sound/magic/fireball.ogg', 200, 1)
for(var/d in GLOB.cardinals)
INVOKE_ASYNC(src, PROC_REF(fire_release_wall), d)
@@ -518,7 +604,7 @@
var/turf/E = get_edge_target_turf(src, dir)
var/range = 10
var/turf/previousturf = get_turf(src)
- for(var/turf/J in getline(src,E))
+ for(var/turf/J in getline(src, E))
if(!range || (J != previousturf && (!previousturf.atmos_adjacent_turfs || !previousturf.atmos_adjacent_turfs[J])))
break
range--
@@ -531,22 +617,23 @@
to_chat(L, span_userdanger("You're hit by the nightkin's release of energy!"))
hit_things += L
previousturf = J
- addtimer(1)
+ sleep(1) // FIX: was addtimer(1) with no callback - invalid call that would runtime
+// Rain lord nightkin - most powerful, dual resistance, healing aura
/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/rain
name = "nightkin rain lord"
desc = "A nightkin that writes the word of the Cult Of Rain. They are covered in dark blue markings and are adorned in pieces of bone armor, indicating that they are blessed by the rain god Hyacinth."
color = "#6666FF"
+ color_rage = "#3333ff"
speak_chance = 10
speak = list("The great flood will come, I will make sure of it!", "Rain god Odile, I call upon you for wrath!", "Rain god Hyacinth, I call upon you for a tranquil mind!", "Rain god Ignacio, I call upon you for protection!", "The storm rages within!")
- maxHealth = 440
- health = 440
- damage_coeff = list(BRUTE = 0.5, BURN = 0.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ maxHealth = 200
+ health = 200
+ damage_coeff = list(BRUTE = 0.8, BURN = 0.8, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 28
melee_damage_upper = 62
extra_projectiles = 1
-
/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/rain/Initialize(mapload)
. = ..()
AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/supermutant, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS | FIRELOSS)
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm b/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm
index 9850ddcb6c1..8abe376e4ae 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm
@@ -1,82 +1,169 @@
+// In this document: Trogs, Spore Carriers, Tunnelers, Blind One
+
+/////////////
+// TROGS //
+/////////////
+
+// BASE TROG
/mob/living/simple_animal/hostile/trog
name = "trog"
- desc = "A human who has mutated and regressed back to a primal, cannibalistic state. Rumor says they're poisonous as well. Want to find out? "
+ desc = "A human who has mutated and regressed back to a primal, cannibalistic state. Rumor says they're poisonous as well. Want to find out?"
icon = 'icons/fallout/mobs/monsters/tunnelers.dmi'
icon_state = "troglodyte"
icon_living = "troglodyte"
icon_dead = "trog_dead"
-
- speed = 2
+
+ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
+
maxHealth = 40
health = 40
- obj_damage = 30
+ speed = 2
+ move_to_delay = 3
+ turns_per_move = 5
+
melee_damage_lower = 5
- melee_damage_upper = 15
+ melee_damage_upper = 12
harm_intent_damage = 5
+ obj_damage = 30
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
waddle_amount = 2
waddle_up_time = 1
waddle_side_time = 1
- mob_biotypes = MOB_ORGANIC|MOB_HUMANOID
- robust_searching = TRUE
- turns_per_move = 5
+
speak_emote = list("growls")
emote_see = list("screeches")
- a_intent = INTENT_HARM
attack_verb_simple = "lunges at"
attack_sound = 'sound/hallucinations/growl1.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
unsuitable_atmos_damage = 20
- stat_attack = CONSCIOUS
- gold_core_spawnable = HOSTILE_SPAWN
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/human = 2,
+ /obj/item/stack/sheet/animalhide/human = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
+
faction = list("trog")
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human = 2,
- /obj/item/stack/sheet/animalhide/human = 1,
- /obj/item/stack/sheet/bone = 1)
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - swarm ambusher
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/trog/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/trog/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// SPORE CARRIER - fungal zombie
/mob/living/simple_animal/hostile/trog/sporecarrier
name = "spore carrier"
- desc = "A victim of the beauveria mordicana fungus, these corpses sole purpose is to spread its spores."
+ desc = "A victim of the beauveria mordicana fungus. This corpse's sole purpose is to spread its spores."
icon_state = "spore_carrier"
icon_living = "spore_carrier"
icon_dead = "spore_dead"
- health = 80
+
maxHealth = 80
- harm_intent_damage = 5
+ health = 80
+
melee_damage_lower = 10
melee_damage_upper = 25
- attack_sound = 'sound/hallucinations/growl1.ogg'
+
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)
unsuitable_atmos_damage = 0
+
faction = list("plants")
+
guaranteed_butcher_results = list(/obj/item/stack/sheet/bone = 1)
+
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+/mob/living/simple_animal/hostile/trog/sporecarrier/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin/spore_toxin, 2)
+
+/mob/living/simple_animal/hostile/trog/sporecarrier/death(gibbed)
+ // Release spores on death
+ if(!gibbed)
+ visible_message(span_warning("[src]'s body ruptures, releasing a cloud of spores!"))
+ var/datum/effect_system/smoke_spread/chem/S = new
+ S.set_up(1, get_turf(src))
+ S.start()
+ . = ..()
+///////////////
+// TUNNELERS //
+///////////////
+
+// BASE TUNNELER - deadly swarm creature
/mob/living/simple_animal/hostile/trog/tunneler
name = "tunneler"
desc = "A mutated creature that is sensitive to light, but can swarm and kill even Deathclaws."
icon_state = "tunneler"
icon_living = "tunneler"
icon_dead = "tunneler_dead"
- robust_searching = TRUE
- stat_attack = CONSCIOUS
- health = 144
+
+ mob_armor = ARMOR_VALUE_TUNNELER
+
maxHealth = 144
+ health = 144
speed = 1
+
melee_damage_lower = 18
melee_damage_upper = 32
armour_penetration = 0.25
obj_damage = 150
+
see_in_dark = 8
+
attack_sound = 'sound/weapons/bladeslice.ogg'
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = -1, CLONE = 0, STAMINA = 0, OXY = 0)
- unsuitable_atmos_damage = 5
- faction = list("tunneler")
- guaranteed_butcher_results = list(/obj/item/stack/sheet/bone = 1)
death_sound = 'sound/f13npc/ghoul/ghoul_death.ogg'
+
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = -1, CLONE = 0, STAMINA = 0, OXY = 0) // Immune to toxins
+
+ faction = list("tunneler")
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/bone = 1,
+ /obj/item/stack/sheet/sinew = 1
+ )
+
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
/mob/living/simple_animal/hostile/trog/tunneler/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
/mob/living/simple_animal/hostile/trog/tunneler/AttackingTarget()
. = ..()
@@ -84,46 +171,53 @@
var/mob/living/carbon/human/H = target
H.reagents.add_reagent(/datum/reagent/toxin, 5)
-
+// BLIND ONE - boss tunneler
/mob/living/simple_animal/hostile/trog/tunneler/blindone
name = "Blind One"
desc = "A...tunneler? Her scales reflect the light oddly, almost making her transparent, but her eyes are solid. She moves blindingly quickly, darting in and out of view despite her size. Overfilled, swelling venom-sacs line her throat."
- icon = 'icons/fallout/mobs/monsters/tunnelers.dmi'
icon_state = "blindone"
icon_living = "blindone"
icon_dead = "trog_dead"
+
mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER
+
gender = FEMALE
- resize = 1.3
- alpha = 150
- speed = 1
+ alpha = 150 // Semi-transparent
+
maxHealth = 150
health = 150
- has_field_of_vision = FALSE
- obj_damage = 30
+ speed = 1
+
melee_damage_lower = 18
melee_damage_upper = 40
+ obj_damage = 30 // Reduced from 150
+
vision_range = 9
aggro_vision_range = 18
- retreat_distance = 6
- turns_per_move = 5
+ has_field_of_vision = FALSE // 360 degree vision
+
speak_emote = list("mumbles incoherently")
- emote_see = list("screeches")
- a_intent = INTENT_HARM
- attack_verb_simple = "lunges at"
attack_sound = 'sound/hallucinations/veryfar_noise.ogg'
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- unsuitable_atmos_damage = 20
- stat_attack = CONSCIOUS
- gold_core_spawnable = HOSTILE_SPAWN
- faction = list("tunneler")
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human = 2,
- /obj/item/stack/sheet/animalhide/human = 1,
- /obj/item/stack/sheet/bone = 1)
+
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+ despawns_when_lonely = FALSE
+
+ // Rage mode at low health
+ low_health_threshold = 0.3
+ var/color_rage = "#FF3333"
+
+/mob/living/simple_animal/hostile/trog/tunneler/blindone/Initialize()
+ . = ..()
+ resize = 1.3
+ update_transform()
/mob/living/simple_animal/hostile/trog/tunneler/blindone/Aggro()
- ..()
- summon_backup(15)
+ . = ..()
+ if(.)
+ return
+ summon_backup(12)
+ if(!ckey)
+ visible_message(span_danger("[src] lets out an otherworldly screech!"))
/mob/living/simple_animal/hostile/trog/tunneler/blindone/AttackingTarget()
. = ..()
@@ -133,3 +227,64 @@
H.reagents.add_reagent(/datum/reagent/toxin/venom, 5)
H.reagents.add_reagent(/datum/reagent/toxin/mindbreaker, 3)
+// RAGE MODE - Blind One becomes faster and more aggressive
+/mob/living/simple_animal/hostile/trog/tunneler/blindone/make_low_health()
+ visible_message(span_danger("[src] writhes in pain, moving with renewed fury!"))
+ playsound(src, 'sound/hallucinations/veryfar_noise.ogg', 100, 1)
+ alpha = 100 // More transparent
+ speed *= 0.75 // Faster
+ melee_damage_lower = round(melee_damage_lower * 1.3)
+ melee_damage_upper = round(melee_damage_upper * 1.3)
+ armour_penetration = 0.35 // Increased from 0.25
+ is_low_health = TRUE
+
+/mob/living/simple_animal/hostile/trog/tunneler/blindone/make_high_health()
+ visible_message(span_notice("[src]'s movements slow slightly."))
+ alpha = initial(alpha)
+ speed = initial(speed)
+ melee_damage_lower = initial(melee_damage_lower)
+ melee_damage_upper = initial(melee_damage_upper)
+ armour_penetration = initial(armour_penetration)
+ is_low_health = FALSE
+
+// TUNNELER SWARM - weak but numerous
+/mob/living/simple_animal/hostile/trog/tunneler/swarm
+ name = "tunneler spawn"
+ desc = "A young tunneler, still developing its deadly capabilities."
+
+ maxHealth = 60
+ health = 60
+ speed = 0.8 // Faster
+
+ melee_damage_lower = 10
+ melee_damage_upper = 18
+ armour_penetration = 0.1
+ obj_damage = 50
+
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+/mob/living/simple_animal/hostile/trog/tunneler/swarm/Initialize()
+ . = ..()
+ resize = 0.7
+ update_transform()
+
+/mob/living/simple_animal/hostile/trog/tunneler/swarm/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(15) // Call for MORE help
+
+// REAGENTS
+/datum/reagent/toxin/spore_toxin
+ name = "Fungal Spores"
+ description = "Airborne spores from the beauveria mordicana fungus."
+ color = "#228B22"
+ toxpwr = 0.5
+ taste_description = "decay"
+
+/datum/reagent/toxin/spore_toxin/on_mob_life(mob/living/M)
+ if(volume >= 10)
+ M.adjustToxLoss(2, 0)
+ if(prob(5))
+ to_chat(M, span_warning("You feel strange spores growing in your lungs..."))
+ ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm b/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm
index 31a19b5f4c1..c591263ec7f 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm
@@ -2,6 +2,7 @@
// GECKO //
///////////
+// BASE GECKO - all shared properties
/mob/living/simple_animal/hostile/gecko
name = "gecko"
desc = "A large mutated reptile with sharp teeth."
@@ -9,450 +10,177 @@
icon_state = "gekko"
icon_living = "gekko"
icon_dead = "gekko_dead"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2,
- /obj/item/stack/sheet/animalhide/gecko = 1)
- butcher_results = list(/obj/item/stack/sheet/bone = 1)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
+
maxHealth = 35
health = 35
- harm_intent_damage = 8
- obj_damage = 20
+ speed = 0
+ move_to_delay = 2.5
+ turns_per_move = 5
+
melee_damage_lower = 4
melee_damage_upper = 12
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
+ harm_intent_damage = 8
+ obj_damage = 20
+
aggro_vision_range = 7
vision_range = 8
+
+ // Reptilian eyes adapted to wasteland conditions
+ has_low_light_vision = TRUE
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2,
+ /obj/item/stack/sheet/animalhide/gecko = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
+ butcher_difficulty = 1
+
waddle_amount = 3
waddle_up_time = 1
waddle_side_time = 2
pass_flags = PASSTABLE
- speak_emote = list(
- "squeaks",
- "cackles",
- "snickers",
- "shriek",
- "scream",
- "skrem",
- "scrambles",
- "warbles",
- "chirps",
- "cries",
- "kyaas",
- "chortles",
- "gecks"
- )
- emote_see = list(
- "screeches",
- "licks its eyes",
- "twitches",
- "scratches its frills",
- "gonks",
- "honks",
- "scronks",
- "sniffs",
- "gecks"
- )
- attack_verb_simple = list(
- "bites",
- "claws",
- "tears at",
- "dabs",
- "scratches",
- "gnaws",
- "chews",
- "chomps",
- "lunges",
- "gecks"
- )
- atmos_requirements = list(
- "min_oxy" = 5,
- "max_oxy" = 0,
- "min_tox" = 0,
- "max_tox" = 1,
- "min_co2" = 0,
- "max_co2" = 5,
- "min_n2" = 0,
- "max_n2" = 0
- )
- faction = list("gecko", "critter-friend") // critter-friend is a flag for related beast friend/master quirk. Makes hostile mob passive for quirk holder.
+
+ response_help_simple = "pets"
+ response_disarm_simple = "gently pushes aside"
+ response_harm_simple = "hits"
+
+ speak_emote = list("squeaks", "cackles", "snickers", "shrieks", "screams", "warbles", "chirps", "cries", "kyaas", "chortles", "gecks")
+ emote_see = list("screeches", "licks its eyes", "twitches", "scratches its frills", "gonks", "honks", "sniffs", "gecks")
+ attack_verb_simple = list("bites", "claws", "tears at", "scratches", "gnaws", "chews", "chomps", "lunges", "gecks")
+
+ atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
+ faction = list("gecko", "critter-friend")
a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
footstep_type = FOOTSTEP_MOB_CLAW
- idlesound = list(
- 'sound/f13npc/gecko/geckocall1.ogg',
- 'sound/f13npc/gecko/geckocall2.ogg',
- 'sound/f13npc/gecko/geckocall3.ogg',
- 'sound/f13npc/gecko/geckocall4.ogg',
- 'sound/f13npc/gecko/geckocall5.ogg'
- )
-
+
emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/f13npc/gecko/gecko_charge1.ogg',
- 'sound/f13npc/gecko/gecko_charge2.ogg',
- 'sound/f13npc/gecko/gecko_charge3.ogg'
- )
+ emote_taunt_sound = list('sound/f13npc/gecko/gecko_charge1.ogg', 'sound/f13npc/gecko/gecko_charge2.ogg', 'sound/f13npc/gecko/gecko_charge3.ogg')
+ taunt_chance = 30
aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg')
+ idlesound = list('sound/f13npc/gecko/geckocall1.ogg', 'sound/f13npc/gecko/geckocall2.ogg', 'sound/f13npc/gecko/geckocall3.ogg', 'sound/f13npc/gecko/geckocall4.ogg', 'sound/f13npc/gecko/geckocall5.ogg')
death_sound = 'sound/f13npc/gecko/gecko_death.ogg'
- can_ghost_into = TRUE // not a bad idea at all
+
+ can_ghost_into = TRUE
desc_short = "Short, angry, and as confused as they are tasty."
- desc_important = "Still in development! Report wierdness on the discord!"
-
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
variation_list = list(
MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255),
MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2, 2.6, 3.0, 3.3, 3.7),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(50),
MOB_HEALTH_LIST(30, 35, 40, 45),
- MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 5, 7, 9),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(0, 0, 4, 6),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
)
+/mob/living/simple_animal/hostile/gecko/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/gecko/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
/mob/living/simple_animal/hostile/gecko/become_the_mob(mob/user)
make_a_nest = /obj/effect/proc_holder/mob_common/make_nest/gecko
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter
send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter
. = ..()
-//Fire Geckos//
-
+// FIRE GECKO - ranged variant that spits fire
/mob/living/simple_animal/hostile/gecko/fire
- name = "fire spitter gecko"
+ name = "fire gecko"
desc = "A large mutated reptile with sharp teeth and a warm disposition. Sorta smells like sulphur."
- icon = 'icons/fallout/mobs/animals/wasteanimals.dmi'
- icon_state = "gekko"
- icon_living = "gekko"
- icon_dead = "gekko_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2,
- /obj/item/stack/sheet/animalhide/gecko = 1)
- butcher_results = list(/obj/item/stack/sheet/bone = 1)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
- maxHealth = 35
- health = 35
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 4
- melee_damage_upper = 12
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
- aggro_vision_range = 7
- vision_range = 8
- waddle_amount = 3
- waddle_up_time = 1
- waddle_side_time = 2
- pass_flags = PASSTABLE
- speak_emote = list(
- "squeaks",
- "cackles",
- "snickers",
- "shriek",
- "scream",
- "skrem",
- "scrambles",
- "warbles",
- "chirps",
- "cries",
- "kyaas",
- "chortles",
- "gecks"
- )
- emote_see = list(
- "screeches",
- "licks its eyes",
- "twitches",
- "scratches its frills",
- "gonks",
- "honks",
- "scronks",
- "sniffs",
- "gecks"
- )
- attack_verb_simple = list(
- "bites",
- "claws",
- "tears at",
- "dabs",
- "scratches",
- "gnaws",
- "chews",
- "chomps",
- "lunges",
- "gecks"
- )
- atmos_requirements = list(
- "min_oxy" = 5,
- "max_oxy" = 0,
- "min_tox" = 0,
- "max_tox" = 1,
- "min_co2" = 0,
- "max_co2" = 5,
- "min_n2" = 0,
- "max_n2" = 0
- )
- a_intent = INTENT_HARM
- gold_core_spawnable = HOSTILE_SPAWN
- footstep_type = FOOTSTEP_MOB_CLAW
+
+ maxHealth = 30
+ health = 30
+
+ // Mixed combat - fire spit + melee
+ combat_mode = COMBAT_MODE_MIXED
ranged = TRUE
+ retreat_distance = 3
+ minimum_distance = 1
+
check_friendly_fire = TRUE
+ ranged_cooldown_time = 3 SECONDS
projectiletype = /obj/item/projectile/geckofire
projectilesound = 'sound/magic/fireball.ogg'
-
- emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/f13npc/gecko/gecko_charge1.ogg',
- 'sound/f13npc/gecko/gecko_charge2.ogg',
- 'sound/f13npc/gecko/gecko_charge3.ogg'
- )
- aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg')
- death_sound = 'sound/f13npc/gecko/gecko_death.ogg'
- can_ghost_into = TRUE // not a bad idea at all
- desc_short = "Short, angry, and as confused as they are tasty."
- desc_important = "Still in development! Report wierdness on the discord!"
-
+ ranged_message = "spits fire"
+
variation_list = list(
MOB_COLOR_VARIATION(200, 40, 40, 255, 45, 45),
MOB_SPEED_LIST(2.6, 3.0, 3.3, 3.7),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(50),
MOB_HEALTH_LIST(28, 30, 32),
- MOB_RETREAT_DISTANCE_LIST(0, 1, 3),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(1, 2, 3),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
)
/mob/living/simple_animal/hostile/gecko/fire/Initialize()
- .=..()
+ . = ..()
resize = 0.8
update_transform()
-
-/* firey gecko spit
- * DAMAGE: 5
- * STAMIN: 5
- * RECOIL: 2
- * WOUNDS: 0
- * WNAKED: 0
- */
-/obj/item/projectile/geckofire
- name = "flaming gecko spit"
- icon = 'icons/effects/fire.dmi'
- icon_state = "3"
- range = 4
- light_range = LIGHT_RANGE_FIRE
- light_color = LIGHT_COLOR_FIRE
- damage = BULLET_DAMAGE_SHOTGUN_PELLET * BULLET_DAMAGE_FIRE
- stamina = BULLET_STAMINA_SHOTGUN_PELLET * BULLET_STAMINA_FIRE
- spread = BULLET_SPREAD_SURPLUS
- recoil = BULLET_RECOIL_SHOTGUN_PELLET
-
- wound_bonus = BULLET_WOUND_SHOTGUN_PELLET * BULLET_WOUND_FIRE
- bare_wound_bonus = BULLET_WOUND_SHOTGUN_PELLET_NAKED_MULT * BULLET_NAKED_WOUND_FIRE
- wound_falloff_tile = BULLET_WOUND_FALLOFF_PISTOL_LIGHT
-
- pixels_per_second = BULLET_SPEED_SHOTGUN_PELLET * 0.35
- damage_falloff = BULLET_FALLOFF_DEFAULT_PISTOL_LIGHT
-
- sharpness = SHARP_NONE
- zone_accuracy_type = ZONE_WEIGHT_SHOTGUN
-
-/obj/item/projectile/geckofire/on_hit(atom/target)
- . = ..()
- if(prob(1))
- name = "flaming gecko yartz"
- if(iscarbon(target))
- var/mob/living/carbon/M = target
- M.adjust_fire_stacks(2)
- if(M.fire_stacks > 2)
- M.IgniteMob()
-
-
-//Smaller Legacy Geckos//
-//Faster and more aggressive than normal geckos, but also easier even squishier.
-
+// LEGACY GECKO (NEWT) - smaller, faster, aggressive
/mob/living/simple_animal/hostile/gecko/legacy
name = "newt"
- desc = "A large dog sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way."
+ desc = "A large dog-sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way."
icon = 'icons/fallout/mobs/legacymobs.dmi'
icon_state = "legacy_gecko"
icon_living = "legacy_gecko"
icon_dead = "legacy_gecko_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
+
sidestep_per_cycle = 2
- guaranteed_butcher_results = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2,
- /obj/item/stack/sheet/animalhide/gecko = 1)
- butcher_results = list(/obj/item/stack/sheet/bone = 1)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
- maxHealth = 35
- health = 35
- harm_intent_damage = 8
- obj_damage = 20
+ dodging = TRUE
+
melee_damage_lower = 7
melee_damage_upper = 18
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
- aggro_vision_range = 7
- vision_range = 7
+
waddle_amount = 5
- waddle_up_time = 1
- waddle_side_time = 1
- pass_flags = PASSTABLE
- speak_emote = list(
- "squeaks",
- "cackles",
- "snickers",
- "shriek",
- "scream",
- "skrem",
- "scrambles",
- "warbles",
- "chirps",
- "cries",
- "kyaas",
- "chortles",
- "gecks"
- )
- emote_see = list(
- "screeches",
- "licks its eyes",
- "twitches",
- "scratches its frills",
- "gonks",
- "honks",
- "scronks",
- "sniffs",
- "gecks"
- )
- attack_verb_simple = list(
- "bites",
- "claws",
- "tears at",
- "dabs",
- "scratches",
- "gnaws",
- "chews",
- "chomps",
- "lunges",
- "gecks"
- )
- atmos_requirements = list(
- "min_oxy" = 5,
- "max_oxy" = 0,
- "min_tox" = 0,
- "max_tox" = 1,
- "min_co2" = 0,
- "max_co2" = 5,
- "min_n2" = 0,
- "max_n2" = 0
- )
- a_intent = INTENT_HARM
- gold_core_spawnable = HOSTILE_SPAWN
- footstep_type = FOOTSTEP_MOB_CLAW
-
sound_pitch = 70
vary_pitches = list(40, 80)
- emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/f13npc/gecko/gecko_charge1.ogg',
- 'sound/f13npc/gecko/gecko_charge2.ogg',
- 'sound/f13npc/gecko/gecko_charge3.ogg'
- )
- aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg')
- death_sound = 'sound/f13npc/gecko/gecko_death.ogg'
- can_ghost_into = TRUE // not a bad idea at all
- desc_short = "Short, angry, and as confused as they are tasty."
- desc_important = "Still in development! Report wierdness on the discord!"
-
-
+
+// ALPHA NEWT - stronger variant with stamina damage
/mob/living/simple_animal/hostile/gecko/legacy/alpha
name = "alpha newt"
- desc = "A large dog sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way. This one's drooling a lot and looks sort of tired."
- icon = 'icons/fallout/mobs/legacymobs.dmi'
+ desc = "A large dog-sized amphibious biped with an oddly large mouth for its size. This one's drooling a lot and looks sort of tired."
icon_state = "legacy_gecko2"
icon_living = "legacy_gecko2"
- icon_dead = "legacy_gecko_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
+
+ vision_range = 9
+
guaranteed_butcher_results = list(
/obj/item/reagent_containers/food/snacks/meat/slab/gecko = 3,
- /obj/item/stack/sheet/animalhide/gecko = 1)
- butcher_results = list(/obj/item/stack/sheet/bone = 1)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
- maxHealth = 35
- health = 35
- harm_intent_damage = 8
- obj_damage = 20
- melee_damage_lower = 7
- melee_damage_upper = 18
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
- aggro_vision_range = 7
- vision_range = 9
+ /obj/item/stack/sheet/animalhide/gecko = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
- faction = list("gecko")
- a_intent = INTENT_HARM
- gold_core_spawnable = HOSTILE_SPAWN
- footstep_type = FOOTSTEP_MOB_CLAW
-
- emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/f13npc/gecko/gecko_charge1.ogg',
- 'sound/f13npc/gecko/gecko_charge2.ogg',
- 'sound/f13npc/gecko/gecko_charge3.ogg'
- )
- aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg')
- death_sound = 'sound/f13npc/gecko/gecko_death.ogg'
- can_ghost_into = TRUE // not a bad idea at all
- desc_short = "Short, angry, and as confused as they are tasty."
- desc_important = "Still in development! Report wierdness on the discord!"
-
variation_list = list(
- MOB_COLOR_VARIATION(180, 255, 255, 255, 255, 255), //Rmin, Gmin, Bmin, Rmax, Gmax, Bmax
+ MOB_COLOR_VARIATION(180, 255, 255, 255, 255, 255),
MOB_SPEED_LIST(1.8, 2.0, 2.2),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(80),
MOB_HEALTH_LIST(30, 35, 40),
- MOB_RETREAT_DISTANCE_LIST(0, 1),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50),
- MOB_MINIMUM_DISTANCE_LIST(1, 2),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50),
- )
+ )
/mob/living/simple_animal/hostile/gecko/legacy/alpha/AttackingTarget()
. = ..()
@@ -460,136 +188,118 @@
var/mob/living/carbon/human/H = target
H.reagents.add_reagent(/datum/reagent/toxin/staminatoxin, 1)
+// BIG GECKO - slow, heavy hitter, poor vision
/mob/living/simple_animal/hostile/gecko/big
- name = "big gecko"
name = "big gecko"
desc = "A large mutated reptile with sharp teeth. This one's pretty big, but its eyes seem clouded and it moves a bit clumsily."
- icon = 'icons/fallout/mobs/animals/wasteanimals.dmi'
- icon_state = "gekko"
- icon_living = "gekko"
- icon_dead = "gekko_dead"
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 6,
- /obj/item/stack/sheet/animalhide/gecko = 2)
- butcher_results = list(/obj/item/stack/sheet/bone = 2)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
- maxHealth = 35
- health = 35
- harm_intent_damage = 8
- obj_damage = 20
+
+ maxHealth = 110
+ health = 110
+
melee_damage_lower = 12
melee_damage_upper = 24
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
+
aggro_vision_range = 4
vision_range = 4
-
-
- faction = list("gecko")
- a_intent = INTENT_HARM
- gold_core_spawnable = HOSTILE_SPAWN
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 6,
+ /obj/item/stack/sheet/animalhide/gecko = 2,
+ /obj/item/stack/sheet/bone = 2
+ )
+
footstep_type = FOOTSTEP_MOB_HEAVY
-
sound_pitch = -75
vary_pitches = list(-100, -80)
- emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/f13npc/gecko/gecko_charge1.ogg',
- 'sound/f13npc/gecko/gecko_charge2.ogg',
- 'sound/f13npc/gecko/gecko_charge3.ogg'
- )
- aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg')
- death_sound = 'sound/f13npc/gecko/gecko_death.ogg'
- can_ghost_into = TRUE // not a bad idea at all
- desc_short = "Short, angry, and as confused as they are tasty."
- desc_important = "Still in development! Report wierdness on the discord!"
-
+
variation_list = list(
- MOB_COLOR_VARIATION(120, 80, 80, 250, 100, 100), //Rmin, Gmin, Bmin, Rmax, Gmax, Bmax
+ MOB_COLOR_VARIATION(120, 80, 80, 250, 100, 100),
MOB_SPEED_LIST(2.5, 2.8, 3.0),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(80),
MOB_HEALTH_LIST(100, 110, 120),
- MOB_RETREAT_DISTANCE_LIST(0, 1),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50),
- MOB_MINIMUM_DISTANCE_LIST(1, 2),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50),
)
/mob/living/simple_animal/hostile/gecko/big/Initialize()
- .=..()
+ . = ..()
resize = 1.5
update_transform()
+// PLAYABLE GECKO - for ghost roles
/mob/living/simple_animal/hostile/gecko/playable
- health = 40
maxHealth = 40
- speed = 0
+ health = 40
+ melee_damage_lower = 8
+ melee_damage_upper = 12
+
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
see_in_dark = 8
wander = 0
- anchored = FALSE
- melee_damage_lower = 8
- melee_damage_upper = 12
- footstep_type = FOOTSTEP_MOB_CLAW
-/// Testing its randomness
+// DEBUG GECKOS - for testing
/mob/living/simple_animal/hostile/gecko/debug
sound_pitch = 100
vary_pitches = list(-200, 200)
variation_list = list(
- MOB_NAME_FROM_GLOBAL_LIST(\
- MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 2),\
- MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1),\
- MOB_RANDOM_NAME(MOB_NAME_RANDOM_ALL_OF_THEM, 5)\
+ "varied_global_names" = list(
+ MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 2),
+ MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1),
+ MOB_RANDOM_NAME(MOB_NAME_RANDOM_ALL_OF_THEM, 5)
),
MOB_COLOR_VARIATION(20, 190, 0, 255, 2, 0),
MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2, 2.6, 3.0, 3.3, 3.7),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(100),
- MOB_HEALTH_LIST(2, 3, 5, 7, 30, 35, 37, 38, 40, 45, 48, 49, 49, 49, 49, 2000),
- MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 5, 7, 9),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(0, 0, 4, 6),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
+ MOB_HEALTH_LIST(2, 3, 5, 7, 30, 35, 37, 38, 40, 45, 48, 49, 2000),
)
-/// Testing its randomness
/mob/living/simple_animal/hostile/gecko/debug/stamcrit
variation_list = list(
MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1)),
MOB_HEALTH_LIST(50),
- MOB_RETREAT_DISTANCE_LIST(4),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(2),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
)
-/// Testing its randomness
/mob/living/simple_animal/hostile/gecko/debug/stamcrit/Initialize()
. = ..()
new /obj/item/gun/energy/disabler/debug(get_turf(src))
-/mob/living/simple_animal/hostile/gecko/Aggro()
+// GECKO FIRE PROJECTILE
+/obj/item/projectile/geckofire
+ name = "flaming gecko spit"
+ icon = 'icons/effects/fire.dmi'
+ icon_state = "3"
+ range = 4
+ light_range = LIGHT_RANGE_FIRE
+ light_color = LIGHT_COLOR_FIRE
+
+ damage = BULLET_DAMAGE_SHOTGUN_PELLET * BULLET_DAMAGE_FIRE
+ stamina = BULLET_STAMINA_SHOTGUN_PELLET * BULLET_STAMINA_FIRE
+ spread = BULLET_SPREAD_SURPLUS
+ recoil = BULLET_RECOIL_SHOTGUN_PELLET
+ wound_bonus = BULLET_WOUND_SHOTGUN_PELLET * BULLET_WOUND_FIRE
+ bare_wound_bonus = BULLET_WOUND_SHOTGUN_PELLET_NAKED_MULT * BULLET_NAKED_WOUND_FIRE
+ wound_falloff_tile = BULLET_WOUND_FALLOFF_PISTOL_LIGHT
+ pixels_per_second = BULLET_SPEED_SHOTGUN_PELLET * 0.35
+ damage_falloff = BULLET_FALLOFF_DEFAULT_PISTOL_LIGHT
+ sharpness = SHARP_NONE
+ zone_accuracy_type = ZONE_WEIGHT_SHOTGUN
+
+/obj/item/projectile/geckofire/on_hit(atom/target)
. = ..()
- if(.)
- return
- summon_backup(15)
+ if(prob(1))
+ name = "flaming gecko yartz"
+ if(iscarbon(target))
+ var/mob/living/carbon/M = target
+ M.adjust_fire_stacks(2)
+ if(M.fire_stacks > 2)
+ M.IgniteMob()
//////////////////////////
// NIGHTSTALKERS & PELT //
//////////////////////////
+// BASE NIGHTSTALKER
/mob/living/simple_animal/hostile/stalker
name = "nightstalker"
desc = "A crazed genetic hybrid of rattlesnake and coyote DNA."
@@ -598,80 +308,118 @@
icon_living = "nightstalker"
icon_dead = "nightstalker-dead"
icon_gib = null
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
+
+ maxHealth = 80
+ health = 80
+ speed = 1
+ move_to_delay = 2.5
turns_per_move = 5
- move_to_delay = 2
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 0
- //how far they pull back
- minimum_distance = 0
- // how close you can get before they try to pull back
-
+ melee_damage_lower = 4
+ melee_damage_upper = 12
+ harm_intent_damage = 8
+ obj_damage = 15
+
aggro_vision_range = 7
- //tiles within they start attacking, doesn't count the mobs tile
-
vision_range = 8
- //tiles within they start making noise, does count the mobs tile
-
+
+ // Nightstalkers are specifically adapted for nocturnal hunting
+ has_low_light_vision = TRUE
+ low_light_bonus = 4
+
guaranteed_butcher_results = list(
/obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2,
/obj/item/stack/sheet/sinew = 2,
/obj/item/stack/sheet/bone = 2
- )
+ )
butcher_results = list(
/obj/item/clothing/head/f13/stalkerpelt = 1,
/obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1
- )
+ )
butcher_difficulty = 3
+
+ waddle_amount = 3
+ waddle_up_time = 1
+ waddle_side_time = 1
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "bites"
- emote_taunt = list("growls")
- taunt_chance = 30
- speed = 1
- maxHealth = 80
- health = 80
- harm_intent_damage = 8
- obj_damage = 15
- melee_damage_lower = 4
- melee_damage_upper = 12
+
+ speak_emote = list("growls")
attack_verb_simple = "bites"
attack_sound = 'sound/creatures/nightstalker_bite.ogg'
- speak_emote = list("growls")
+
+ emote_taunt = list("growls")
+ emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg')
+ idlesound = list('sound/f13npc/nightstalker/idle1.ogg')
+ death_sound = 'sound/f13npc/nightstalker/death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("nightstalkers")
- gold_core_spawnable = HOSTILE_SPAWN
a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
footstep_type = FOOTSTEP_MOB_CLAW
- waddle_amount = 3
- waddle_up_time = 1
- waddle_side_time = 1
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - venomous bite
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/stalker/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/stalker/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+/mob/living/simple_animal/hostile/stalker/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 6)
+// PLAYABLE NIGHTSTALKER
/mob/living/simple_animal/hostile/stalker/playable
- health = 80
maxHealth = 80
+ health = 80
+ melee_damage_lower = 10
+ melee_damage_upper = 15
+
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
see_in_dark = 8
wander = 0
- anchored = FALSE
- melee_damage_lower = 10
- melee_damage_upper = 15
-
-/mob/living/simple_animal/hostile/stalker/AttackingTarget()
- . = ..()
- if(. && ishuman(target))
- var/mob/living/carbon/human/H = target
- H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 6)
-/mob/living/simple_animal/hostile/stalker/playable/legion
+/mob/living/simple_animal/hostile/stalker/playable/legion
name = "legionstalker"
- desc = "A nightstalker bred specifically for the legion under the use of combat and companionship. legionstalkers have the body and loyalty of a canine but the agility and deadlyness of rattlesnake."
+ desc = "A nightstalker bred specifically for the legion for combat and companionship. Legionstalkers have the body and loyalty of a canine but the agility and deadliness of a rattlesnake."
icon_state = "nightstalker-legion"
icon_living = "nightstalker-legion"
icon_dead = "nightstalker-legion-dead"
@@ -684,62 +432,87 @@
icon_state = "nightstalker_cub"
icon_living = "nightstalker_cub"
icon_dead = "nightstalker_cub_dead"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- retreat_distance = 8
- minimum_distance = 6
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2, /obj/item/stack/sheet/sinew = 1, /obj/item/stack/sheet/bone = 1)
- butcher_results = list(/obj/item/clothing/head/f13/stalkerpelt = 1, /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1)
- response_help_simple = "pets"
- response_disarm_simple = "pushes aside"
- response_harm_simple = "kicks"
- taunt_chance = 30
- speed = 1
+
maxHealth = 50
health = 50
- harm_intent_damage = 8
- obj_damage = 15
+ speed = 1
+ turns_per_move = 5
+
melee_damage_lower = 5
melee_damage_upper = 10
- attack_verb_simple = "bites"
+ harm_intent_damage = 8
+ obj_damage = 15
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2,
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
+ butcher_results = list(
+ /obj/item/clothing/head/f13/stalkerpelt = 1,
+ /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1
+ )
+
+ waddle_amount = 4
+ waddle_up_time = 1
+ waddle_side_time = 2
+
+ response_help_simple = "pets"
+ response_disarm_simple = "pushes aside"
+ response_harm_simple = "kicks"
+
speak_emote = list("howls")
+ attack_verb_simple = "bites"
+ attack_sound = 'sound/f13npc/nightstalker/attack1.ogg'
+
+ emote_taunt = list("growls", "snarls")
+ emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg')
+ idlesound = list('sound/f13npc/nightstalker/idle1.ogg')
+ death_sound = 'sound/f13npc/nightstalker/death.ogg'
+
atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
faction = list("nightstalkers", "critter-friend")
- gold_core_spawnable = HOSTILE_SPAWN
a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
footstep_type = FOOTSTEP_MOB_CLAW
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Pure melee - runs away and calls for help
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
- emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg')
- emote_taunt = list("growls", "snarls")
- aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg')
- idlesound = list('sound/f13npc/nightstalker/idle1.ogg')
- death_sound = 'sound/f13npc/nightstalker/death.ogg'
- attack_sound = 'sound/f13npc/nightstalker/attack1.ogg'
- waddle_amount = 4
- waddle_up_time = 1
- waddle_side_time = 2
+/mob/living/simple_animal/hostile/stalkeryoung/Aggro()
+ ..()
+ summon_backup(12)
+
+/mob/living/simple_animal/hostile/stalkeryoung/AttackingTarget()
+ . = ..()
+ if(. && ishuman(target))
+ var/mob/living/carbon/human/H = target
+ H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 2)
/mob/living/simple_animal/hostile/stalkeryoung/playable
- health = 80
maxHealth = 80
+ health = 80
+ melee_damage_lower = 5
+ melee_damage_upper = 10
+
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
see_in_dark = 8
wander = 0
- anchored = FALSE
- melee_damage_lower = 5
- melee_damage_upper = 10
- footstep_type = FOOTSTEP_MOB_CLAW
-
-/mob/living/simple_animal/hostile/stalker/AttackingTarget()
- . = ..()
- if(. && ishuman(target))
- var/mob/living/carbon/human/H = target
- H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 2)
+// NIGHTSTALKER ITEMS
/obj/item/clothing/head/f13/stalkerpelt
name = "nightstalker pelt"
desc = "A hat made from nightstalker pelt which makes the wearer feel both comfortable and elegant."
@@ -758,19 +531,18 @@
name = "nightstalker meat"
desc = "Could taste like rich red meat or flavorful chicken, depending on where the cut comes from."
list_reagents = list(/datum/reagent/consumable/nutriment = 6, /datum/reagent/consumable/nutriment/vitamin = 2)
- bitesize = 4 //Average animal
+ bitesize = 4
filling_color = "#FA8072"
tastes = list("rich meat" = 3)
cooked_type = /obj/item/reagent_containers/food/snacks/meat/steak/nightstalker_meat
- slice_path = null
foodtype = RAW | MEAT
/obj/item/reagent_containers/food/snacks/meat/steak/nightstalker_meat
name = "nightstalker steak"
- desc = "A surprisingly high quality steak that could come in a variety of textures and may taste of either good chicken or rich beef"
+ desc = "A surprisingly high quality steak that could come in a variety of textures and may taste of either good chicken or rich beef."
/////////////
-// MOLERAT // It's time ~TK
+// MOLERAT //
/////////////
/mob/living/simple_animal/hostile/molerat
@@ -780,208 +552,288 @@
icon_state = "molerat"
icon_living = "molerat"
icon_dead = "molerat_dead"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- can_ghost_into = TRUE
- speak_chance = 0
+
+ maxHealth = 25
+ health = 25
+ speed = 2
turns_per_move = 5
- guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/molerat = 2, /obj/item/stack/sheet/sinew = 1,/obj/item/stack/sheet/animalhide/molerat = 1, /obj/item/stack/sheet/bone = 1)
- butcher_results = list(/obj/item/stack/sheet/bone = 1)
+
+ melee_damage_lower = 4
+ melee_damage_upper = 10
+ harm_intent_damage = 8
+ obj_damage = 15
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/molerat = 2,
+ /obj/item/stack/sheet/sinew = 1,
+ /obj/item/stack/sheet/animalhide/molerat = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
butcher_difficulty = 1.5
+
+ waddle_amount = 3
+ waddle_up_time = 1
+ waddle_side_time = 2
+
response_help_simple = "pets"
response_disarm_simple = "gently pushes aside"
response_harm_simple = "hits"
- taunt_chance = 30
- speed = 2
- maxHealth = 25
- health = 25
- harm_intent_damage = 8
- obj_damage = 15
- melee_damage_lower = 4
- melee_damage_upper = 10
+
+ speak_emote = list("chitters")
attack_verb_simple = "bites"
attack_sound = 'sound/creatures/molerat_attack.ogg'
- speak_emote = list("chitters")
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- faction = list("hostile", "gecko")
- gold_core_spawnable = HOSTILE_SPAWN
- a_intent = INTENT_HARM
-
- emote_taunt_sound = list('sound/f13npc/molerat/taunt.ogg')
+
emote_taunt = list("hisses")
- aggrosound = list('sound/f13npc/molerat/aggro1.ogg', 'sound/f13npc/molerat/aggro2.ogg',)
+ emote_taunt_sound = list('sound/f13npc/molerat/taunt.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/f13npc/molerat/aggro1.ogg', 'sound/f13npc/molerat/aggro2.ogg')
idlesound = list('sound/f13npc/molerat/idle.ogg')
death_sound = 'sound/f13npc/molerat/death.ogg'
- waddle_amount = 3
- waddle_up_time = 1
- waddle_side_time = 2
+
+ atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
+ faction = list("hostile", "gecko")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
desc_short = "Small, squishy, and numerous."
pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
-
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 40
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
variation_list = list(
MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255),
MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(5),
MOB_HEALTH_LIST(15, 20, 25, 26),
- MOB_RETREAT_DISTANCE_LIST(0, 1),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100),
- MOB_MINIMUM_DISTANCE_LIST(0, 1),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(5),
)
+/mob/living/simple_animal/hostile/molerat/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/molerat/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
/mob/living/simple_animal/hostile/molerat/become_the_mob(mob/user)
call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter
send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter
make_a_nest = /obj/effect/proc_holder/mob_common/make_nest/molerat
. = ..()
+//////////////////
+// GELATIN CUBE //
+//////////////////
+
/mob/living/simple_animal/hostile/gelcube
name = "gelatinous cube"
- desc = "A big green radioactive cube creature, it jiggles with menacing wiggles and is making some sort of goofy face at you."
+ desc = "A big green radioactive cube creature. It jiggles with menacing wiggles and is making some sort of goofy face at you."
icon = 'fallout/icons/mob/vatgrowing.dmi'
icon_state = "gelatinous"
icon_living = "gelatinous"
icon_dead = "gelatinous_dead"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- can_ghost_into = TRUE
- speak_chance = 0
+
+ maxHealth = 850
+ health = 850
+ speed = 8
+ move_to_delay = 6
turns_per_move = 10
+
+ melee_damage_lower = 35
+ melee_damage_upper = 45
+ harm_intent_damage = 30
+ obj_damage = 15
+
guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/soup/amanitajelly = 2)
butcher_results = list(/obj/item/reagent_containers/food/snacks/soup/amanitajelly = 1)
butcher_difficulty = 1.5
+
loot = list(/obj/item/stack/f13Cash/random/med)
- /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list
loot_drop_amount = 10
- /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items
loot_amount_random = TRUE
- /// slots in a list of trash loot
var/random_trash_loot = TRUE
+
+ waddle_amount = 4
+ waddle_up_time = 3
+ waddle_side_time = 2
+
response_help_simple = "jiggles"
response_disarm_simple = "wiggles"
response_harm_simple = "shakes"
- taunt_chance = 30
- speed = 8
- maxHealth = 850
- health = 850
- harm_intent_damage = 30
- obj_damage = 15
- melee_damage_lower = 35
- melee_damage_upper = 45
- move_to_delay = 10
+
+ speak_emote = list("glorbles")
attack_verb_simple = "goops"
attack_sound = 'sound/effects/attackblob.ogg'
- speak_emote = list("glorbles")
- atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
- faction = list("the tungsten cube") //at last, I am at peace ~TK
- gold_core_spawnable = HOSTILE_SPAWN
- a_intent = INTENT_HARM
-
- emote_taunt_sound = list('sound/effects/bubbles.ogg')
+
emote_taunt = list("blorgles")
+ emote_taunt_sound = list('sound/effects/bubbles.ogg')
+ taunt_chance = 30
aggrosound = list('sound/misc/splort.ogg')
- idlesound = list('sound/vore/prey/squish_01.ogg') //God forgive me for what I must do. Its just a perfect sound. ~TK
+ idlesound = list('sound/vore/prey/squish_01.ogg')
death_sound = 'sound/misc/crack.ogg'
- waddle_amount = 4
- waddle_up_time = 3
- waddle_side_time = 2
+
+ atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
+
+ faction = list("the tungsten cube")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
desc_short = "Big, squishy, and gelatinous."
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 80 // Very slow
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
/mob/living/simple_animal/hostile/gelcube/Initialize()
. = ..()
if(random_trash_loot)
loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment
+/mob/living/simple_animal/hostile/gelcube/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/gelcube/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
////////////
-//T-Birds//
-//////////
+// T-BIRD //
+////////////
/mob/living/simple_animal/hostile/bloodbird
- name = "Blood Bird"
+ name = "blood bird"
desc = "A large mutated turkey vulture."
icon = 'icons/fallout/mobs/animals/bloodbird.dmi'
icon_state = "bloodbird"
icon_living = "bloodbird"
icon_dead = "bloodbird_dead"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- speak_chance = 0
- turns_per_move = 5
- guaranteed_butcher_results = list(
- /obj/item/reagent_containers/food/snacks/meat/slab/chicken = 4,
- /obj/item/feather = 3)
- butcher_results = list(/obj/item/stack/sheet/bone = 2)
- butcher_difficulty = 1
- response_help_simple = "pets"
- response_disarm_simple = "gently pushes aside"
- response_harm_simple = "hits"
- taunt_chance = 30
- speed = 0
+
maxHealth = 100
health = 100
- harm_intent_damage = 8
- obj_damage = 20
+ speed = 0
+ move_to_delay = 2.5
+ turns_per_move = 5
+
melee_damage_lower = 25
melee_damage_upper = 35
- move_to_delay = 1.5
- retreat_distance = 0
- minimum_distance = 0
+ harm_intent_damage = 8
+ obj_damage = 20
+
aggro_vision_range = 9
vision_range = 8
+
+ guaranteed_butcher_results = list(
+ /obj/item/reagent_containers/food/snacks/meat/slab/chicken = 4,
+ /obj/item/feather = 3,
+ /obj/item/stack/sheet/bone = 2
+ )
+ butcher_difficulty = 1
+
waddle_amount = 5
waddle_up_time = 1
waddle_side_time = 1
pass_flags = PASSTABLE
- speak_emote = list(
- "cackles",
- "squawks",
- "clacks",
- )
- emote_see = list(
- "screeches",
- "gonks"
- )
- attack_verb_simple = list(
- "bites",
- "claws",
- "rends",
- "mutilates"
- )
+
+ response_help_simple = "pets"
+ response_disarm_simple = "gently pushes aside"
+ response_harm_simple = "hits"
+
+ speak_emote = list("cackles", "squawks", "clacks")
+ emote_see = list("screeches", "gonks")
+ attack_verb_simple = list("bites", "claws", "rends", "mutilates")
+
+ emote_taunt = list("screeches")
+ emote_taunt_sound = list('sound/creatures/terrorbird/hoot1.ogg', 'sound/creatures/terrorbird/hoot2.ogg', 'sound/creatures/terrorbird/hoot3.ogg', 'sound/creatures/terrorbird/hoot4.ogg')
+ taunt_chance = 30
+ aggrosound = list('sound/creatures/terrorbird/growl1.ogg', 'sound/creatures/terrorbird/growl2.ogg', 'sound/creatures/terrorbird/growl3.ogg')
+ idlesound = list('sound/creatures/terrorbird/clack1.ogg', 'sound/creatures/terrorbird/clack2.ogg', 'sound/creatures/terrorbird/clack3.ogg')
+ death_sound = list('sound/creatures/terrorbird/groan1.ogg', 'sound/creatures/terrorbird/groan2.ogg')
+
faction = list("terror bird")
a_intent = INTENT_HARM
gold_core_spawnable = HOSTILE_SPAWN
footstep_type = FOOTSTEP_MOB_HEAVY
- idlesound = list(
- 'sound/creatures/terrorbird/clack1.ogg',
- 'sound/creatures/terrorbird/clack2.ogg',
- 'sound/creatures/terrorbird/clack3.ogg',
- )
-
- emote_taunt = list("screeches")
- emote_taunt_sound = list(
- 'sound/creatures/terrorbird/hoot1.ogg',
- 'sound/creatures/terrorbird/hoot2.ogg',
- 'sound/creatures/terrorbird/hoot3.ogg',
- 'sound/creatures/terrorbird/hoot4.ogg',
- )
- aggrosound = list(
- 'sound/creatures/terrorbird/growl1.ogg',
- 'sound/creatures/terrorbird/growl2.ogg',
- 'sound/creatures/terrorbird/growl3.ogg',
- )
- death_sound = list(
- 'sound/creatures/terrorbird/groan1.ogg',
- 'sound/creatures/terrorbird/groan2.ogg',
- )
- can_ghost_into = FALSE //One day Kotetsu will return to us. ~TK
+
+ can_ghost_into = FALSE
desc_short = "What a terrifying bird."
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30 // Faster
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
-
variation_list = list(
MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255),
MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2),
MOB_SPEED_CHANGE_PER_TURN_CHANCE(50),
MOB_HEALTH_LIST(80, 90, 100, 110),
- MOB_RETREAT_DISTANCE_LIST(0, 0, 1),
- MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(90),
- MOB_MINIMUM_DISTANCE_LIST(0, 0, 0, 1),
- MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(90),
)
+
+/mob/living/simple_animal/hostile/bloodbird/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(10)
+
+/mob/living/simple_animal/hostile/bloodbird/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm b/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm
index 868bef63b07..45748a45b88 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm
@@ -1,11 +1,17 @@
-/*IN THIS FILE:
--Handy
--Gutsy
--Protectrons
--Robobrains
--Assaultrons
-*/
+// In this document: Handy, Gutsy, Protectrons, Robobrains, Assaultrons, Liberator
+// Shared robot properties as a define for clarity:
+// - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
+// - damage_coeff: immune to TOX, CLONE, STAMINA, OXY
+// - atmos_requirements: no atmosphere needed
+// - blood_volume = 0, healable = FALSE
+// - del_on_death = TRUE (except playable variants)
+
+///////////////
+// MR. HANDY //
+///////////////
+
+// BASE HANDY - melee saw bot
/mob/living/simple_animal/hostile/handy
name = "mr. handy"
desc = "A crazed pre-war household assistant robot, armed with a cutting saw."
@@ -13,135 +19,175 @@
icon_state = "handy"
icon_living = "handy"
icon_dead = "robot_dead"
- speed = 2
- can_ghost_into = TRUE
- gender = NEUTER
+
mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
- move_resist = MOVE_FORCE_OVERPOWERING // Can't be pulled
mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
- maxHealth = 100
+
+ maxHealth = 100
health = 100
+ speed = 2
stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT
+
+ melee_damage_lower = 12
+ melee_damage_upper = 24
+
+ robust_searching = TRUE
+ stat_attack = CONSCIOUS
+
+ move_resist = MOVE_FORCE_OVERPOWERING
+
+ faction = list("wastebot")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+ check_friendly_fire = TRUE
+ del_on_death = TRUE
+ healable = FALSE
+ blood_volume = 0
+
+ gender = NEUTER
+
emp_flags = list(
MOB_EMP_STUN,
MOB_EMP_BERSERK,
MOB_EMP_DAMAGE,
MOB_EMP_SCRAMBLE
- )
- healable = FALSE
- stat_attack = CONSCIOUS
- auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER
- melee_damage_lower = 12
- melee_damage_upper = 24
- robust_searching = TRUE
- attack_verb_simple = "saws"
- faction = list("wastebot")
+ )
+
+ damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
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)
minbodytemp = 0
+
speak_emote = list("states")
- gold_core_spawnable = HOSTILE_SPAWN
- del_on_death = TRUE
- deathmessage = "blows apart!"
- taunt_chance = 30
- blood_volume = 0
- waddle_amount = 3
- waddle_up_time = 2
- waddle_side_time = 1
- send_mobs = null
- call_backup = null
-
+ attack_verb_simple = "saws"
+ attack_sound = 'sound/f13npc/handy/attack.wav'
+
+ emote_taunt = list("raises a saw")
emote_taunt_sound = list(
'sound/f13npc/handy/taunt1.ogg',
'sound/f13npc/handy/taunt2.ogg'
- )
- emote_taunt = list("raises a saw")
-
+ )
+ taunt_chance = 30
aggrosound = list(
'sound/f13npc/handy/aggro1.ogg',
'sound/f13npc/handy/aggro2.ogg'
- )
+ )
idlesound = list(
'sound/f13npc/handy/idle1.wav',
'sound/f13npc/handy/idle2.ogg',
'sound/f13npc/handy/idle3.ogg'
- )
-
+ )
death_sound = 'sound/f13npc/handy/robo_death.ogg'
- attack_sound = 'sound/f13npc/handy/attack.wav'
-
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
+ deathmessage = "blows apart!"
+
loot = list(
/obj/effect/decal/cleanable/robot_debris,
/obj/item/stack/crafting/electronicparts/three
- )
- pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+ )
+
+ waddle_amount = 3
+ waddle_up_time = 2
+ waddle_side_time = 1
+
+ can_ghost_into = TRUE
desc_short = "A snooty robot with a circular saw."
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+ send_mobs = null
+ call_backup = null // Robots don't call for organic backup
+
+ // Z-movement - flies but slow
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 50
+
+ can_open_doors = TRUE
+ can_open_airlocks = TRUE
+
+ // Pure melee - close range saw bot
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/handy/Initialize()
+ . = ..()
+ add_overlay("eyes-[initial(icon_state)]")
+/mob/living/simple_animal/hostile/handy/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+
+// Friendly fire resistance - wastebots are coordinated
+/mob/living/simple_animal/hostile/handy/bullet_act(obj/item/projectile/P)
+ if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/handy))
+ var/original_damage = P.damage
+ P.damage *= 0.2
+ . = ..()
+ P.damage = original_damage
+ return
+ return ..()
+
+// PLAYABLE HANDY
/mob/living/simple_animal/hostile/handy/playable
- mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
- maxHealth = 300
+ maxHealth = 300
health = 300
attack_verb_simple = "shoots a burst of flame at"
- emote_taunt_sound = null
- emote_taunt = null
- aggrosound = null
- idlesound = null
see_in_dark = 8
wander = FALSE
force_threshold = 10
anchored = FALSE
del_on_death = FALSE
dextrous = TRUE
- possible_a_intents = list(INTENT_HELP, INTENT_HARM)
ranged = FALSE
+ emote_taunt_sound = null
+ emote_taunt = null
+ aggrosound = null
+ idlesound = null
+ possible_a_intents = list(INTENT_HELP, INTENT_HARM)
-/mob/living/simple_animal/hostile/handy/Initialize()
- . = ..()
- add_overlay("eyes-[initial(icon_state)]")
-
-/mob/living/simple_animal/hostile/handy/nsb //NSB + Raider Bunker specific
- name = "mr.handy"
+// NSB HANDY - raider bunker variant
+/mob/living/simple_animal/hostile/handy/nsb
+ name = "mr. handy"
aggro_vision_range = 15
faction = list("raider")
obj_damage = 300
+ can_ghost_into = FALSE
+
+///////////////
+// MR. GUTSY //
+///////////////
+// GUTSY - combat variant with plasma + flamer
/mob/living/simple_animal/hostile/handy/gutsy
name = "mr. gutsy"
desc = "A pre-war combat robot based off the Mr. Handy design, armed with plasma weaponry and a deadly close-range flamer."
icon_state = "gutsy"
icon_living = "gutsy"
- icon_dead = "robot_dead"
- can_ghost_into = FALSE
+
mob_armor = ARMOR_VALUE_ROBOT_MILITARY
- maxHealth = 100
+
+ maxHealth = 100
health = 100
stat_attack = UNCONSCIOUS
+
melee_damage_lower = 18
melee_damage_upper = 64
attack_sound = 'sound/items/welder.ogg'
attack_verb_simple = "shoots a burst of flame at"
- projectilesound = 'sound/weapons/laser.ogg'
- projectiletype = /obj/item/projectile/f13plasma/scatter
- extra_projectiles = 1
- ranged = TRUE
- retreat_distance = 4
- minimum_distance = 4
- check_friendly_fire = TRUE
- loot = list(
- /obj/effect/decal/cleanable/robot_debris,
- /obj/item/stack/crafting/electronicparts/three,
- /obj/item/stock_parts/cell/ammo/mfc
- )
- pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
-
+
+ can_ghost_into = FALSE
+
+ emote_taunt = list("raises a flamer")
emote_taunt_sound = list(
'sound/f13npc/gutsy/taunt1.ogg',
'sound/f13npc/gutsy/taunt2.ogg',
'sound/f13npc/gutsy/taunt3.ogg',
'sound/f13npc/gutsy/taunt4.ogg'
- )
- emote_taunt = list("raises a flamer")
-
+ )
aggrosound = list(
'sound/f13npc/gutsy/aggro1.ogg',
'sound/f13npc/gutsy/aggro2.ogg',
@@ -149,12 +195,32 @@
'sound/f13npc/gutsy/aggro4.ogg',
'sound/f13npc/gutsy/aggro5.ogg',
'sound/f13npc/gutsy/aggro6.ogg'
- )
+ )
idlesound = list(
'sound/f13npc/gutsy/idle1.ogg',
'sound/f13npc/gutsy/idle2.ogg',
'sound/f13npc/gutsy/idle3.ogg'
- )
+ )
+
+ loot = list(
+ /obj/effect/decal/cleanable/robot_debris,
+ /obj/item/stack/crafting/electronicparts/three,
+ /obj/item/stock_parts/cell/ammo/mfc
+ )
+
+ desc_short = "A gutsy robot with a plasma gun."
+ pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS
+
+ // Pure ranged - plasma gun
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
+ extra_projectiles = 1
+ projectiletype = /obj/item/projectile/f13plasma/scatter
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(PLASMA_VOLUME),
@@ -165,67 +231,95 @@
SP_DISTANT_SOUND(PLASMA_DISTANT_SOUND),
SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT)
)
- desc_short = "A gutsy robot with a plasma gun."
+// PLAYABLE GUTSY
/mob/living/simple_animal/hostile/handy/gutsy/playable
- mob_armor = ARMOR_VALUE_ROBOT_MILITARY
- maxHealth = 100
- health = 100
speed = 1
attack_verb_simple = "shoots a burst of flame at"
- emote_taunt_sound = null
- emote_taunt = null
- aggrosound = null
- idlesound = null
see_in_dark = 8
- environment_smash = 2 //can break lockers, but not walls
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
wander = FALSE
force_threshold = 10
anchored = FALSE
del_on_death = FALSE
- possible_a_intents = list(INTENT_HELP, INTENT_HARM)
dextrous = TRUE
ranged = FALSE
+ emote_taunt_sound = null
+ emote_taunt = null
+ aggrosound = null
+ idlesound = null
+ possible_a_intents = list(INTENT_HELP, INTENT_HARM)
-/mob/living/simple_animal/hostile/handy/gutsy/nsb //NSB + Raider Bunker specific
+// NSB GUTSY
+/mob/living/simple_animal/hostile/handy/gutsy/nsb
name = "mr. gutsy"
aggro_vision_range = 15
faction = list("raider")
obj_damage = 300
+ can_ghost_into = FALSE
+// MR. BURNSY - flamer variant
+/mob/living/simple_animal/hostile/handy/gutsy/flamer
+ name = "Mr. Burnsy"
+ desc = "A modified mr. gutsy, equipped with a more precise flamer, ditching its plasma weaponry."
+ color = "#B85C00"
+ can_ghost_into = FALSE
+
+ projectiletype = /obj/item/projectile/bullet/incendiary/shotgun
+ projectilesound = 'sound/magic/fireball.ogg'
+ extra_projectiles = 1
+
+///////////////
+// LIBERATOR //
+///////////////
+
+// LIBERATOR - small Chinese PLA drone
/mob/living/simple_animal/hostile/handy/liberator
name = "liberator"
- desc = "A small pre-War droned used by the People's Liberation Army."
+ desc = "A small pre-War drone used by the People's Liberation Army."
icon = 'icons/fallout/mobs/robots/weirdrobots.dmi'
icon_state = "liberator"
icon_living = "leberator"
icon_dead = "liberator_d"
icon_gib = "liberator_g"
+
mob_armor = ARMOR_VALUE_ROBOT_SECURITY
- maxHealth = 50
+
+ maxHealth = 50
health = 50
+
melee_damage_lower = 5
melee_damage_upper = 10
- can_ghost_into = FALSE
attack_verb_simple = "slaps"
- projectilesound = 'sound/weapons/laser.ogg'
- projectiletype = /obj/item/projectile/beam/laser/pistol
- extra_projectiles = 1
- ranged = TRUE
- retreat_distance = 2
- minimum_distance = 2
- check_friendly_fire = TRUE
+
+ can_ghost_into = FALSE
+
+ emote_taunt = list("levels its laser")
+ emote_taunt_sound = null
+ aggrosound = list('sound/f13npc/liberator/chineserobotcarinsurance.ogg')
+ idlesound = null
+ attack_sound = null
+ death_sound = null
+
loot = list(
/obj/effect/decal/cleanable/robot_debris,
/obj/item/stack/crafting/electronicparts/three,
/obj/item/stock_parts/cell/ammo/mfc
- )
- emote_taunt_sound = null
- emote_taunt = list("levels its laser")
- aggrosound = list("sound/f13npc/liberator/chineserobotcarinsurance.ogg")
- idlesound = null
- death_sound = null
- attack_sound = null
+ )
+
+ desc_short = "A robot that shoots lasers."
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Pure ranged - laser pistol
+ combat_mode = COMBAT_MODE_RANGED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
+ extra_projectiles = 1
+ projectiletype = /obj/item/projectile/beam/laser/pistol
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -236,49 +330,62 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
- desc_short = "A robot that shoots lasers."
+// YELLOW LIBERATOR
/mob/living/simple_animal/hostile/handy/liberator/yellow
- name = "liberator"
- desc = "A small pre-War droned used by the People's Liberation Army."
icon_state = "liberator_y"
- can_ghost_into = FALSE
icon_living = "leberator_y"
icon_dead = "liberator_y_d"
+///////////////
+// ROBOBRAIN //
+///////////////
+
+// ROBOBRAIN - cyborg with laser
/mob/living/simple_animal/hostile/handy/robobrain
name = "robobrain"
- desc = "A next-gen cyborg developed by General Atomic International"
- icon = 'icons/fallout/mobs/robots/wasterobots.dmi'
+ desc = "A next-gen cyborg developed by General Atomic International."
icon_state = "robobrain"
icon_living = "robobrain"
icon_dead = "robobrain_d"
+
mob_armor = ARMOR_VALUE_ROBOT_SECURITY
- maxHealth = 110
+
+ maxHealth = 110
health = 110
stat_attack = UNCONSCIOUS
- can_ghost_into = FALSE
+
melee_damage_lower = 15
melee_damage_upper = 37
attack_verb_simple = "slaps"
- projectilesound = 'sound/weapons/laser.ogg'
- projectiletype = /obj/item/projectile/beam/laser
- extra_projectiles = 1
- ranged = TRUE
- retreat_distance = 2
- minimum_distance = 2
- check_friendly_fire = TRUE
- loot = list(
- /obj/effect/decal/cleanable/robot_debris,
- /obj/item/stack/crafting/electronicparts/three,
- /obj/item/stock_parts/cell/ammo/mfc
- )
- emote_taunt_sound = null
+
+ can_ghost_into = FALSE
+
emote_taunt = list("levels its laser")
+ emote_taunt_sound = null
aggrosound = null
idlesound = null
- death_sound = null
attack_sound = null
+ death_sound = null
+
+ loot = list(
+ /obj/effect/decal/cleanable/robot_debris,
+ /obj/item/stack/crafting/electronicparts/three,
+ /obj/item/stock_parts/cell/ammo/mfc
+ )
+
+ desc_short = "A brainy robot with lasers."
+
+ // Mixed combat - has melee but prefers ranged
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
+ extra_projectiles = 1
+ projectiletype = /obj/item/projectile/beam/laser
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -289,20 +396,22 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
- desc_short = "A brainy robot with lasers."
-
-/mob/living/simple_animal/hostile/handy/robobrain/AttackingTarget()
- . = ..()
-/mob/living/simple_animal/hostile/handy/robobrain/nsb //NSB + Raider Bunker specific
+// NSB ROBOBRAIN
+/mob/living/simple_animal/hostile/handy/robobrain/nsb
name = "robobrain"
aggro_vision_range = 15
faction = list("raider")
- can_ghost_into = FALSE
obj_damage = 300
- health = 300
maxHealth = 300
+ health = 300
+ can_ghost_into = FALSE
+/////////////////
+// PROTECTRON //
+/////////////////
+
+// BASE PROTECTRON - slow, laser-armed security bot
/mob/living/simple_animal/hostile/handy/protectron
name = "protectron"
desc = "A pre-war security robot armed with deadly lasers."
@@ -310,86 +419,74 @@
icon_state = "protectron"
icon_living = "protectron"
icon_dead = "protectron_dead"
+
mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN
- maxHealth = 100
+
+ maxHealth = 100
health = 100
- stat_attack = UNCONSCIOUS
- speed = 4
- can_ghost_into = TRUE
- melee_damage_lower = 5 //severely reduced melee damage here because its silly to have a ranged mob also be a cqc master
- melee_damage_upper = 10
- extra_projectiles = 0 //removed extra projectiles to make these easier to deal with on super lowpop
+ speed = 4 // Noticeably slow
+ move_to_delay = 4
stat_attack = CONSCIOUS
- ranged = TRUE
- move_to_delay = 9 //WAY slower than average,
- // m2d 3 = standard, less is fast, more is slower.
-
- retreat_distance = 0 // Mob doesn't retreat
- //how far they pull back
- minimum_distance = 1
- // how close you can get before they try to pull back
-
+ melee_damage_lower = 5 // Weak melee - it's a ranged bot
+ melee_damage_upper = 10
+
aggro_vision_range = 7
- //tiles within they start attacking, doesn't count the mobs tile
-
vision_range = 8
- //tiles within they start making noise, does count the mobs tile
-
- attack_verb_simple = list(
- "baps",
- "bops",
- "boops",
- "smacks",
- "clamps",
- "pinches",
- "thumps",
- "fistos"
- )
+
+ attack_verb_simple = list("baps", "bops", "boops", "smacks", "clamps", "pinches", "thumps", "fistos")
attack_sound = 'sound/weapons/punch1.ogg'
- projectilesound = 'sound/weapons/laser.ogg'
- projectiletype = /obj/item/projectile/beam/laser/pistol
- faction = list("wastebot")
- check_friendly_fire = TRUE
- loot = list(
- /obj/effect/decal/cleanable/robot_debris,
- /obj/item/stack/crafting/electronicparts/five
- )
- attack_phrase = list(
- "Howdy pardner!",
- "Shoot out at the O.K. Corral!",
- "Go back to Oklahoma!",
- "Please assume the position.",
- "Protect and serve.",
- "Antisocial behavior detected.",
- "Criminal behavior willbe punished.",
- "Please step into the open and identify yourself, law abiding citizens have nothing to fear."
- )
+
+ emote_taunt = list("raises its arm laser", "gets ready to rumble", "assumes the position", "whirls up its servos", "takes aim", "holds its ground")
emote_taunt_sound = list(
'sound/f13npc/protectron/taunt1.ogg',
'sound/f13npc/protectron/taunt2.ogg',
'sound/f13npc/protectron/taunt3.ogg'
- )
- emote_taunt = list(
- "raises its arm laser",
- "gets ready to rumble",
- "assumes the position",
- "whirls up its servos",
- "takes aim",
- "holds its ground"
- )
+ )
+ taunt_chance = 30
aggrosound = list(
'sound/f13npc/protectron/aggro1.ogg',
'sound/f13npc/protectron/aggro2.ogg',
'sound/f13npc/protectron/aggro3.ogg',
'sound/f13npc/protectron/aggro4.ogg'
- )
+ )
idlesound = list(
'sound/f13npc/protectron/idle1.ogg',
'sound/f13npc/protectron/idle2.ogg',
'sound/f13npc/protectron/idle3.ogg',
- 'sound/f13npc/protectron/idle4.ogg',
- )
+ 'sound/f13npc/protectron/idle4.ogg'
+ )
+
+ attack_phrase = list(
+ "Howdy pardner!",
+ "Shoot out at the O.K. Corral!",
+ "Go back to Oklahoma!",
+ "Please assume the position.",
+ "Protect and serve.",
+ "Antisocial behavior detected.",
+ "Criminal behavior will be punished.",
+ "Please step into the open and identify yourself, law abiding citizens have nothing to fear."
+ )
+
+ loot = list(
+ /obj/effect/decal/cleanable/robot_debris,
+ /obj/item/stack/crafting/electronicparts/five
+ )
+
+ can_ghost_into = TRUE
+ desc_short = "A clunky hunk of junk with a laser."
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Mixed combat - laser + melee when cornered. Slow so retreating is pointless.
+ combat_mode = COMBAT_MODE_MIXED
+ ranged = TRUE
+ retreat_distance = 4
+ minimum_distance = 1
+
+ ranged_cooldown_time = 2 SECONDS
+ extra_projectiles = 0 // One shot at a time on lowpop
+ projectiletype = /obj/item/projectile/beam/laser/pistol
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -400,70 +497,57 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
- pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
- desc_short = "A clunky hunk of junk with a laser."
+// PLAYABLE PROTECTRON
/mob/living/simple_animal/hostile/handy/protectron/playable
- ranged = FALSE
melee_damage_lower = 25
melee_damage_upper = 38
- health = 100
- maxHealth = 100
speed = 2
attack_verb_simple = "clamps"
- emote_taunt_sound = null
- emote_taunt = null
- aggrosound = null
- idlesound = null
see_in_dark = 8
- environment_smash = 1 //can break lockers, but not walls
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
wander = FALSE
force_threshold = 10
anchored = FALSE
del_on_death = FALSE
+ ranged = FALSE
+ emote_taunt_sound = null
+ emote_taunt = null
+ aggrosound = null
+ idlesound = null
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
-/mob/living/simple_animal/hostile/handy/protectron/nsb //NSB + Raider Bunker specific
+// NSB PROTECTRON
+/mob/living/simple_animal/hostile/handy/protectron/nsb
name = "protectron"
aggro_vision_range = 15
- can_ghost_into = FALSE
faction = list("raider")
obj_damage = 300
+ can_ghost_into = FALSE
-/mob/living/simple_animal/pet/dog/protectron //Not an actual dog
+// TRADING PROTECTRON - pet/friendly variant
+/mob/living/simple_animal/pet/dog/protectron
name = "Trading Protectron"
desc = "A standard RobCo RX2 V1.16.4 \"Trade-o-Vend\", loaded with Trade protocols.
Looks like it was kept operational for an indefinite period of time. Its body is covered in cracks and dents of various sizes.
As it has been repaired countless times, it's amazing the machine is still functioning at all."
icon = 'icons/fallout/mobs/robots/protectrons.dmi'
icon_state = "protectron_trade"
icon_living = "protectron_trade"
icon_dead = "protectron_trade_dead"
+
+ mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
+
maxHealth = 200
health = 200
- can_ghost_into = FALSE
speak_chance = 5
- mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
+ can_ghost_into = FALSE
+ blood_volume = 0
+
faction = list(
- "neutral",
- "silicon",
- "dog",
- "hostile",
- "pirate",
- "wastebot",
- "wolf",
- "plants",
- "turret",
- "enclave",
- "ghoul",
- "cazador",
- "supermutant",
- "gecko",
- "slime",
- "radscorpion",
- "skeleton",
- "carp",
- "bs",
- "bighorner"
- )
+ "neutral", "silicon", "dog", "hostile", "pirate", "wastebot", "wolf",
+ "plants", "turret", "enclave", "ghoul", "cazador", "supermutant",
+ "gecko", "slime", "radscorpion", "skeleton", "carp", "bs", "bighorner"
+ )
+
speak = list(
"Howdy partner! How about you spend some of them there hard earned caps on some of this fine merchandise.",
"Welcome back partner! Hoo-wee it's a good day to buy some personal protection!",
@@ -471,104 +555,133 @@
"What a fine day partner. A fine day indeed.",
"Reminds me of what my grandpappy used to say, make a snap decision now and never question it. You look like you could use some product there partner.",
"Lotta critters out there want to chew you up partner, you could use a little hand with that now couldn't you?"
- )
+ )
+
speak_emote = list()
emote_hear = list()
emote_see = list()
- response_help_simple = "shakes its manipulator"
+
+ response_help_simple = "shakes its manipulator"
response_disarm_simple = "pushes"
- response_harm_simple = "punches"
+ response_harm_simple = "punches"
attack_sound = 'sound/voice/liveagain.ogg'
+
butcher_results = list(/obj/effect/gibspawner/robot = 1)
- blood_volume = 0
+/////////////////
+// ASSAULTRON //
+/////////////////
+
+// BASE ASSAULTRON - fast melee combat robot
/mob/living/simple_animal/hostile/handy/assaultron
name = "assaultron"
desc = "A deadly close combat robot developed by RobCo in a vaguely feminine, yet ominous chassis."
icon_state = "assaultron"
icon_living = "assaultron"
- gender = FEMALE //Pffffffffffffffffffffff
icon_dead = "gib7"
+
+ mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
mob_armor = ARMOR_VALUE_ROBOT_MILITARY
- maxHealth = 100
+
+ maxHealth = 100
health = 100
+ speed = 1 // Fast for a robot
stat_attack = UNCONSCIOUS
- can_ghost_into = FALSE
- mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC
- speed = 1
+ gender = FEMALE
+
melee_damage_lower = 18
melee_damage_upper = 45
- environment_smash = 2 //can smash walls
attack_verb_simple = "grinds their claws on"
- faction = list("wastebot")
+
+ environment_smash = ENVIRONMENT_SMASH_STRUCTURES
+
+ can_ghost_into = FALSE
+ desc_short = "A deadly robot."
+
+ emote_taunt = null
+ emote_taunt_sound = null
+ aggrosound = null
+ idlesound = null
+
loot = list(
/obj/effect/decal/cleanable/robot_debris,
/obj/item/stack/crafting/electronicparts/three,
/obj/item/stock_parts/cell/ammo/mfc
- )
-
- emote_taunt_sound = FALSE
- emote_taunt = FALSE
-
- aggrosound = FALSE
- idlesound = FALSE
- desc_short = "A sexy robot."
+ )
+
+ // Z-movement - can climb
+ can_z_move = TRUE
+ can_climb_ladders = TRUE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 30
+
+ // Pure melee - fast close combat
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
-/mob/living/simple_animal/hostile/handy/assaultron/nsb //NSB + Raider Bunker specific.
+// NSB ASSAULTRON
+/mob/living/simple_animal/hostile/handy/assaultron/nsb
name = "assaultron"
aggro_vision_range = 15
faction = list("raider")
obj_damage = 300
can_ghost_into = FALSE
-/mob/living/simple_animal/hostile/handy/assaultron/martha //Tipton specific
+// MARTHA - unique named assaultron
+/mob/living/simple_animal/hostile/handy/assaultron/martha
name = "lil martha"
- desc = "A deadly close combat robot developed by RobCo and covered in thick, dark blue armor plating, the name lil martha scratched onto it"
+ desc = "A deadly close combat robot developed by RobCo and covered in thick, dark blue armor plating, the name 'lil martha' scratched onto it."
+
+ mob_armor = ARMOR_VALUE_ROBOT_MILITARY_HEAVY
+
maxHealth = 500
health = 500
aggro_vision_range = 15
- faction = list("hostile")
obj_damage = 300
+
+ faction = list("hostile")
can_ghost_into = FALSE
- color = "#3444C8" //dark blue
- emp_flags = list() //no emp instakill for you
+ color = "#3444C8" // Dark blue
+
+ emp_flags = list() // No EMP instakill
+// PLAYABLE ASSAULTRON
/mob/living/simple_animal/hostile/handy/assaultron/playable
see_in_dark = 8
force_threshold = 15
- wander = 0
+ wander = FALSE
anchored = FALSE
del_on_death = FALSE
- possible_a_intents = list(INTENT_HELP, INTENT_HARM, INTENT_GRAB, INTENT_DISARM)
dextrous = TRUE
- deathmessage = "abruptly shuts down, falling to the ground!"
can_ghost_into = FALSE
+ deathmessage = "abruptly shuts down, falling to the ground!"
+ possible_a_intents = list(INTENT_HELP, INTENT_HARM, INTENT_GRAB, INTENT_DISARM)
+// SA-S-E - medical assaultron
/mob/living/simple_animal/hostile/handy/assaultron/playable/medical
name = "SA-S-E"
- desc = "An Assaultron modified for the Medical field, SA-S-E forgoes the weaponry and deadliness of her military countarparts to save lives. Painted white with blue highlights, and a blue cross on the front of her visor, this robot comes equipped with what looks like modified medical gear. Her head has no eye-laser, instead a gently pulsing blue eye that scans people the analyze their health, a defibrilator on her back, and articulated hands to be able to use the myriad medical tools strapped to parts of her body under protective cases all show this model is meant to save lives. She's stockier than other Assaultrons due to all the added gear, and her legs seem much thicker than normal due to reinforced servos and gears."
+ desc = "An Assaultron modified for the Medical field, SA-S-E forgoes the weaponry and deadliness of her military counterparts to save lives. Painted white with blue highlights, and a blue cross on the front of her visor, this robot comes equipped with what looks like modified medical gear. Her head has no eye-laser, instead a gently pulsing blue eye that scans people to analyze their health, a defibrillator on her back, and articulated hands to be able to use the myriad medical tools strapped to parts of her body under protective cases all show this model is meant to save lives."
icon_state = "assaultron_sase"
icon_dead = "assaultron_sase_dead"
-//Junkers
-/mob/living/simple_animal/hostile/handy/gutsy/flamer
- name = "Mr. Burnsy"
- desc = "A modified mr. gutsy, equipped with a more precise flamer, ditching it's plasma weaponry."
- color = "#B85C00"
- projectilesound = 'sound/magic/fireball.ogg'
- projectiletype = /obj/item/projectile/bullet/incendiary/shotgun
- extra_projectiles = 1
- can_ghost_into = FALSE
-
+// RED EYE ASSAULTRON - laser eye variant
/mob/living/simple_animal/hostile/handy/assaultron/laser
name = "red eye assaultron"
- desc = "A modified assaultron. It's eye has been outfitted with a deadly laser."
+ desc = "A modified assaultron. Its eye has been outfitted with a deadly laser."
color = "#B85C00"
+ can_ghost_into = FALSE
+
+ // Mixed - laser at range, claws up close
+ combat_mode = COMBAT_MODE_MIXED
ranged = TRUE
- retreat_distance = null
+ retreat_distance = 4
minimum_distance = 1
- projectilesound = 'sound/weapons/laser.ogg'
+
+ ranged_cooldown_time = 3 SECONDS
projectiletype = /obj/item/projectile/beam/laser/lasgun
+ projectilesound = 'sound/weapons/laser.ogg'
projectile_sound_properties = list(
SP_VARY(FALSE),
SP_VOLUME(LASER_VOLUME),
@@ -579,5 +692,3 @@
SP_DISTANT_SOUND(LASER_DISTANT_SOUND),
SP_DISTANT_RANGE(LASER_RANGE_DISTANT)
)
- can_ghost_into = FALSE
-
diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm b/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm
index 9aeb9ccaafa..1629403aefe 100644
--- a/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm
+++ b/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm
@@ -1,6 +1,10 @@
-//Fallout 13 canine directory
+// In this document: Feral dogs, Alpha dogs, Wolves, Unique named dogs
-// Feral dog - visually some sort of mutt, at some point a coyote style dog can be made from the previous dog sprite, saved as coyote
+///////////////
+// FERAL DOG //
+///////////////
+
+// BASE FERAL DOG
/mob/living/simple_animal/hostile/wolf
name = "feral dog"
desc = "The dogs that survived the Great War are a larger, and tougher breed, size of a wolf.
This one seems to be severely malnourished and its eyes are bloody red."
@@ -9,89 +13,183 @@
icon_living = "dog_feral"
icon_dead = "dog_feral_dead"
icon_gib = "gib"
+
mob_biotypes = MOB_ORGANIC|MOB_BEAST
- turns_per_move = 1
- response_help_simple = "pets"
- response_disarm_simple = "pushes aside"
- response_harm_simple = "kicks"
+
maxHealth = 50
health = 50
- faction = list("hostile", "wolf")
- environment_smash = 0
- guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 1, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 1,/obj/item/stack/sheet/bone = 1)
+ move_to_delay = 2.5
+ turns_per_move = 1
+
melee_damage_lower = 8
melee_damage_upper = 16
+
aggro_vision_range = 15
+
+ // Canine predators with natural night vision
+ has_low_light_vision = TRUE
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/animalhide/wolf = 1,
+ /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 1,
+ /obj/item/stack/sheet/bone = 1
+ )
+
waddle_amount = 2
- waddle_up_time = 0 //Dogs can't look up ~TK
+ waddle_up_time = 0 // Dogs can't look up
waddle_side_time = 1
-// idle_vision_range = 7
+ footstep_type = FOOTSTEP_MOB_BAREFOOT
+
+ response_help_simple = "pets"
+ response_disarm_simple = "pushes aside"
+ response_harm_simple = "kicks"
attack_verb_simple = "bites"
attack_sound = 'sound/weapons/bite.ogg'
- move_to_delay = 2
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-
- emote_taunt_sound = list('sound/f13npc/dog/dog_charge1.ogg', 'sound/f13npc/dog/dog_charge2.ogg', 'sound/f13npc/dog/dog_charge3.ogg', 'sound/f13npc/dog/dog_charge4.ogg', 'sound/f13npc/dog/dog_charge5.ogg', 'sound/f13npc/dog/dog_charge6.ogg', 'sound/f13npc/dog/dog_charge7.ogg',)
+
emote_taunt = list("growls", "barks", "snarls")
+ emote_taunt_sound = list(
+ 'sound/f13npc/dog/dog_charge1.ogg',
+ 'sound/f13npc/dog/dog_charge2.ogg',
+ 'sound/f13npc/dog/dog_charge3.ogg',
+ 'sound/f13npc/dog/dog_charge4.ogg',
+ 'sound/f13npc/dog/dog_charge5.ogg',
+ 'sound/f13npc/dog/dog_charge6.ogg',
+ 'sound/f13npc/dog/dog_charge7.ogg'
+ )
taunt_chance = 30
- aggrosound = list('sound/f13npc/dog/dog_alert1.ogg', 'sound/f13npc/dog/dog_alert2.ogg', 'sound/f13npc/dog/dog_alert3.ogg')
- idlesound = list('sound/f13npc/dog/dog_bark1.ogg', 'sound/f13npc/dog/dog_bark2.ogg', 'sound/f13npc/dog/dog_bark3.ogg')
+ aggrosound = list(
+ 'sound/f13npc/dog/dog_alert1.ogg',
+ 'sound/f13npc/dog/dog_alert2.ogg',
+ 'sound/f13npc/dog/dog_alert3.ogg'
+ )
+ idlesound = list(
+ 'sound/f13npc/dog/dog_bark1.ogg',
+ 'sound/f13npc/dog/dog_bark2.ogg',
+ 'sound/f13npc/dog/dog_bark3.ogg'
+ )
death_sound = 'sound/f13npc/centaur/centaur_death.ogg'
+
+ faction = list("hostile", "wolf")
+ a_intent = INTENT_HARM
+ gold_core_spawnable = HOSTILE_SPAWN
+
+ can_ghost_into = TRUE
+ pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS
+
+ // Z-movement
+ can_z_move = TRUE
+ can_climb_ladders = FALSE
+ can_climb_stairs = TRUE
+ can_jump_down = TRUE
+ z_move_delay = 20 // Fast runner
+
+ can_open_doors = FALSE
+ can_open_airlocks = FALSE
+
+ // Pure melee - rush and bite
+ combat_mode = COMBAT_MODE_MELEE
+ retreat_distance = null
+ minimum_distance = 1
+
+/mob/living/simple_animal/hostile/wolf/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(8)
+
+/mob/living/simple_animal/hostile/wolf/CanAttack(atom/the_target)
+ if(isliving(the_target))
+ var/mob/living/L = the_target
+ if(L.stat >= UNCONSCIOUS)
+ return FALSE
+ return ..()
+// PLAYABLE FERAL DOG
/mob/living/simple_animal/hostile/wolf/playable
- health = 150
maxHealth = 150
+ health = 150
+ see_in_dark = 8
+ wander = FALSE
+ anchored = FALSE
+ del_on_death = FALSE
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
- see_in_dark = 8
- wander = 0
- anchored = FALSE
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-// Alpha dog
+// ALPHA DOG - pack leader
/mob/living/simple_animal/hostile/wolf/alpha
name = "alpha feral dog"
desc = "The dogs that survived the Great War are a larger, and tougher breed, size of a wolf.
Wait... This one's a wolf!"
icon_state = "dog_alpha"
icon_living = "dog_alpha"
icon_dead = "dog_alpha_dead"
- guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 2, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,/obj/item/stack/sheet/bone = 2)
+
maxHealth = 70
health = 70
+
melee_damage_lower = 12
melee_damage_upper = 28
- footstep_type = FOOTSTEP_MOB_BAREFOOT
+
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/animalhide/wolf = 2,
+ /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,
+ /obj/item/stack/sheet/bone = 2
+ )
+
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+/mob/living/simple_animal/hostile/wolf/alpha/Aggro()
+ . = ..()
+ if(.)
+ return
+ summon_backup(12) // Alpha calls more backup
+
+// PLAYABLE ALPHA DOG
/mob/living/simple_animal/hostile/wolf/alpha/playable
- health = 70
- maxHealth = 70
+ see_in_dark = 8
+ wander = FALSE
+ anchored = FALSE
+ del_on_death = FALSE
emote_taunt_sound = null
emote_taunt = null
aggrosound = null
idlesound = null
- see_in_dark = 8
- wander = 0
- anchored = FALSE
possible_a_intents = list(INTENT_HELP, INTENT_HARM)
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-// The first proper wolf, got to love just relabels without repathing.
+
+//////////
+// WOLF //
+//////////
+
+// WOLF - proper wolf, tougher than a feral dog
/mob/living/simple_animal/hostile/wolf/cold
name = "wolf"
desc = "A mangy wolf."
icon_state = "wolf"
icon_living = "wolf"
icon_dead = "wolf_dead"
- guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 2, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,/obj/item/stack/sheet/bone = 2)
+
maxHealth = 100
health = 100
+
melee_damage_lower = 20
melee_damage_upper = 28
- footstep_type = FOOTSTEP_MOB_BAREFOOT
-//Unique Dogs - Guerilla for Khans is a Rottweiler, Brutus and Lupa german shepherds, Sniffs-the-Earth a sheepdog.
-//Feel free to move or add code for different behaviours like sleep, some unused sprites prepped for that sort of thing.
+ guaranteed_butcher_results = list(
+ /obj/item/stack/sheet/animalhide/wolf = 2,
+ /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,
+ /obj/item/stack/sheet/bone = 2
+ )
+
+ pop_required_to_jump_into = MED_MOB_MIN_PLAYERS
+
+/////////////////
+// UNIQUE DOGS //
+/////////////////
+
+// Unique named dogs - playable characters.
+// Guerilla (Khan) = Rottweiler, Brutus/Lupa = German Shepherds, Sniffs-the-Earth = Sheepdog.
/mob/living/simple_animal/hostile/wolf/playable/rottweiler
icon_state = "rottweiler"
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 36de5a1238f..641c75edcd5 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -15,7 +15,7 @@
if(L.reagents)
L.reagents.add_reagent(poison_type, poison_per_bite)
-//basic spider mob, these generally guard nests
+// Basic spider mob, these generally guard nests
/mob/living/simple_animal/hostile/poison/giant_spider
name = "radspider"
desc = "Furry and black, it makes you shudder to look at it. This one has deep red eyes."
@@ -52,10 +52,10 @@
see_in_dark = 4
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE
footstep_type = FOOTSTEP_MOB_CLAW
- has_field_of_vision = FALSE // 360° vision.
+ has_field_of_vision = FALSE // 360° vision
var/playable_spider = FALSE
var/datum/action/innate/spider/lay_web/lay_web
- var/directive = "" //Message passed down to children, to relay the creator's orders
+ var/directive = "" // Message passed down to children to relay the creator's orders
/mob/living/simple_animal/hostile/poison/giant_spider/Initialize()
. = ..()
@@ -63,22 +63,20 @@
lay_web.Grant(src)
/mob/living/simple_animal/hostile/poison/giant_spider/Destroy()
- // Remove from global list FIRST
GLOB.spidermobs -= src
- // Clear ranged ability reference without calling remove (which may try to access src)
+ // Clear ranged ability reference
if(ranged_ability)
if(ranged_ability.ranged_ability_user == src)
ranged_ability.ranged_ability_user = null
ranged_ability = null
- // Clear ALL abilities with proper cleanup
+ // Qdel all ability objects with proper reference cleanup
if(LAZYLEN(abilities))
var/list/abilities_copy = abilities.Copy()
- abilities = null // Clear the list first to break references
+ abilities = null
for(var/obj/effect/proc_holder/ability in abilities_copy)
- // Clear cross-references
if(ability.ranged_ability_user == src)
ability.ranged_ability_user = null
if(ability.action)
@@ -86,16 +84,14 @@
ability.action.owner = null
qdel(ability)
- // Clear the lay_web action
+ // Clear lay_web action (datum/action/innate - not in abilities list)
if(lay_web)
lay_web.Remove(src)
if(lay_web.owner == src)
lay_web.owner = null
QDEL_NULL(lay_web)
- // Clear directive
directive = null
-
return ..()
/mob/living/simple_animal/hostile/poison/giant_spider/Topic(href, href_list)
@@ -105,7 +101,8 @@
humanize_spider(ghost)
/mob/living/simple_animal/hostile/poison/giant_spider/Login()
- ..()
+ . = ..()
+ GLOB.spidermobs[src] = TRUE
if(directive)
to_chat(src, span_notice("Your mother left you a directive! Follow it at all costs."))
to_chat(src, "[directive]")
@@ -117,7 +114,7 @@
humanize_spider(user)
/mob/living/simple_animal/hostile/poison/giant_spider/proc/humanize_spider(mob/user)
- if(key || !playable_spider || stat)//Someone is in it, it's dead, or the fun police are shutting it down
+ if(key || !playable_spider || stat)
return FALSE
if(isobserver(user))
var/mob/dead/observer/O = user
@@ -132,7 +129,27 @@
user.transfer_ckey(src, FALSE)
return TRUE
-//nursemaids - these create webs and eggs
+/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_action()
+ if(!..()) // AIStatus is off
+ return 0
+ if(AIStatus == AI_IDLE)
+ // 1% chance to skitter madly - only when not busy doing something
+ if(!busy && prob(1))
+ stop_automated_movement = 1
+ Goto(pick(urange(20, src, 1)), move_to_delay)
+ addtimer(CALLBACK(src, PROC_REF(stop_skitter)), 5 SECONDS, TIMER_DELETE_ME)
+ return 1
+
+/mob/living/simple_animal/hostile/poison/giant_spider/proc/stop_skitter()
+ if(!busy) // Don't interrupt if we've started doing something
+ stop_automated_movement = 0
+ walk(src, 0)
+
+// ============================================================
+// NURSE SPIDER
+// Nursemaids create webs, cocoon prey, and lay eggs
+// ============================================================
+
/mob/living/simple_animal/hostile/poison/giant_spider/nurse
desc = "Furry and black, it makes you shudder to look at it. This one has brilliant green eyes."
icon_state = "nurse"
@@ -150,7 +167,7 @@
var/obj/effect/proc_holder/wrap/wrap
var/datum/action/innate/spider/lay_eggs/lay_eggs
var/datum/action/innate/spider/set_directive/set_directive
- var/static/list/consumed_mobs = list() //the tags of mobs that have been consumed by nurse spiders to lay eggs
+ var/static/list/consumed_mobs = list() // Tags of mobs consumed to prevent re-feeding
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Initialize()
. = ..()
@@ -161,16 +178,15 @@
set_directive = new
set_directive.Grant(src)
-
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/Destroy()
- // Clear cocoon target reference
cocoon_target = null
- // CRITICAL FIX: Do NOT qdel the wrap here - let parent handle it
- // Just clear the reference to break the cycle
+ // wrap is in the abilities list - parent giant_spider/Destroy() will qdel it
+ // Just clear our reference so we don't double-free
wrap = null
- // Clear actions with proper cleanup
+ // lay_eggs and set_directive are datum/action/innate - NOT in abilities list
+ // Must be cleaned up explicitly here
if(lay_eggs)
lay_eggs.Remove(src)
if(lay_eggs.owner == src)
@@ -185,7 +201,148 @@
return ..()
-//hunters have the most poison and move the fastest, so they can find prey
+/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/GiveUp(atom/movable/C)
+ addtimer(CALLBACK(src, PROC_REF(do_give_up), C), 10 SECONDS, TIMER_DELETE_ME)
+
+/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/do_give_up(atom/movable/C)
+ if(busy == MOVING_TO_TARGET && cocoon_target == C && get_dist(src, cocoon_target) > 1)
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = 0
+
+/mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_action()
+ if(!..())
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return 0
+
+ if(busy == MOVING_TO_TARGET && cocoon_target)
+ if(QDELETED(cocoon_target))
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ else if(get_dist(src, cocoon_target) <= 1)
+ cocoon()
+ return 1
+
+ if(!busy && prob(30))
+ var/list/nearby = view(src, 10) // Renamed from can_see to avoid shadowing proc
+
+ // Priority 1: Cocoon an incapacitated living mob
+ for(var/mob/living/C in nearby)
+ if(C.stat && !istype(C, /mob/living/simple_animal/hostile/poison/giant_spider) && !C.anchored)
+ cocoon_target = C
+ busy = MOVING_TO_TARGET
+ Goto(C, move_to_delay)
+ GiveUp(C)
+ return 1
+
+ // Priority 2: Spin a web on this tile if there isn't one
+ var/obj/structure/spider/stickyweb/W = locate() in get_turf(src)
+ if(!W)
+ lay_web.Activate()
+ return 1
+
+ // Priority 3: Lay eggs if we're fed
+ if(fed)
+ lay_eggs.Activate()
+ return 1
+
+ // Priority 4: Cocoon nearby items
+ for(var/obj/O in nearby)
+ if(O.anchored)
+ continue
+ if(isitem(O) || isstructure(O) || ismachinery(O))
+ cocoon_target = O
+ busy = MOVING_TO_TARGET
+ stop_automated_movement = 1
+ Goto(O, move_to_delay)
+ GiveUp(O)
+ return 1 // BUG FIX: break after first target, don't overwrite with last obj in loop
+
+ return 1
+
+/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/cocoon()
+ if(stat == DEAD)
+ return
+
+ if(!cocoon_target || QDELETED(cocoon_target))
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return
+
+ if(cocoon_target.anchored)
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return
+
+ if(cocoon_target == src)
+ to_chat(src, span_warning("You can't wrap yourself!"))
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return
+
+ if(istype(cocoon_target, /mob/living/simple_animal/hostile/poison/giant_spider))
+ to_chat(src, span_warning("You can't wrap other spiders!"))
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return
+
+ if(!Adjacent(cocoon_target))
+ to_chat(src, span_warning("You can't reach [cocoon_target]!"))
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+ return
+
+ if(busy == SPINNING_COCOON)
+ return // Already spinning, don't restart
+
+ busy = SPINNING_COCOON
+ visible_message(
+ span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."),
+ span_notice("You begin wrapping [cocoon_target] into a cocoon.")
+ )
+ stop_automated_movement = TRUE
+ walk(src, 0)
+
+ if(do_after(src, 50, target = cocoon_target))
+ // SAFETY: re-check target validity after the do_after sleep
+ if(busy == SPINNING_COCOON && cocoon_target && !QDELETED(cocoon_target) && !cocoon_target.anchored)
+ var/obj/structure/spider/cocoon/C = new(cocoon_target.loc)
+
+ if(isliving(cocoon_target))
+ var/mob/living/L = cocoon_target
+ if(L.blood_volume && (L.stat != DEAD || !consumed_mobs[L.tag]))
+ consumed_mobs[L.tag] = TRUE
+ fed++
+ lay_eggs.UpdateButtonIcon(TRUE)
+ visible_message(
+ span_danger("[src] sticks a proboscis into [L] and sucks a viscous substance out."),
+ span_notice("You suck the nutriment out of [L], feeding you enough to lay a cluster of eggs.")
+ )
+ L.death()
+ else
+ to_chat(src, span_warning("[L] cannot sate your hunger!"))
+
+ cocoon_target.forceMove(C)
+
+ if(cocoon_target.density || ismob(cocoon_target))
+ C.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3")
+
+ cocoon_target = null
+ busy = SPIDER_IDLE
+ stop_automated_movement = FALSE
+
+// ============================================================
+// HUNTER SPIDER
+// Fastest, most venomous - hunts prey for the nest
+// ============================================================
+
/mob/living/simple_animal/hostile/poison/giant_spider/hunter
desc = "Furry and black, it makes you shudder to look at it. This one has sparkling purple eyes."
icon_state = "hunter"
@@ -198,7 +355,7 @@
poison_per_bite = 5
move_to_delay = 5
-//vipers are the rare variant of the hunter, no IMMEDIATE damage but so much poison medical care will be needed fast.
+// Viper: glass cannon variant - almost no physical damage, lethal poison
/mob/living/simple_animal/hostile/poison/giant_spider/hunter/viper
name = "radspider viper"
desc = "Furry and black, it makes you shudder to look at it. This one has effervescent purple eyes."
@@ -211,18 +368,22 @@
melee_damage_upper = 1
poison_per_bite = 12
move_to_delay = 4
- poison_type = /datum/reagent/toxin/venom //all in venom, glass cannon. you bite 5 times and they are DEFINITELY dead, but 40 health and you are extremely obvious. Ambush, maybe?
+ poison_type = /datum/reagent/toxin/venom
speed = 1
gold_core_spawnable = NO_SPAWN
-//tarantulas are really tanky, regenerating (maybe), hulky monster but are also extremely slow, so.
+// ============================================================
+// TARANTULA
+// Extremely tanky, extremely slow
+// ============================================================
+
/mob/living/simple_animal/hostile/poison/giant_spider/tarantula
name = "radtarantula"
desc = "Furry and black, it makes you shudder to look at it. This one has abyssal red eyes."
icon_state = "tarantula"
icon_living = "tarantula"
icon_dead = "tarantula_dead"
- maxHealth = 300 // woah nelly
+ maxHealth = 300
health = 300
melee_damage_lower = 35
melee_damage_upper = 40
@@ -236,15 +397,19 @@
/mob/living/simple_animal/hostile/poison/giant_spider/tarantula/Moved(atom/oldloc, dir)
. = ..()
- if(slowed_by_webs)
- if(!(locate(/obj/structure/spider/stickyweb) in loc))
- remove_movespeed_modifier(/datum/movespeed_modifier/tarantula_web)
- slowed_by_webs = FALSE
- else if(locate(/obj/structure/spider/stickyweb) in loc)
+ var/on_web = !!(locate(/obj/structure/spider/stickyweb) in loc)
+ if(slowed_by_webs && !on_web)
+ remove_movespeed_modifier(/datum/movespeed_modifier/tarantula_web)
+ slowed_by_webs = FALSE
+ else if(!slowed_by_webs && on_web)
add_movespeed_modifier(/datum/movespeed_modifier/tarantula_web)
slowed_by_webs = TRUE
-//midwives are the queen of the spiders, can send messages to all them and web faster. That rare round where you get a queen spider and turn your 'for honor' players into 'r6siege' players will be a fun one.
+// ============================================================
+// MIDWIFE (QUEEN)
+// Alpha of the spider colony - communicates with all spiders
+// ============================================================
+
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife
name = "radspider midwife"
desc = "Furry and black, it makes you shudder to look at it. This one has scintillating green eyes."
@@ -263,22 +428,25 @@
letmetalkpls.Grant(src)
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife/Destroy()
- // Clear communication action
if(letmetalkpls)
letmetalkpls.Remove(src)
if(letmetalkpls.owner == src)
letmetalkpls.owner = null
QDEL_NULL(letmetalkpls)
-
return ..()
-/mob/living/simple_animal/hostile/poison/giant_spider/ice //spiders dont usually like tempatures of 140 kelvin who knew
+// ============================================================
+// ICE VARIANTS
+// Survive in vacuum/extreme cold, inflict frostoil poison
+// ============================================================
+
+/mob/living/simple_animal/hostile/poison/giant_spider/ice
name = "giant ice radspider"
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)
minbodytemp = 0
maxbodytemp = 1500
poison_type = /datum/reagent/consumable/frostoil
- color = rgb(114,228,250)
+ color = rgb(114, 228, 250)
gold_core_spawnable = NO_SPAWN
/mob/living/simple_animal/hostile/poison/giant_spider/nurse/ice
@@ -287,7 +455,7 @@
minbodytemp = 0
maxbodytemp = 1500
poison_type = /datum/reagent/consumable/frostoil
- color = rgb(114,228,250)
+ color = rgb(114, 228, 250)
gold_core_spawnable = NO_SPAWN
/mob/living/simple_animal/hostile/poison/giant_spider/hunter/ice
@@ -296,118 +464,18 @@
minbodytemp = 0
maxbodytemp = 1500
poison_type = /datum/reagent/consumable/frostoil
- color = rgb(114,228,250)
+ color = rgb(114, 228, 250)
gold_core_spawnable = NO_SPAWN
-/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_action()
- if(!..()) //AIStatus is off
- return 0
- if(AIStatus == AI_IDLE)
- //1% chance to skitter madly away
- if(!busy && prob(1))
- stop_automated_movement = 1
- Goto(pick(urange(20, src, 1)), move_to_delay)
- spawn(50)
- stop_automated_movement = 0
- walk(src,0)
- return 1
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/GiveUp(C)
- spawn(100)
- if(busy == MOVING_TO_TARGET)
- if(cocoon_target == C && get_dist(src,cocoon_target) > 1)
- cocoon_target = null
- busy = FALSE
- stop_automated_movement = 0
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_action()
- if(..())
- var/list/can_see = view(src, 10)
- if(!busy && prob(30)) //30% chance to stop wandering and do something
- //first, check for potential food nearby to cocoon
- for(var/mob/living/C in can_see)
- if(C.stat && !istype(C, /mob/living/simple_animal/hostile/poison/giant_spider) && !C.anchored)
- cocoon_target = C
- busy = MOVING_TO_TARGET
- Goto(C, move_to_delay)
- //give up if we can't reach them after 10 seconds
- GiveUp(C)
- return
-
- //second, spin a sticky spiderweb on this tile
- var/obj/structure/spider/stickyweb/W = locate() in get_turf(src)
- if(!W)
- lay_web.Activate()
- else
- //third, lay an egg cluster there
- if(fed)
- lay_eggs.Activate()
- else
- //fourthly, cocoon any nearby items so those pesky pinkskins can't use them
- for(var/obj/O in can_see)
-
- if(O.anchored)
- continue
-
- if(isitem(O) || isstructure(O) || ismachinery(O))
- cocoon_target = O
- busy = MOVING_TO_TARGET
- stop_automated_movement = 1
- Goto(O, move_to_delay)
- //give up if we can't reach them after 10 seconds
- GiveUp(O)
-
- else if(busy == MOVING_TO_TARGET && cocoon_target)
- if(get_dist(src, cocoon_target) <= 1)
- cocoon()
-
- else
- busy = SPIDER_IDLE
- stop_automated_movement = FALSE
-
-/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/cocoon()
- if(stat != DEAD && cocoon_target && !cocoon_target.anchored)
- if(cocoon_target == src)
- to_chat(src, span_warning("You can't wrap yourself!"))
- return
- if(istype(cocoon_target, /mob/living/simple_animal/hostile/poison/giant_spider))
- to_chat(src, span_warning("You can't wrap other spiders!"))
- return
- if(!Adjacent(cocoon_target))
- to_chat(src, span_warning("You can't reach [cocoon_target]!"))
- return
- if(busy == SPINNING_COCOON)
- to_chat(src, span_warning("You're already spinning a cocoon!"))
- return //we're already doing this, don't cancel out or anything
- busy = SPINNING_COCOON
- visible_message(span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."),span_notice("You begin wrapping [cocoon_target] into a cocoon."))
- stop_automated_movement = TRUE
- walk(src,0)
- if(do_after(src, 50, target = cocoon_target))
- if(busy == SPINNING_COCOON)
- var/obj/structure/spider/cocoon/C = new(cocoon_target.loc)
- if(isliving(cocoon_target))
- var/mob/living/L = cocoon_target
- if(L.blood_volume && (L.stat != DEAD || !consumed_mobs[L.tag])) //if they're not dead, you can consume them anyway
- consumed_mobs[L.tag] = TRUE
- fed++
- lay_eggs.UpdateButtonIcon(TRUE)
- visible_message(span_danger("[src] sticks a proboscis into [L] and sucks a viscous substance out."),span_notice("You suck the nutriment out of [L], feeding you enough to lay a cluster of eggs."))
- L.death() //you just ate them, they're dead.
- else
- to_chat(src, span_warning("[L] cannot sate your hunger!"))
- cocoon_target.forceMove(C)
-
- if(cocoon_target.density || ismob(cocoon_target))
- C.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3")
- cocoon_target = null
- busy = SPIDER_IDLE
- stop_automated_movement = FALSE
+// ============================================================
+// SPIDER ACTIONS
+// ============================================================
/datum/action/innate/spider
icon_icon = 'icons/mob/actions/actions_animal.dmi'
background_icon_state = "bg_alien"
+// Spin Web action - available to all giant_spiders
/datum/action/innate/spider/lay_web
name = "Spin Web"
desc = "Spin a web to slow down potential prey."
@@ -423,23 +491,27 @@
return
var/turf/T = get_turf(S)
- var/obj/structure/spider/stickyweb/W = locate() in T
- if(W)
+ if(locate(/obj/structure/spider/stickyweb) in T)
to_chat(S, span_warning("There's already a web here!"))
return
- if(S.busy != SPINNING_WEB)
- S.busy = SPINNING_WEB
- S.visible_message(span_notice("[S] begins to secrete a sticky substance."),span_notice("You begin to lay a web."))
- S.stop_automated_movement = TRUE
- if(do_after(S, 40, target = T))
- if(S.busy == SPINNING_WEB && S.loc == T)
- new /obj/structure/spider/stickyweb(T)
- S.busy = SPIDER_IDLE
- S.stop_automated_movement = FALSE
- else
+ if(S.busy == SPINNING_WEB)
to_chat(S, span_warning("You're already spinning a web!"))
+ return
+ S.busy = SPINNING_WEB
+ S.visible_message(
+ span_notice("[S] begins to secrete a sticky substance."),
+ span_notice("You begin to lay a web.")
+ )
+ S.stop_automated_movement = TRUE
+ if(do_after(S, 40, target = T))
+ if(S.busy == SPINNING_WEB && S.loc == T)
+ new /obj/structure/spider/stickyweb(T)
+ S.busy = SPIDER_IDLE
+ S.stop_automated_movement = FALSE
+
+// Wrap ability - nurse spider only (obj/effect/proc_holder, added via AddAbility)
/obj/effect/proc_holder/wrap
name = "Wrap"
panel = "Spider"
@@ -457,35 +529,11 @@
action = new(src)
/obj/effect/proc_holder/wrap/Destroy()
- // CRITICAL: Clear ALL references that might keep this alive
-
- // Clear from user's ranged ability AND click intercept
- if(ranged_ability_user)
- // Remove from their abilities list
- if(ranged_ability_user.abilities)
- ranged_ability_user.abilities -= src
- // If we're their active ranged ability, clear it
- if(ranged_ability_user.ranged_ability == src)
- ranged_ability_user.ranged_ability = null
- // If we're their click intercept, clear it
- if(ranged_ability_user.click_intercept == src)
- ranged_ability_user.click_intercept = null
- ranged_ability_user = null
-
- // Clear the action completely
+ // action was created in Initialize() - qdel it explicitly to prevent leak
if(action)
- // Remove from owner's action list
- if(action.owner)
- if(action.owner.actions)
- action.owner.actions -= action
+ if(action.owner == src)
action.owner = null
- // Delete the action
QDEL_NULL(action)
-
- // Clear any other vars
- active = FALSE
- has_action = FALSE
-
return ..()
/obj/effect/proc_holder/wrap/update_icon()
@@ -499,13 +547,10 @@
return TRUE
/obj/effect/proc_holder/wrap/proc/activate(mob/living/user)
- var/message
if(active)
- message = span_notice("You no longer prepare to wrap something in a cocoon.")
- remove_ranged_ability(message)
+ remove_ranged_ability(span_notice("You no longer prepare to wrap something in a cocoon."))
else
- message = "You prepare to wrap something in a cocoon. Left-click your target to start wrapping!"
- add_ranged_ability(user, message, TRUE)
+ add_ranged_ability(user, "You prepare to wrap something in a cocoon. Left-click your target to start wrapping!", TRUE)
return 1
/obj/effect/proc_holder/wrap/InterceptClickOn(mob/living/caller, params, atom/target)
@@ -528,9 +573,9 @@
/obj/effect/proc_holder/wrap/on_lose(mob/living/carbon/user)
remove_ranged_ability()
- // Don't set ranged_ability_user to null here - let Destroy() handle it
..()
+// Lay Eggs action - nurse spider only
/datum/action/innate/spider/lay_eggs
name = "Lay Eggs"
desc = "Lay a cluster of eggs, which will soon grow into more spiders. You must wrap a living being to do this."
@@ -542,40 +587,45 @@
if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/nurse))
return 0
var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner
- if(S.fed)
- return 1
- return 0
+ return S.fed ? 1 : 0
+ return 0
/datum/action/innate/spider/lay_eggs/Activate()
if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/nurse))
return
var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner
- var/obj/structure/spider/eggcluster/E = locate() in get_turf(S)
- if(E)
+ if(locate(/obj/structure/spider/eggcluster) in get_turf(S))
to_chat(S, span_warning("There is already a cluster of eggs here!"))
- else if(!S.fed)
+ return
+ if(!S.fed)
to_chat(S, span_warning("You are too hungry to do this!"))
- else if(S.busy != LAYING_EGGS)
- S.busy = LAYING_EGGS
- S.visible_message(span_notice("[S] begins to lay a cluster of eggs."),span_notice("You begin to lay a cluster of eggs."))
- S.stop_automated_movement = TRUE
- if(do_after(S, 50, target = get_turf(S)))
- if(S.busy == LAYING_EGGS)
- E = locate() in get_turf(S)
- if(!E || !isturf(S.loc))
- var/obj/structure/spider/eggcluster/C = new /obj/structure/spider/eggcluster(get_turf(S))
- if(S.ckey)
- C.player_spiders = TRUE
- C.directive = S.directive
- C.poison_type = S.poison_type
- C.poison_per_bite = S.poison_per_bite
- C.faction = S.faction.Copy()
- S.fed--
- UpdateButtonIcon(TRUE)
- S.busy = SPIDER_IDLE
- S.stop_automated_movement = FALSE
+ return
+ if(S.busy == LAYING_EGGS)
+ return
+ S.busy = LAYING_EGGS
+ S.visible_message(
+ span_notice("[S] begins to lay a cluster of eggs."),
+ span_notice("You begin to lay a cluster of eggs.")
+ )
+ S.stop_automated_movement = TRUE
+ if(do_after(S, 50, target = get_turf(S)))
+ if(S.busy == LAYING_EGGS && isturf(S.loc))
+ if(!(locate(/obj/structure/spider/eggcluster) in get_turf(S)))
+ var/obj/structure/spider/eggcluster/C = new /obj/structure/spider/eggcluster(get_turf(S))
+ if(S.ckey)
+ C.player_spiders = TRUE
+ C.directive = S.directive
+ C.poison_type = S.poison_type
+ C.poison_per_bite = S.poison_per_bite
+ C.faction = S.faction.Copy()
+ S.fed--
+ UpdateButtonIcon(TRUE)
+ S.busy = SPIDER_IDLE
+ S.stop_automated_movement = FALSE
+
+// Set Directive action - nurse spider only
/datum/action/innate/spider/set_directive
name = "Set Directive"
desc = "Set a directive for your children to follow."
@@ -588,10 +638,7 @@
var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner
S.directive = stripped_input(S, "Enter the new directive", "Create directive", "[S.directive]")
-/mob/living/simple_animal/hostile/poison/giant_spider/Login()
- . = ..()
- GLOB.spidermobs[src] = TRUE
-
+// Spider Communication action - midwife only
/datum/action/innate/spider/comm
name = "Command"
desc = "Send a command to all living spiders."
@@ -612,15 +659,16 @@
/datum/action/innate/spider/comm/proc/spider_command(mob/living/user, message)
if(!message)
return
- var/my_message
- my_message = "Command from [user]: [message]"
+ var/my_message = "Command from [user]: [message]"
for(var/mob/living/simple_animal/hostile/poison/giant_spider/M in GLOB.spidermobs)
to_chat(M, my_message)
for(var/M in GLOB.dead_mob_list)
var/link = FOLLOW_LINK(M, user)
to_chat(M, "[link] [my_message]")
- usr.log_talk(message, LOG_SAY, tag="spider command")
+ usr.log_talk(message, LOG_SAY, tag = "spider command")
+// Temperature damage override - spiders take brute from extreme temps
+// (bypasses the normal fire/burn damage path)
/mob/living/simple_animal/hostile/poison/giant_spider/handle_temperature_damage()
if(bodytemperature < minbodytemp)
adjustBruteLoss(20)
diff --git a/code/modules/mob/living/simple_animal/hostile/headcrab.dm b/code/modules/mob/living/simple_animal/hostile/headcrab.dm
index ecb394e5a43..cd321da54f5 100644
--- a/code/modules/mob/living/simple_animal/hostile/headcrab.dm
+++ b/code/modules/mob/living/simple_animal/hostile/headcrab.dm
@@ -49,7 +49,7 @@
return
Infect(target)
to_chat(src, span_userdanger("With our egg laid, our death approaches rapidly..."))
- addtimer(CALLBACK(src, PROC_REF(death)), 100)
+ addtimer(CALLBACK(src, PROC_REF(death)), 100, TIMER_DELETE_ME)
/obj/item/organ/body_egg/changeling_egg
name = "changeling egg"
diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm
index 688a4bc0a38..a02dba6a578 100644
--- a/code/modules/mob/living/simple_animal/hostile/hostile.dm
+++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm
@@ -1,3 +1,8 @@
+// Combat mode defines
+#define COMBAT_MODE_MELEE 1 // Pure melee, always rush in
+#define COMBAT_MODE_RANGED 2 // Pure ranged, never voluntarily enter melee
+#define COMBAT_MODE_MIXED 3 // Ranged but will melee if target gets close (like reaver)
+
/mob/living/simple_animal/hostile
faction = list("hostile")
stop_automated_movement_when_pulled = 0
@@ -77,6 +82,12 @@
var/robust_searching = 0 //By default, mobs have a simple searching method, set this to 1 for the more scrutinous searching (stat_attack, stat_exclusive, etc), should be disabled on most mobs
var/vision_range = 9 //How big of an area to search for targets in, a vision of 9 attempts to find targets as soon as they walk into screen view
var/aggro_vision_range = 9 //If a mob is aggro, we search in this radius. Defaults to 9 to keep in line with original simple mob aggro radius
+
+ // LOW-LIGHT VISION: Some mobs (mutants, animals, nightkin) see better in darkness
+ var/has_low_light_vision = FALSE // Set to TRUE for mutants, animals, nightkin, deathclaws, etc.
+ var/low_light_bonus = 3 // Extra tiles of vision in darkness when has_low_light_vision is TRUE
+ var/debug_vision = FALSE // Set to TRUE to see cone detection messages in chat
+
var/search_objects = 0 //If we want to consider objects when searching around, set this to 1. If you want to search for objects while also ignoring mobs until hurt, set it to 2. To completely ignore mobs, even when attacked, set it to 3
var/search_objects_timer_id //Timer for regaining our old search_objects value after being attacked
var/search_objects_regain_time = 30 //the delay between being attacked and gaining our old search_objects value back
@@ -110,8 +121,14 @@
/// timer for despawning when lonely
var/lonely_timer_id
- /// Can this mob pursue targets across Z-levels?
+ /// Can this mob pursue targets across Z-levels? (Master toggle)
var/can_z_move = TRUE
+ /// Can this mob climb ladders specifically?
+ var/can_climb_ladders = TRUE
+ /// Can this mob use stairs specifically?
+ var/can_climb_stairs = TRUE
+ /// Can this mob jump down through openspace?
+ var/can_jump_down = TRUE
/// How long to wait before attempting Z pursuit (deciseconds)
var/z_move_delay = 30
/// Last time we attempted Z-level movement
@@ -119,6 +136,260 @@
/// What Z-level was our target last seen on?
var/target_last_z = 0
+ // Z-PURSUIT STATE TRACKING - prevents AI from getting confused during vertical movement
+ /// Are we currently in the middle of Z-level pursuit? Used to lock the mob into climbing mode
+ var/pursuing_z_target = FALSE
+ /// What structure (ladder/stairs) are we trying to reach for Z pursuit?
+ var/atom/z_pursuit_structure = null
+ /// When did we start Z-pursuit? Used for timeout
+ var/z_pursuit_started = 0
+ /// Maximum time to spend in Z-pursuit before giving up (10 seconds)
+ var/z_pursuit_timeout = 100
+
+ /// Remembered target during Z-pursuit - won't lose them even if out of sight
+ var/atom/remembered_target = null
+ /// When did we last see our target? Used to prevent losing them during brief moments out of sight
+ var/last_target_sighting = 0
+ /// How long to remember target after losing sight (deciseconds) - 15 seconds
+ var/target_memory_duration = 150
+
+ /// Are we in active search mode?
+ var/searching = FALSE
+ /// Timer ID for search mode
+ var/search_timer_id = null
+ /// How long to search after losing sight (deciseconds) - 30 seconds
+ var/search_duration = 300
+ /// Last known location of target
+ var/turf/last_known_location = null
+ /// Search radius around last known location
+ var/search_radius = 7
+ /// SMART SEARCH: Track visited search locations to avoid repetition
+ var/list/searched_turfs = list()
+ /// SMART SEARCH: Current search expansion level (increases over time)
+ var/search_expansion = 0
+ /// SMART SEARCH: Doors we've already tried to open during search
+ var/list/searched_doors = list()
+ /// SMART SEARCH: Containers (closets/crates/lockers) we've searched
+ var/list/searched_containers = list()
+ /// SMART SEARCH: Recently opened door (to prioritize exploring beyond it)
+ var/atom/recently_opened_door = null
+ /// SMART SEARCH: Container we're currently moving toward to investigate
+ var/obj/structure/closet/investigating_container = null
+ /// SMART SEARCH: Timestamp when search started (for timeout)
+ var/search_start_time = 0
+ /// SMART SEARCH: Base search timeout for non-aggressive mobs (2.5 minutes)
+ var/search_timeout_base = 1500 // 150 seconds = 2.5 minutes
+ /// SMART SEARCH: Extended search timeout for aggressive mobs (5 minutes)
+ var/search_timeout_aggressive = 3000 // 300 seconds = 5 minutes
+ /// SMART SEARCH: Last time we exited search mode
+ var/last_search_exit_time = 0
+ /// SMART SEARCH: Cooldown before re-entering search (prevents spam loop)
+ var/search_entry_cooldown = 10 // 1 second
+
+ /// How far to call for backup when finding a target (tiles)
+ var/backup_call_range = 2
+ /// Cooldown between backup calls to prevent spam
+ COOLDOWN_DECLARE(backup_call_cooldown)
+ /// How long between backup calls (deciseconds)
+ var/backup_call_delay = 50 // 5 seconds
+
+ /// Are we currently rallying to allies?
+ var/rallying = FALSE
+ /// When did we start rallying?
+ var/rally_start_time = 0
+ /// How long to rally before advancing (deciseconds) - 3 seconds
+ var/rally_duration = 30
+ /// Rally point location
+ var/turf/rally_point = null
+
+ /// Last time we successfully changed Z-levels
+ var/last_successful_z_move = 0
+ /// Cooldown after changing Z before we can pursue again (prevents stair loops)
+ var/z_move_success_cooldown = 30 // 3 seconds
+
+ /// Can this mob open doors?
+ var/can_open_doors = FALSE
+ /// Can this mob open airlocks specifically?
+ var/can_open_airlocks = FALSE
+ /// Cooldown between door opening attempts
+ COOLDOWN_DECLARE(door_open_cooldown)
+ /// How long between door opening attempts
+ var/door_open_delay = 20 // 2 seconds
+
+ /// CURIOSITY SYSTEM: Idle mobs occasionally investigate nearby closed doors/rooms
+ var/curiosity_enabled = TRUE
+ /// When did we last investigate out of curiosity?
+ var/last_curiosity_check = 0
+ /// How often to consider investigating (deciseconds) - 10-20 seconds
+ var/curiosity_check_interval = 100
+ /// Chance to actually investigate when checking (0-100)
+ var/curiosity_chance = 35
+
+ /// What Z-level are we committed to searching?
+ var/committed_z_level = 0
+ /// When did we commit to this Z-level?
+ var/z_commit_time = 0
+ /// How long to stay on a Z-level before switching (deciseconds) - 10 seconds
+ var/z_commit_duration = 100
+ /// How many times have we climbed recently? (prevents yo-yoing)
+ var/recent_climbs = 0
+ /// Last time we reset climb counter
+ var/last_climb_reset = 0
+
+ /// Can we hear combat sounds to locate targets?
+ var/can_hear_combat = TRUE // Work on what mobs can hear or not, leave on TRUE for now
+ /// Range we can hear combat sounds (tiles)
+ var/combat_hearing_range = 7 // Reduced to 7 tiles by default
+ /// Last time we heard combat
+ var/last_combat_sound = 0
+ /// Range we can hear impact sounds (tiles) - same as gunfire (impacts ARE from gunfire)
+ var/impact_hearing_range = 7 // Increased to 7 tiles to match combat sounds
+
+ // LIGHT DETECTION SYSTEM - Mobs notice player light sources in darkness
+ /// Can this mob detect light sources?
+ var/can_detect_light = TRUE
+
+ // DETECTION DELAY SYSTEM - Sneaking players have detection buildup time
+ /// Detection progress toward player (0 to detection_time_required)
+ var/detection_progress = 0
+ /// How long to fully detect player (deciseconds) - 0 = instant, 30 = 3 seconds for sneaking
+ var/detection_time_required = 0
+ /// Who are we currently building detection progress toward?
+ var/atom/detecting_target = null
+ /// Cooldown for detection tick updates
+ COOLDOWN_DECLARE(detection_tick_cooldown)
+ /// How far can we detect light sources? (tiles)
+ var/light_detection_range = 9
+ /// Minimum light_range value to be detected (flashlights are ~3-4, torches ~3)
+ var/light_detection_threshold = 2
+ /// Last time we detected a light source
+ var/last_light_detected = 0
+ /// Cooldown between light detections (deciseconds) - prevents spam
+ var/light_detection_cooldown = 50 // 5 seconds
+ /// Memory of last detected light location
+ var/turf/last_light_location = null
+ /// Should we only detect lights in dark areas? (TRUE = ignore lights in well-lit rooms)
+ var/only_detect_lights_in_darkness = TRUE
+ /// Minimum darkness level required to detect lights (0-1, lower = darker)
+ var/darkness_threshold = 0.5
+
+ /// SPATIAL AWARENESS & DOOR MEMORY
+ /// List of door locations we know about and can potentially use
+ var/list/known_doors = list()
+ /// Last position we were at
+ var/turf/last_position = null
+ /// How long have we been stuck in the same position?
+ var/stuck_time = 0
+ /// Position where we got stuck
+ var/turf/stuck_position = null
+ /// How long to wait before considering ourselves stuck (deciseconds) - 10 seconds
+ var/stuck_timeout = 100
+ /// Faster timeout for corner situations where we can see target (deciseconds) - 3 seconds
+ var/corner_stuck_timeout = 30
+ /// How long to wait before smashing (deciseconds) - 15 seconds (stuck_timeout + smash_delay)
+ var/smash_delay = 150
+
+ /// DOOR KITING PREVENTION: Track TARGET's door passages to detect exploit
+ /// Last door the target passed through
+ var/atom/target_last_door = null
+ /// How many times has target passed through this door recently?
+ var/target_door_pass_count = 0
+ /// Last time target passed through a door
+ var/target_last_door_pass_time = 0
+ /// Have we committed to one side due to target kiting?
+ var/committed_to_door_side = FALSE
+ /// Which side of the door are we committed to?
+ var/turf/committed_door_location = null
+ /// Last known position of target (for tracking movement)
+ var/turf/target_last_position = null
+
+ /// CORNER KITING PREVENTION: Track target peeking around corners
+ /// How many times has target peeked from same area?
+ var/corner_peek_count = 0
+ /// Last time target peeked
+ var/last_corner_peek_time = 0
+ /// Location where target keeps peeking from
+ var/turf/corner_peek_location = null
+ /// Have we committed to rushing a corner?
+ var/committed_to_corner = FALSE
+
+ /// LIGHT PATROL SYSTEM: Idle mobs patrol to nearby doors when players are nearby
+ /// Are we on light patrol?
+ var/light_patrolling = FALSE
+ /// Where did we start the patrol from?
+ var/turf/patrol_home = null
+ /// Which door are we patrolling to?
+ var/atom/patrol_target_door = null
+ /// How many times have we visited each door?
+ var/list/patrol_door_visits = list()
+ /// Maximum visits before resting at home
+ var/patrol_max_visits = 2
+ /// Are we resting after patrol?
+ var/patrol_resting = FALSE
+ /// When did we start resting?
+ var/patrol_rest_start = 0
+ /// How long to rest before patrolling again (30 seconds)
+ var/patrol_rest_duration = 300
+ /// Timer for patrol checks
+ var/patrol_check_timer = null
+
+ /// THROWN ITEM DETECTION: Track thrown items and investigate spammers
+ /// Track recent thrown items and their sources
+ var/list/recent_thrown_items = list()
+ /// When did we last clear the thrown item list?
+ var/last_thrown_clear = 0
+ /// How many thrown items before we investigate the source?
+ var/thrown_spam_threshold = 3
+ /// Cooldown for investigating thrown items (deciseconds)
+ var/thrown_investigation_cooldown = 150 // 15 seconds
+
+ /// LIGHT DESTRUCTION DETECTION: Track shot-out lights and investigate
+ /// Track recent light destructions and their sources
+ var/list/recent_light_shots = list()
+ /// When did we last clear the light shot list?
+ var/last_light_shot_clear = 0
+ /// How many lights shot before we investigate the shooter?
+ var/light_spam_threshold = 3
+
+ /// Last time we smashed something (prevents spam)
+ var/last_smash_time = 0
+ /// Cooldown between smash attempts (deciseconds) - 5 seconds
+ var/smash_cooldown = 50
+ /// Are we currently stuck and trying alternate routes?
+ var/is_stuck = FALSE
+ /// How many alternate path attempts have we made?
+ var/path_attempts = 0
+ /// Maximum path attempts before smashing
+ var/max_path_attempts = 3
+ /// Last time we gave up chasing target due to being stuck
+ var/last_give_up_time = 0
+ /// Cooldown after giving up before we can re-acquire targets (deciseconds) - 15 seconds
+ var/give_up_cooldown = 150
+
+ /// How long have we been on stairs?
+ var/time_on_stairs = 0
+ /// Last time we checked if on stairs
+ var/last_stairs_check = 0
+ /// Maximum time allowed on stairs before forced off (deciseconds) - 5 seconds
+ var/max_stairs_time = 50
+
+ /// How many consecutive times have we stepped on stairs?
+ var/consecutive_stair_steps = 0
+ /// Maximum consecutive stair steps before forcing off
+ var/max_consecutive_stair_steps = 5
+
+ /// What's our primary combat style?
+ var/combat_mode = COMBAT_MODE_MELEE // COMBAT_MODE_MELEE, COMBAT_MODE_RANGED, or COMBAT_MODE_MIXED
+
+ /// CHAIN REACTION ALERTING - tracks if this mob is the "alpha" that woke up others
+ var/is_alpha_alerter = FALSE
+ /// Range for chain reaction LOS alerting (tiles)
+ var/chain_alert_range = 3
+ /// Cooldown for chain alerting to prevent spam
+ COOLDOWN_DECLARE(chain_alert_cooldown)
+ /// How long between chain alerts (deciseconds)
+ var/chain_alert_delay = 30 // 3 seconds
+
/mob/living/simple_animal/hostile/Initialize()
. = ..()
@@ -134,6 +405,37 @@
GiveTarget(null)
targets_from = null
+ // Clear Z-pursuit memory references - IMPORTANT FOR GC
+ remembered_target = null
+ z_pursuit_structure = null
+ last_known_location = null
+
+ // Clear Z-commitment
+ committed_z_level = 0
+
+ // Clear search mode
+ if(search_timer_id)
+ deltimer(search_timer_id)
+ search_timer_id = null
+ searching = FALSE
+
+ // Clear smart search memory
+ if(searched_turfs)
+ searched_turfs.Cut()
+ searched_turfs = null
+
+ if(searched_doors)
+ searched_doors.Cut()
+ searched_doors = null
+
+ search_expansion = 0
+
+ // Clear stuck state and door memory
+ known_doors = null
+ last_position = null
+ stuck_position = null
+ is_stuck = FALSE
+
// Clear lists
if(friends)
friends.Cut()
@@ -217,17 +519,206 @@
if(AIStatus == AI_OFF)
return 0
- var/list/possible_targets = ListTargets() //we look around for potential targets and make it a list for later use.
+ var/list/possible_targets = ListTargets()
+
+ // Safety check: ensure possible_targets is a proper list, not nested
+ if(!possible_targets)
+ possible_targets = list()
+ else if(!islist(possible_targets))
+ possible_targets = list(possible_targets)
+ else if(islist(possible_targets))
+ // Check for nested lists and flatten them
+ var/has_nested = FALSE
+ for(var/item in possible_targets)
+ if(islist(item))
+ has_nested = TRUE
+ break
+
+ if(has_nested)
+ var/list/flattened = list()
+ var/original_len = possible_targets.len
+ for(var/entry in possible_targets)
+ if(islist(entry))
+ for(var/subitem in entry)
+ flattened += subitem
+ else
+ flattened += entry
+ possible_targets = flattened
+
+ // Debug logging to track down the source
+ log_runtime("HOSTILE MOB: [src] ([type]) had nested list from ListTargets() at [get_turf(src)]. Original list: [original_len] entries, flattened to [flattened.len] entries.")
+
+ // IMMEDIATE TARGET RESPONSE: Fast acquisition for both search mode and initial detection
+ // When idle or searching, quickly grab first valid target without waiting for next cycle
+ if((searching || AIStatus == AI_IDLE) && possible_targets && possible_targets.len > 0)
+ for(var/atom/possible_target in possible_targets)
+ // Dead/ghost check
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+
+ // CORE FIX: Check distance FIRST using effective range
+ var/effective_range = get_effective_vision_range(possible_target)
+ var/actual_distance = get_dist(src, possible_target)
+
+ if(actual_distance > effective_range)
+ continue // Too far in darkness
+
+ // Now check line of sight
+ if(!can_see(src, possible_target, effective_range))
+ continue
+
+ // Faction check
+ if(!CanAttack(possible_target))
+ continue
+
+ // All checks passed - acquire target
+ if(AIStatus == AI_IDLE)
+ toggle_ai(AI_ON)
+
+ if(searching)
+ exit_search_mode(FALSE, found_target = TRUE)
+
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ call_for_backup(possible_target, "found")
+ break
+
+ // LIGHT DETECTION: Periodically check for light sources in darkness
+ // Has its own cooldown to avoid spam
+ if(!target || searching)
+ detect_light_source()
+
+ // POSITION TRACKING
+ var/turf/current_pos = get_turf(src)
+ if(target && current_pos)
+ if(last_position == current_pos)
+ if(!stuck_position)
+ stuck_position = current_pos
+ stuck_time = world.time
+ else
+ var/stuck_duration = world.time - stuck_time
+ var/target_dist = get_dist(src, target)
+ var/can_see_target = can_see(src, target, get_effective_vision_range(target))
+ if(can_see_target && target_dist >= 2 && target_dist <= 10)
+ if(stuck_duration > corner_stuck_timeout && !is_stuck)
+ is_stuck = TRUE
+ path_attempts = 0
+ visible_message(span_notice("[src] changes approach..."))
+ else if(stuck_duration > stuck_timeout)
+ if(!is_stuck)
+ is_stuck = TRUE
+ path_attempts = 0
+ else
+ if(is_stuck || stuck_position)
+ is_stuck = FALSE
+ stuck_position = null
+ stuck_time = 0
+ path_attempts = 0
+ else
+ is_stuck = FALSE
+ stuck_position = null
+ stuck_time = 0
+ path_attempts = 0
+
+ last_position = current_pos
+
+ if(can_open_doors && prob(10))
+ scan_for_doors()
+
+ var/on_stairs = FALSE
+ for(var/obj/structure/stairs/S in loc)
+ on_stairs = TRUE
+ break
+
+ if(on_stairs)
+ if(time_on_stairs == 0)
+ time_on_stairs = world.time
+ else if((world.time - time_on_stairs) > max_stairs_time)
+ commit_to_z_level()
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ time_on_stairs = 0
+ var/list/escape_turfs = list()
+ for(var/turf/T in range(3, src))
+ var/has_stairs = FALSE
+ for(var/obj/structure/stairs/ST in T)
+ has_stairs = TRUE
+ break
+ if(!has_stairs && istype(T, /turf/open))
+ escape_turfs += T
+ if(escape_turfs.len)
+ Goto(pick(escape_turfs), move_to_delay, 0)
+ else
+ time_on_stairs = 0
+
+ if(can_open_doors && target)
+ var/turf/T = get_step(src, get_dir(src, target))
+ if(T)
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density)
+ try_open_door(SD)
+ for(var/obj/machinery/door/D in T)
+ if(D.density)
+ try_open_door(D)
if(environment_smash)
EscapeConfinement()
+ // CURIOSITY SYSTEM: Idle mobs occasionally investigate nearby doors/rooms
+ if(AIStatus == AI_IDLE && curiosity_enabled && can_open_doors && !target && !searching)
+ if((world.time - last_curiosity_check) > curiosity_check_interval)
+ last_curiosity_check = world.time
+ if(prob(curiosity_chance))
+ // PERFORMANCE: Use direct object range() instead of turf iteration
+ // Limit to range 3 instead of vision_range (9) to reduce lag
+ var/list/nearby_doors = list()
+ for(var/obj/structure/simple_door/SD in range(3, src))
+ if(SD.density)
+ nearby_doors += SD
+ for(var/obj/machinery/door/D in range(3, src))
+ if(D.density)
+ nearby_doors += D
+
+ if(nearby_doors.len > 0)
+ // Prefer doors closer to us
+ var/atom/door_to_check = null
+ var/closest_dist = 999
+ for(var/atom/door in nearby_doors)
+ var/dist = get_dist(src, door)
+ if(dist < closest_dist && prob(70)) // 70% chance to keep closer one
+ closest_dist = dist
+ door_to_check = door
+
+ if(!door_to_check)
+ door_to_check = pick(nearby_doors)
+
+ visible_message(span_notice("[src] investigates a nearby door..."))
+ Goto(door_to_check, move_to_delay, 0)
+ // Try to open it when we get adjacent
+ addtimer(CALLBACK(src, PROC_REF(curiosity_check_door), door_to_check), 3 SECONDS, TIMER_DELETE_ME)
+
+ // LIGHT PATROL SYSTEM: Check for nearby players and patrol
+ if(AIStatus == AI_IDLE && !target && !searching && !light_patrolling)
+ // Check every 5 seconds (50 deciseconds)
+ if(!patrol_check_timer || (world.time - patrol_check_timer) > 50)
+ patrol_check_timer = world.time
+ check_light_patrol()
+
if(AICanContinue(possible_targets))
if(!QDELETED(target) && !targets_from.Adjacent(target))
- DestroyPathToTarget()
- if(!MoveToTarget(possible_targets)) //if we lose our target
- if(AIShouldSleep(possible_targets)) // we try to acquire a new one
- toggle_ai(AI_IDLE) // otherwise we go idle
+ if(is_stuck)
+ var/can_see_target_now = target && can_see(src, target, get_effective_vision_range(target))
+ // If we can SEE the target but are stuck, smash immediately - don't wait for smash_delay
+ if(can_see_target_now && (world.time - stuck_time > corner_stuck_timeout))
+ DestroyPathToTarget()
+ else if(world.time - stuck_time > smash_delay)
+ DestroyPathToTarget()
+ if(!MoveToTarget(possible_targets))
+ if(AIShouldSleep(possible_targets))
+ toggle_ai(AI_IDLE)
consider_despawning()
return 1
@@ -309,17 +800,268 @@
if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client && user)
FindTarget(list(user), 1)
COOLDOWN_RESET(src, sight_shoot_delay) // Let them shoot back immediately when attacked
+
+ // CHAIN REACTION: This mob just woke up, alert nearby allies
+ trigger_chain_alert(user)
return ..()
/mob/living/simple_animal/hostile/bullet_act(obj/item/projectile/P)
+ // ALERT NEARBY ALLIES ABOUT COMBAT - alert at multiple locations
+ if(P.firer)
+ // Alert from shooter location (full range, muffled by walls)
+ alert_allies_of_combat(get_turf(P.firer), P.firer)
+
+ // Alert from impact location (smaller radius, also muffled)
+ // This represents the sound of bullet hitting wall/target/mob
+ var/turf/impact_loc = get_turf(src)
+ if(impact_loc)
+ // Use a smaller range for impact sounds
+ alert_allies_of_impact(impact_loc, P.firer, impact_hearing_range)
+
. = ..()
if (peaceful == TRUE)
peaceful = FALSE
- if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client)
- if(P.firer && get_dist(src, P.firer) <= aggro_vision_range)
- FindTarget(list(P.firer), 1)
- COOLDOWN_RESET(src, sight_shoot_delay) // Let them shoot back immediately
- Goto(P.starting, move_to_delay, 3)
+
+ // IMPROVED RETARGETING: Switch targets if being shot by someone closer or while searching
+ if(stat == CONSCIOUS && AIStatus != AI_OFF && !client && P.firer)
+ if(get_dist(src, P.firer) <= aggro_vision_range)
+ var/should_retarget = FALSE
+
+ if(!target)
+ // No target, always acquire the shooter
+ should_retarget = TRUE
+ else
+ var/current_target_dist = get_dist(src, target)
+ var/shooter_dist = get_dist(src, P.firer)
+
+ // Retarget if: shooter is much closer, OR we're searching (lost current target)
+ if(shooter_dist < (current_target_dist - 3) || searching)
+ should_retarget = TRUE
+
+ if(should_retarget)
+ if(searching)
+ exit_search_mode(found_target = TRUE) // Found shooter
+ FindTarget(list(P.firer), 1)
+ COOLDOWN_RESET(src, sight_shoot_delay)
+ trigger_chain_alert(P.firer)
+ visible_message(span_danger("[src] turns its attention to [P.firer]!"))
+ return
+
+ if(!target) // Only run to projectile source if we have no target after all checks
+ Goto(P.starting, move_to_delay, 3)
+
+// GLOBAL HELPER: Alert all hostile mobs about a projectile impact at a location
+// This should be called from turf/obj bullet_act implementations or projectile on_range
+/proc/alert_hostile_mobs_of_impact(turf/impact_location, atom/firer, impact_range = 5)
+ if(!impact_location || !firer)
+ return
+
+ for(var/mob/living/simple_animal/hostile/M in range(7, impact_location))
+ if(M.stat == DEAD || M.ckey || !M.can_hear_combat)
+ continue
+
+ // Only alert mobs actively engaged (has target or searching)
+ if(!M.target && !M.searching)
+ continue
+
+ // Check muffled range
+ var/effective_range = M.calculate_muffled_sound_range(impact_location, impact_range)
+ var/distance = get_dist(M, impact_location)
+ var/z_distance = abs(M.z - impact_location.z)
+
+ if(distance <= effective_range && z_distance <= 1)
+ M.hear_impact_sound(impact_location, firer)
+
+// CHAIN REACTION ALERTING
+// When a mob wakes up, it alerts all allies within LOS (vision range)
+// Those allies then check for the original target and also wake up
+// This creates a cascading alert system
+/mob/living/simple_animal/hostile/proc/trigger_chain_alert(atom/threat)
+ if(!threat)
+ return
+
+ // BUG 2 FIX: Only alpha mobs broadcast chain alerts
+ if(!is_alpha_alerter)
+ return
+
+ if(!COOLDOWN_FINISHED(src, chain_alert_cooldown))
+ return
+
+ COOLDOWN_START(src, chain_alert_cooldown, chain_alert_delay)
+
+ visible_message(span_danger("[src] alerts nearby allies!"))
+
+ var/list/alerted_allies = list()
+
+ for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, src))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(!faction_check_mob(M, TRUE))
+ continue
+ if(can_see(src, M, chain_alert_range))
+ alerted_allies += M
+
+ var/turf/above_us = get_step_multiz(get_turf(src), UP)
+ if(above_us)
+ for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, above_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(!faction_check_mob(M, TRUE))
+ continue
+ if(can_see(src, M, chain_alert_range))
+ alerted_allies += M
+
+ var/turf/below_us = get_step_multiz(get_turf(src), DOWN)
+ if(below_us)
+ for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, below_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(!faction_check_mob(M, TRUE))
+ continue
+ if(can_see(src, M, chain_alert_range))
+ alerted_allies += M
+
+ for(var/mob/living/simple_animal/hostile/ally in alerted_allies)
+ ally.receive_chain_alert(threat, src)
+
+// Receive a chain alert from an ally
+/mob/living/simple_animal/hostile/proc/receive_chain_alert(atom/threat, mob/living/simple_animal/hostile/alerter)
+ if(!threat || QDELETED(threat))
+ return
+
+ if(AIStatus == AI_ON || target)
+ return
+
+ // Ghost check
+ if(isliving(threat))
+ var/mob/living/L = threat
+ if(L.stat == DEAD || L.ckey && !L.client)
+ return
+
+ visible_message(span_danger("[src] perks up from [alerter]'s alert!"))
+
+ if(can_see(src, threat, get_effective_vision_range(threat)) && CanAttack(threat))
+ // Direct confirmation - becomes alpha, can propagate
+ if(AIStatus != AI_ON)
+ toggle_ai(AI_ON)
+ FindTarget(list(threat), 1)
+ remembered_target = threat
+ last_target_sighting = world.time
+ last_known_location = get_turf(threat)
+ // GiveTarget (called by FindTarget) sets is_alpha_alerter = TRUE
+ // so this mob CAN chain-alert further
+ trigger_chain_alert(threat)
+ else
+ // Can't confirm - search quietly, no further propagation
+ is_alpha_alerter = FALSE
+ if(AIStatus != AI_ON)
+ toggle_ai(AI_ON)
+ last_known_location = get_turf(threat)
+ enter_search_mode() // is_alpha_alerter=FALSE → no broadcast
+
+
+// SOUND MUFFLING THROUGH WALLS
+// Calculates effective hearing range by counting walls between source and listener
+// Each wall reduces range by 1 tile
+/mob/living/simple_animal/hostile/proc/calculate_muffled_sound_range(turf/sound_source, base_range)
+ if(!sound_source)
+ return 0
+
+ var/turf/our_turf = get_turf(src)
+ if(!our_turf)
+ return 0
+
+ // Get line between source and us
+ var/list/line = getline(sound_source, our_turf)
+
+ // Count walls
+ var/wall_count = 0
+ for(var/turf/T in line)
+ if(iswallturf(T))
+ wall_count++
+
+ // Each wall reduces range by 1
+ var/effective_range = base_range - wall_count
+
+ // Can't go below 0
+ return max(effective_range, 0)
+
+// Updated alert_allies_of_combat with sound muffling
+/mob/living/simple_animal/hostile/proc/alert_allies_of_combat(turf/combat_location, atom/attacker)
+ if(!combat_location)
+ return
+
+ // Find allies in hearing range across Z-levels
+ var/list/allies_in_range = list()
+
+ // Same Z
+ for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, combat_location))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ // Check muffled range
+ var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range)
+ var/actual_distance = get_dist(M, combat_location)
+
+ if(actual_distance <= effective_range)
+ allies_in_range += M
+
+ // Z above
+ var/turf/above = get_step_multiz(combat_location, UP)
+ if(above)
+ for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, above))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ // Check muffled range (Z-distance adds 1 wall equivalent)
+ var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range) - 1
+ var/actual_distance = get_dist(M, above)
+
+ if(actual_distance <= effective_range)
+ allies_in_range += M
+
+ // Z below
+ var/turf/below = get_step_multiz(combat_location, DOWN)
+ if(below)
+ for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, below))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ // Check muffled range (Z-distance adds 1 wall equivalent)
+ var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range) - 1
+ var/actual_distance = get_dist(M, below)
+
+ if(actual_distance <= effective_range)
+ allies_in_range += M
+
+ // Alert all allies
+ for(var/mob/living/simple_animal/hostile/ally in allies_in_range)
+ ally.hear_combat_sound(combat_location, attacker)
+
+// Alert allies about impact sounds (bullet hitting wall/target)
+// Smaller radius than gunfire, but still muffled by walls
+/mob/living/simple_animal/hostile/proc/alert_allies_of_impact(turf/impact_location, atom/attacker, impact_range = 5)
+ if(!impact_location)
+ return
+
+ var/notified = 0
+ var/max_notifications = 10 // CPU OPTIMIZATION: Limit notifications per impact
+
+ // CPU OPTIMIZATION: Reduced range from 20 to 5 tiles, ALL hostile mobs (not just faction)
+ // Impacts are universal - everyone hears bullets hitting things nearby
+ for(var/mob/living/simple_animal/hostile/M in range(5, impact_location))
+ if(M.stat == DEAD || M.ckey || !M.can_hear_combat)
+ continue
+
+ // Check muffled range
+ var/effective_range = M.calculate_muffled_sound_range(impact_location, impact_range)
+ var/actual_distance = get_dist(M, impact_location)
+
+ if(actual_distance <= effective_range)
+ M.hear_impact_sound(impact_location, attacker)
+ notified++
+ if(notified >= max_notifications) // Early exit optimization
+ return
/mob/living/simple_animal/hostile/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, message_mode, atom/movable/source)
. = ..()
@@ -336,22 +1078,423 @@
//////////////HOSTILE MOB TARGETTING AND AGGRESSION////////////
+/// Determine which vision cone the target is in relative to mob's facing direction
+/// Returns CONE_FRONT, CONE_PERIPHERAL, or CONE_REAR
+/mob/living/simple_animal/hostile/proc/get_vision_cone(atom/target)
+ if(!target)
+ return CONE_REAR
+
+ var/turf/my_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
+ if(!my_turf || !target_turf)
+ return CONE_REAR
+
+ // Same tile = front cone
+ if(my_turf == target_turf)
+ return CONE_FRONT
+
+ // Calculate relative position
+ var/dx = target_turf.x - my_turf.x
+ var/dy = target_turf.y - my_turf.y
+
+ // If no direction set, face the target
+ if(!dir || dir == 0)
+ setDir(get_dir(src, target))
+ return CONE_FRONT
+
+ // ============================================
+ // QUADRANT-BASED DETECTION (matches visual sectors exactly)
+ // ============================================
+ // Uses IDENTICAL quadrant logic as get_sector_turfs() for perfect 1:1 match
+ //
+ // Sector layout:
+ // FRONT (RED): -30° to +30° → CONE_FRONT
+ // RIGHT PERIPHERAL (YELLOW): 30° to 90° → CONE_PERIPHERAL
+ // RIGHT REAR (CYAN): 90° to 150° → CONE_REAR
+ // REAR (GRAY): 150° to 210° → CONE_REAR
+ // LEFT REAR (CYAN): 210° to 270° → CONE_REAR
+ // LEFT PERIPHERAL (YELLOW): 270° to 330° → CONE_PERIPHERAL
+ // ============================================
+
+ // Get base direction angle
+ var/base_angle = dir_to_angle_detection(dir)
+
+ // Calculate angle to target using SAME quadrant logic as visuals
+ var/target_angle = calculate_angle_from_offset_detection(dx, dy)
+
+ // Calculate relative angle from facing direction
+ var/relative_angle = target_angle - base_angle
+
+ // Normalize to -180 to +180 range
+ while(relative_angle > 180)
+ relative_angle -= 360
+ while(relative_angle < -180)
+ relative_angle += 360
+
+ // Classify based on angle (matching visual sectors EXACTLY)
+ if(relative_angle >= -30 && relative_angle <= 30)
+ // Front center: -30° to +30° (60° total)
+ return CONE_FRONT
+ else if((relative_angle > 30 && relative_angle <= 90) || (relative_angle >= -90 && relative_angle < -30))
+ // Peripheral zones: 30° to 90° and -90° to -30° (270° to 330°)
+ return CONE_PERIPHERAL
+ else
+ // Rear zones: 90° to 270° (includes cyan rear peripheral and gray rear center)
+ return CONE_REAR
+
+/// Get directional vision multiplier (DISABLED FOR TESTING)
+/// Returns 1.0 to disable directional vision while testing lighting
+/mob/living/simple_animal/hostile/proc/get_directional_vision_multiplier(atom/target)
+ // DISABLED: Just return full range for lighting-only testing
+ return 1.0
+
+/// Check if target is within the mob's field of view (direction-based)
+/// Returns TRUE if target is in front half (180-degree cone) of mob's facing direction
+/// Legacy compatibility wrapper for the multiplier system
+/mob/living/simple_animal/hostile/proc/in_field_of_view(atom/target)
+ if(!target)
+ return FALSE
+
+ var/multiplier = get_directional_vision_multiplier(target)
+ return (multiplier >= 0.5) // TRUE if in front or side, FALSE if behind
+
+/// Calculate effective vision range based on lighting AND directional cone
+/// Front cone: Better dark vision (5 tiles in darkness vs 3 in peripheral, 0 in rear)
+/mob/living/simple_animal/hostile/proc/get_effective_vision_range(atom/target)
+ if(!target)
+ return vision_range
+
+ // Determine which cone target is in
+ var/cone_type = get_vision_cone(target)
+
+ // No vision behind us ever
+ if(cone_type == CONE_REAR)
+ return 0
+
+ // Check if target has a light source
+ var/has_light = FALSE
+ if(isliving(target))
+ var/mob/living/L = target
+ if(L.light_range > 0)
+ has_light = TRUE
+
+ // If they have a light, they're visible at full range
+ if(has_light)
+ // Apply cone multiplier
+ if(cone_type == CONE_FRONT)
+ return vision_range
+ else // CONE_PERIPHERAL
+ return max(round(vision_range * 0.6), 2)
+
+ // Check ambient lighting at target location
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return 2
+
+ var/light_amount = target_turf.get_lumcount()
+ var/base_range = vision_range
+
+ // Lighting-based range calculation (same for all cones)
+ if(light_amount >= 0.5)
+ base_range = vision_range // Bright: full range
+ else if(light_amount >= 0.2)
+ base_range = max(round(vision_range * 0.6), 3) // Dim: 60% range
+ else
+ // Darkness: reduced to 40% range
+ base_range = max(round(vision_range * 0.4), 3)
+
+ // STEALTH BOY CHECK: Drastically reduce detection range for cloaked targets
+ if(ishuman(target))
+ var/mob/living/carbon/human/H = target
+ if(H.alpha < 100) // Heavily cloaked by stealth boy
+ // Reduce detection range to 20% - they need to be VERY close
+ base_range = max(round(base_range * 0.2), 1)
+
+ // Apply cone multipliers
+ if(cone_type == CONE_FRONT)
+ return base_range // Full range in front cone
+ else // CONE_PERIPHERAL
+ return max(round(base_range * 0.6), 2) // 60% of lighting range in peripheral
+
+/// LOW-LIGHT VISION: Modified version that adds low-light vision bonus for mutants/animals
+/mob/living/simple_animal/hostile/proc/get_effective_vision_range_lowlight(atom/target)
+ var/base_range = get_effective_vision_range(target)
+
+ if(!has_low_light_vision)
+ return base_range
+
+ // Check if it's actually dark
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return base_range
+
+ var/light_amount = target_turf.get_lumcount()
+
+ // Only apply low-light bonus in darkness
+ if(light_amount < 0.2)
+ // Add bonus to dark vision
+ return base_range + low_light_bonus
+
+ return base_range
+
+/// Determine which sound cone the target is in (for rear detection)
+/mob/living/simple_animal/hostile/proc/get_sound_cone(atom/target)
+ if(!target)
+ return null
+
+ var/turf/my_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
+ if(!my_turf || !target_turf)
+ return null
+
+ var/dx = target_turf.x - my_turf.x
+ var/dy = target_turf.y - my_turf.y
+
+ // ============================================
+ // QUADRANT-BASED SOUND DETECTION (matches visual sectors exactly)
+ // ============================================
+ // Sound detection only works in REAR zones (90° to 270° from facing)
+ //
+ // REAR CENTER (GRAY): 150° to 210° → SOUND_REAR_CENTER (harder to hear)
+ // REAR PERIPHERAL (CYAN): 90° to 150° and 210° to 270° → SOUND_REAR_PERIPHERAL (easier to hear)
+ // ============================================
+
+ // Get base direction angle
+ var/base_angle = dir_to_angle_detection(dir)
+
+ // Calculate angle to target using SAME quadrant logic
+ var/target_angle = calculate_angle_from_offset_detection(dx, dy)
+
+ // Calculate relative angle
+ var/relative_angle = target_angle - base_angle
+
+ // Normalize to -180 to +180
+ while(relative_angle > 180)
+ relative_angle -= 360
+ while(relative_angle < -180)
+ relative_angle += 360
+
+ // Check if target is in rear zones
+ if(relative_angle >= -90 && relative_angle <= 90)
+ return null // Front zones - no sound detection
+
+ // Target is behind - determine if center or peripheral
+ // Rear center: 150° to 210° (±30° from opposite = relative ±30° from 180°)
+ // Which is: relative >= 150 OR relative <= -150
+ var/abs_rel = abs(relative_angle)
+ if(abs_rel >= 150)
+ return SOUND_REAR_CENTER // Directly behind (harder to hear)
+ else
+ return SOUND_REAR_PERIPHERAL // Rear diagonal (easier to hear)
+
+// Helper functions for detection (identical to visual logic)
+/mob/living/simple_animal/hostile/proc/dir_to_angle_detection(byond_dir)
+ switch(byond_dir)
+ if(NORTH)
+ return 0
+ if(NORTHEAST)
+ return 45
+ if(EAST)
+ return 90
+ if(SOUTHEAST)
+ return 135
+ if(SOUTH)
+ return 180
+ if(SOUTHWEST)
+ return 225
+ if(WEST)
+ return 270
+ if(NORTHWEST)
+ return 315
+ return 0
+
+/mob/living/simple_animal/hostile/proc/calculate_angle_from_offset_detection(dx, dy)
+ var/angle = 0
+
+ // Cardinals (avoids arctan edge cases)
+ if(dx == 0)
+ if(dy > 0)
+ return 0 // NORTH
+ else
+ return 180 // SOUTH
+ else if(dy == 0)
+ if(dx > 0)
+ return 90 // EAST
+ else
+ return 270 // WEST
+
+ // Diagonals - use arctan on absolute values
+ var/acute_angle = arctan(abs(dy), abs(dx))
+
+ // Map to correct quadrant
+ if(dx > 0 && dy > 0)
+ angle = acute_angle // NE
+ else if(dx < 0 && dy > 0)
+ angle = 360 - acute_angle // NW
+ else if(dx > 0 && dy < 0)
+ angle = 180 - acute_angle // SE
+ else // dx < 0 && dy < 0
+ angle = 180 + acute_angle // SW
+
+ // Normalize
+ while(angle < 0)
+ angle += 360
+ while(angle >= 360)
+ angle -= 360
+
+ return angle
+
+/// Detect moving targets behind us via sound
+/mob/living/simple_animal/hostile/proc/detect_rear_movement(atom/target)
+ if(!target)
+ return 0
+
+ if(!isliving(target))
+ return 0
+
+ var/mob/living/L = target
+
+ // FIX 1: Check if they're BEHIND us first
+ var/sound_cone = get_sound_cone(target)
+ if(!sound_cone)
+ return 0 // Not behind us - no sound detection
+
+ // FIX 2: Check for recent movement
+ // Must have moved within last 2 seconds (20 deciseconds)
+ // This catches continuous walking/running between actual movement ticks
+ if(!L.last_move_time)
+ return 0 // Never moved
+
+ var/time_since_move = world.time - L.last_move_time
+
+ // 2 second window catches ongoing movement
+ if(time_since_move > 20)
+ return 0 // Stopped moving - no sound
+
+ // Get their movement sound level
+ var/sound_level = 1.0 // Default for non-humans
+ if(ishuman(L))
+ var/mob/living/carbon/human/H = L
+ sound_level = H.get_movement_sound_level()
+
+ // Calculate detection range based on zone
+ // REAR CENTER: Directly behind - sound travels straight, easier to hear
+ // REAR PERIPHERAL: Diagonal/side angles - slightly muffled by head position
+ var/detection_range = 0
+
+ if(sound_cone == SOUND_REAR_CENTER)
+ detection_range = round(sound_level * 3) // Directly behind - sound travels clearly
+ else // SOUND_REAR_PERIPHERAL
+ detection_range = round(sound_level * 2.5) // Diagonal - slightly muffled
+
+ return detection_range
+
/mob/living/simple_animal/hostile/proc/ListTargets()//Step 1, find out what we can see
+ if(stat == DEAD) // Dead mobs can't detect targets
+ return list()
if(!search_objects)
- . = hearers(vision_range, targets_from) - src
+ // CORE FIX: Filter by effective range BEFORE adding to list
+ . = list()
+
+ // Get all potential targets in max vision range
+ var/list/potential_targets = hearers(vision_range, targets_from) - src
+
+ for(var/atom/A in potential_targets)
+ // Skip non-attackable things early
+ if(!isliving(A) && !ismecha(A) && !istype(A, /obj/machinery/porta_turret))
+ continue
+
+ // VISION-BASED DETECTION
+ var/effective_range = get_effective_vision_range(A)
+
+ // Apply low-light vision bonus if applicable
+ if(has_low_light_vision)
+ effective_range = get_effective_vision_range_lowlight(A)
+
+ var/actual_distance = get_dist(src, A)
+
+ // Check if within visual range
+ if(actual_distance <= effective_range)
+ . += A
+ if(debug_vision && isliving(A))
+ var/mob/living/L = A
+ var/cone = get_vision_cone(L)
+ var/cone_name = "UNKNOWN"
+ switch(cone)
+ if(CONE_FRONT)
+ cone_name = "FRONT (90°)"
+ if(CONE_PERIPHERAL)
+ cone_name = "PERIPHERAL (45°)"
+ if(CONE_REAR)
+ cone_name = "REAR (blind)"
+ to_chat(L, span_notice("[src] detected you via VISION in [cone_name] cone at [actual_distance] tiles (max: [effective_range])"))
+ continue
+
+ // SOUND-BASED DETECTION (for targets behind us)
+ var/sound_range = detect_rear_movement(A)
+ if(sound_range > 0 && actual_distance <= sound_range)
+ // Heard movement! Turn to face the sound source
+ var/face_dir = get_dir(src, A)
+ if(face_dir && face_dir != dir)
+ setDir(face_dir)
+
+ // Check line-of-sight AND if they're within stealth-adjusted range
+ // Note: effective_range is already calculated above with stealth penalty
+ var/can_actually_see = can_see(src, A, effective_range) && (actual_distance <= effective_range)
+
+ if(!can_actually_see)
+ // Heard something, turned to check, but can't see it - enter search mode
+ if(!searching && !target)
+ last_known_location = get_turf(A)
+ remembered_target = A
+ last_target_sighting = world.time
+
+ // Wake up and enter search mode immediately
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+ enter_search_mode()
+
+ if(debug_vision && isliving(A))
+ var/mob/living/L = A
+ to_chat(L, span_warning("[src] heard you but can't see you (stealth: [effective_range] tiles) - searching!"))
+ var/sound_cone = get_sound_cone(L)
+ var/cone_name = "UNKNOWN"
+ switch(sound_cone)
+ if(SOUND_REAR_CENTER)
+ cone_name = "REAR CENTER (90°)"
+ if(SOUND_REAR_PERIPHERAL)
+ cone_name = "REAR PERIPHERAL (45°)"
+ to_chat(L, span_warning("[src] detected you via SOUND in [cone_name] cone at [actual_distance] tiles (max: [sound_range])"))
+ continue
+
+ // Successfully detected via sound - add as target
+ . += A
+ if(debug_vision && isliving(A))
+ var/mob/living/L = A
+ var/sound_cone = get_sound_cone(L)
+ var/cone_name = "UNKNOWN"
+ switch(sound_cone)
+ if(SOUND_REAR_CENTER)
+ cone_name = "REAR CENTER (90°)"
+ if(SOUND_REAR_PERIPHERAL)
+ cone_name = "REAR PERIPHERAL (45°)"
+ to_chat(L, span_notice("[src] detected you via SOUND in [cone_name] cone at [actual_distance] tiles (max: [sound_range])"))
- // Check for targets one Z-level ABOVE through openspace above us
+ // Check for targets one Z-level ABOVE through openspace
var/turf/our_turf = get_turf(targets_from)
var/turf/above_us = get_step_multiz(our_turf, UP)
if(above_us && istype(above_us, /turf/open/transparent/openspace))
- // We're under openspace, we can see up
for(var/mob/living/M in range(vision_range, above_us))
- // Make sure they're actually on the openspace (visible to us)
var/turf/their_turf = get_turf(M)
if(istype(their_turf, /turf/open/transparent/openspace))
- . += M
+ var/effective_range = get_effective_vision_range(M)
+ if(has_low_light_vision)
+ effective_range = get_effective_vision_range_lowlight(M)
+ var/actual_distance = get_dist(src, M)
+ if(actual_distance <= effective_range)
+ . += M
- // Check for targets one Z-level BELOW through openspace we're standing on/near
+ // Check for targets one Z-level BELOW through openspace
var/list/openspace_tiles = list()
if(istype(our_turf, /turf/open/transparent/openspace))
openspace_tiles += our_turf
@@ -366,42 +1509,86 @@
var/turf/their_turf = get_turf(M)
var/turf/above_them = get_step_multiz(their_turf, UP)
if(above_them && istype(above_them, /turf/open/transparent/openspace))
- . += M
+ var/effective_range = get_effective_vision_range(M)
+ if(has_low_light_vision)
+ effective_range = get_effective_vision_range_lowlight(M)
+ var/actual_distance = get_dist(src, M)
+ if(actual_distance <= effective_range)
+ . += M
+
+ // Add hostile machines that are in effective range
+ var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/destructible/clockwork/ocular_warden, /obj/item/electronic_assembly))
- var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/destructible/clockwork/ocular_warden,/obj/item/electronic_assembly))
-
for(var/HM in typecache_filter_list(range(vision_range, targets_from), hostile_machines))
CHECK_TICK
- if(can_see(targets_from, HM, vision_range))
+ var/effective_range = get_effective_vision_range(HM)
+ var/actual_distance = get_dist(src, HM)
+ if(actual_distance <= effective_range && can_see(targets_from, HM, effective_range))
. += HM
else
+ // Object search mode - also respect effective range
. = list()
- for (var/obj/A in oview(vision_range, targets_from))
+ for(var/obj/A in oview(vision_range, targets_from))
CHECK_TICK
- . += A
- for (var/mob/living/A in oview(vision_range, targets_from))
+ var/effective_range = get_effective_vision_range(A)
+ var/actual_distance = get_dist(src, A)
+ if(actual_distance <= effective_range)
+ . += A
+ for(var/mob/living/A in oview(vision_range, targets_from))
CHECK_TICK
- . += A
+ var/effective_range = get_effective_vision_range(A)
+ var/actual_distance = get_dist(src, A)
+ if(actual_distance <= effective_range)
+ if(ishuman(A))
+ if(!can_see(src, A, effective_range))
+ continue // No detection through walls, regardless of sneak mode
+ . += A
-/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets, HasTargetsList = 0)//Step 2, filter down possible targets to things we actually care about
+/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets, HasTargetsList = 0)
. = list()
- if (peaceful == FALSE)
+
+ if(peaceful == FALSE)
if(!HasTargetsList)
possible_targets = ListTargets()
+
+ // SEARCH MODE: If searching and target reappears in LOS, re-acquire
+ if(searching && target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target)))
+ exit_search_mode(found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = target
+ GiveTarget(target)
+ COOLDOWN_START(src, sight_shoot_delay, sight_shoot_delay_time)
+ call_for_backup(target, "found")
+ return target
+
+ // Normal target acquisition loop
for(var/pos_targ in possible_targets)
var/atom/A = pos_targ
- if(Found(A))//Just in case people want to override targetting
+ if(Found(A))
. = list(A)
break
- if(CanAttack(A))//Can we attack it?
+ if(CanAttack(A))
. += A
continue
+
var/Target = PickTarget(.)
+
+ // Check detection delay for sneaking targets
+ if(Target && !check_detection_delay(Target))
+ return // Still building detection, don't acquire yet
+
GiveTarget(Target)
COOLDOWN_START(src, sight_shoot_delay, sight_shoot_delay_time)
- return Target //We now have a target
+ if(Target)
+ remembered_target = Target
+ last_target_sighting = world.time
+ last_known_location = get_turf(Target)
+
+ if(!target || target != Target)
+ call_for_backup(Target, "found")
+ return Target
/mob/living/simple_animal/hostile/proc/PossibleThreats()
. = list()
@@ -414,8 +1601,6 @@
. += A
continue
-
-
/mob/living/simple_animal/hostile/proc/Found(atom/A)//This is here as a potential override to pick a specific target if available
return
@@ -527,21 +1712,81 @@
return FALSE
-/mob/living/simple_animal/hostile/proc/GiveTarget(new_target)//Step 4, give us our selected target
+/// Check if enough time has passed to detect a sneaking target
+/mob/living/simple_animal/hostile/proc/check_detection_delay(atom/candidate_target)
+ if(!candidate_target)
+ return FALSE
+
+ // Check if target is sneaking
+ var/target_sneaking = FALSE
+ if(ishuman(candidate_target))
+ var/mob/living/carbon/human/H = candidate_target
+ target_sneaking = H.sneaking
+
+ // Not sneaking = instant detection
+ if(!target_sneaking)
+ detection_progress = 0
+ detecting_target = null
+ return TRUE
+
+ // Sneaking = 3 second buildup (30 deciseconds)
+ var/required_time = 30
+
+ // Reset if we switched targets
+ if(detecting_target != candidate_target)
+ detection_progress = 0
+ detecting_target = candidate_target
+
+ // Start detection timer
+ if(!detection_progress)
+ detection_progress = world.time // store start time
+ return FALSE
+
+ var/elapsed = world.time - detection_progress
+ if(elapsed >= required_time)
+ // Fully detected!
+ detection_progress = 0
+ detecting_target = null
+ return TRUE
+
+ // Still building up - not detected yet
+ return FALSE
+
+/mob/living/simple_animal/hostile/proc/GiveTarget(new_target)
+ if(stat == DEAD) // Dead mobs can't acquire targets
+ return
add_target(new_target)
LosePatience()
if(target != null)
+ remembered_target = target
+ last_target_sighting = world.time
+ is_alpha_alerter = TRUE // We personally have a confirmed target
GainPatience()
Aggro()
return 1
+ else
+ is_alpha_alerter = FALSE // Lost target, no longer primary alerter
//What we do after closing in
/mob/living/simple_animal/hostile/proc/MeleeAction(patience = TRUE)
- if(rapid_melee > 1)
- var/datum/callback/cb = CALLBACK(src, PROC_REF(CheckAndAttack))
- var/delay = SSnpcpool.wait / rapid_melee
+ // Vision check before attempting melee
+ if(target)
+ var/effective_range = get_effective_vision_range(target)
+ if(effective_range == 0)
+ // Target is behind us - lose target
+ LoseTarget()
+ return
+ var/dist = get_dist(src, target)
+ if(dist > effective_range)
+ // Target is beyond our vision range - lose target
+ LoseTarget()
+ return
+
+ if(rapid_melee > 1)
+ var/datum/callback/cb = CALLBACK(src, PROC_REF(CheckAndAttack))
+ var/delay = SSnpcpool.wait / rapid_melee
for(var/i in 1 to rapid_melee)
- addtimer(cb, (i - 1)*delay)
+ addtimer(cb, (i - 1)*delay, TIMER_DELETE_ME)
else
AttackingTarget()
if(patience)
@@ -549,92 +1794,305 @@
/mob/living/simple_animal/hostile/proc/CheckAndAttack()
if(target && targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && !incapacitated())
+ // Vision check before rapid melee attack
+ var/effective_range = get_effective_vision_range(target)
+ if(effective_range == 0)
+ return // Can't see target (behind us)
+ var/dist = get_dist(src, target)
+ if(dist > effective_range)
+ return // Target beyond vision range
AttackingTarget()
-/mob/living/simple_animal/hostile/proc/MoveToTarget(list/possible_targets)//Step 5, handle movement between us and our target
+/mob/living/simple_animal/hostile/proc/MoveToTarget(list/possible_targets)
stop_automated_movement = 1
- if (peaceful == TRUE)
+
+ // Safety check: flatten nested lists if somehow we received one
+ if(possible_targets && islist(possible_targets))
+ for(var/item in possible_targets)
+ if(islist(item))
+ // We have a nested list - flatten it
+ var/list/flattened = list()
+ for(var/entry in possible_targets)
+ if(islist(entry))
+ // Add all items from the nested list
+ for(var/subitem in entry)
+ flattened += subitem
+ else
+ flattened += entry
+ possible_targets = flattened
+ log_runtime("HOSTILE MOB MoveToTarget: [src] ([type]) had nested list at [get_turf(src)]. Flattened to [flattened.len] entries.")
+ break
+
+ if(peaceful == TRUE)
+ LoseTarget()
+ return 0
+
+ // DOOR KITING CHECK: Track if target is exploiting door kiting
+ if(target)
+ check_target_door_kiting()
+
+ // If target is kiting and we're committed, stay on our side
+ if(committed_to_door_side && committed_door_location)
+ var/dist_from_commitment = get_dist(src, committed_door_location)
+
+ if(dist_from_commitment > 2)
+ // Moved too far from commitment point - return to it
+ Goto(committed_door_location, move_to_delay, 0)
+ return 1
+ else
+ // Stay here and wait for target to come to us
+ walk(src, 0)
+
+ // If target gets far enough away, reset commitment
+ if(get_dist(src, target) > 7)
+ committed_to_door_side = FALSE
+ committed_door_location = null
+ target_door_pass_count = 0
+ target_last_door = null
+
+ return 1
+
+ // CORNER KITING CHECK: Track if target is exploiting corner peeks
+ check_corner_kiting()
+
+ // If target is corner peeking, commit to rushing their position
+ if(committed_to_corner && corner_peek_location)
+ // Rush toward their last known peek location
+ Goto(corner_peek_location, move_to_delay, 0)
+
+ // Reset if we reach the corner or target gets far away
+ if(get_dist(src, corner_peek_location) <= 1 || (target && get_dist(src, target) > 10))
+ committed_to_corner = FALSE
+ corner_peek_location = null
+ corner_peek_count = 0
+
+ return 1
+
+ if(searching)
+ if(target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target)))
+ exit_search_mode(found_target = TRUE)
+ last_target_sighting = world.time
+ call_for_backup(target, "found")
+ else
+ return 1
+
+ if(rallying)
+ if(target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target)))
+ rallying = FALSE
+ rally_point = null
+ else if((world.time - rally_start_time) > rally_duration)
+ advance_after_rally()
+ else
+ return 1
+
+ if(!target)
+ if(remembered_target && !QDELETED(remembered_target) && last_known_location)
+ if((world.time - last_target_sighting) < target_memory_duration)
+ if(!searching)
+ enter_search_mode()
+ return 1
LoseTarget()
return 0
- if(!target || !CanAttack(target))
+
+ if(!CanAttack(target))
+ if(remembered_target && (world.time - last_target_sighting) < target_memory_duration)
+ if(!searching)
+ enter_search_mode()
+ return 1
LoseTarget()
return 0
- if(target in possible_targets)
- if(target.z != z)
+
+ var/has_los = (target in possible_targets) && can_see(src, target, get_effective_vision_range(target))
+
+ if(has_los)
+ last_target_sighting = world.time
+ remembered_target = target
+ last_known_location = get_turf(target)
+ else
+ var/time_since_seen = world.time - last_target_sighting
+ if(time_since_seen > 30)
+ if(!searching)
+ enter_search_mode()
+ var/saved_remembered = remembered_target
+ var/saved_location = last_known_location
+ var/saved_sighting = last_target_sighting
+ GiveTarget(null)
+ remembered_target = saved_remembered
+ last_known_location = saved_location
+ last_target_sighting = saved_sighting
+ return 1
+
+ // Z-PURSUIT
+ if(pursuing_z_target)
+ if(target && target.z == z)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ else if((world.time - z_pursuit_started) > z_pursuit_timeout)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+
+ if(pursuing_z_target && z_pursuit_structure && target && target.z != z)
+ var/dist = get_dist(src, z_pursuit_structure)
+ if(dist <= 1 && istype(z_pursuit_structure, /obj/structure/ladder))
+ walk(src, 0)
+ step(src, get_dir(src, z_pursuit_structure))
+ else
+ Goto(z_pursuit_structure, move_to_delay, 0)
+ return 1
+
+ // DIFFERENT Z LEVEL
+ if(target && target.z != z)
+ var/on_stairs = FALSE
+ for(var/obj/structure/stairs/S in loc)
+ on_stairs = TRUE
+ break
+
+ if((world.time - last_successful_z_move) < z_move_success_cooldown)
+ if(on_stairs)
+ Goto(target, move_to_delay, minimum_distance)
+ return 1
+
+ if(!on_stairs)
if(attempt_z_pursuit())
return 1
- LoseTarget()
- return 0
-
- var/target_distance = get_dist(targets_from, target)
-
- // Check if we THINK we're adjacent but target is on different Z
- if(targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from))
- // Verify they're actually on the same Z-level
- if(target.z != z)
- // Target is on different Z but appears adjacent (openspace issue)
- // Don't try to melee, just pursue
- if(attempt_z_pursuit())
- return 1
- LoseTarget()
- return 0
-
- if(ranged)
- if(!target.Adjacent(targets_from) && ranged_cooldown <= world.time)
- OpenFire(target)
-
- if(!Process_Spacemove())
- walk(src,0)
+
+ if(on_stairs)
+ Goto(target, move_to_delay, minimum_distance)
return 1
-
+
+ LoseTarget()
+ return 0
+
+ // DOOR OPENING toward target
+ if(can_open_doors && target)
+ var/turf/T = get_step(src, get_dir(src, target))
+ if(T)
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density)
+ try_open_door(SD)
+ for(var/obj/machinery/door/D in T)
+ if(D.density)
+ try_open_door(D)
+
+ if(!Process_Spacemove())
+ walk(src, 0)
+ return 1
+
+ var/target_distance = get_dist(targets_from, target)
+
+ // TARGET IN LOS - normal combat path
+ if(has_los)
+ // RANGED ATTACK
+ // Allow shooting even when adjacent if we can't maintain proper distance (tight spaces)
+ var/can_shoot = FALSE
+ if(ranged && ranged_cooldown <= world.time)
+ if(!target.Adjacent(targets_from))
+ can_shoot = TRUE // Normal case: not adjacent
+ else if(retreat_distance != null && target_distance < retreat_distance)
+ // We want to retreat but target is adjacent - shoot anyway (cornered)
+ can_shoot = TRUE
+ else if(target_distance <= 1 && minimum_distance > 0)
+ // Target is right on us but we want distance - shoot anyway (tight space)
+ can_shoot = TRUE
+
+ if(can_shoot)
+ var/fired = OpenFire(target)
+ if(!fired || is_shot_blocked(target))
+ if(retreat_distance != null && abs(target_distance - retreat_distance) <= 2)
+ var/turf/better_position = find_firing_position(target)
+ if(better_position)
+ Move(better_position, get_dir(src, better_position))
+ return 1
+
+ // MOVEMENT
if(retreat_distance != null)
- if(target_distance <= retreat_distance && CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE))
+ if(is_stuck && target_distance >= 2)
+ if(target_distance > 1)
+ Goto(target, move_to_delay, 1)
+ else
+ is_stuck = FALSE
+ stuck_position = null
+ stuck_time = 0
+ path_attempts = 0
+ else if(target_distance < retreat_distance && CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE))
set_glide_size(DELAY_TO_GLIDE_SIZE(move_to_delay))
- walk_away(src,target,retreat_distance,move_to_delay)
+ walk_away(src, target, retreat_distance, move_to_delay)
+ else if(target_distance > retreat_distance)
+ Goto(target, move_to_delay, retreat_distance)
else
- Goto(target,move_to_delay,minimum_distance)
+ walk(src, 0)
else
- Goto(target,move_to_delay,minimum_distance)
-
- if(variation_list[MOB_RETREAT_DISTANCE_CHANCE] && LAZYLEN(variation_list[MOB_RETREAT_DISTANCE]) && prob(variation_list[MOB_RETREAT_DISTANCE_CHANCE]))
- retreat_distance = vary_from_list(variation_list[MOB_RETREAT_DISTANCE])
-
- if(target)
- if(COOLDOWN_TIMELEFT(src, melee_cooldown))
- return TRUE
- COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown)
-
- // Only try melee if we're ACTUALLY adjacent (same Z-level)
- if(targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && target.z == z)
- MeleeAction()
+ if(target_distance > minimum_distance)
+ Goto(target, move_to_delay, minimum_distance)
else
- if(rapid_melee > 1 && target_distance <= melee_queue_distance)
- MeleeAction(FALSE)
- in_melee = FALSE
- return 1
- return 0
-
- if(environment_smash)
+ walk(src, 0)
+
+ // MELEE ATTACK
+ if(COOLDOWN_TIMELEFT(src, melee_cooldown))
+ return TRUE
+
+ // Safety check: target might have been cleared during movement/shooting
+ if(!target)
+ return 0
+
+ var/is_adjacent = targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && target.z == z
+
+ if(is_adjacent)
+ var/effective_combat_mode = combat_mode ? combat_mode : COMBAT_MODE_MELEE
+ var/should_melee = (effective_combat_mode == COMBAT_MODE_MELEE || effective_combat_mode == COMBAT_MODE_MIXED)
+
+ if(should_melee)
+ COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown)
+ MeleeAction()
+ else if(rapid_melee > 1 && target_distance <= melee_queue_distance)
+ var/effective_combat_mode = combat_mode ? combat_mode : COMBAT_MODE_MELEE
+ if(effective_combat_mode != COMBAT_MODE_RANGED)
+ COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown)
+ MeleeAction(FALSE)
+ else
+ in_melee = FALSE
+
+ return 1
+
+ // TARGET NOT IN LOS but we know roughly where they are (within grace period)
+ // Move toward last known position - this is the "searching while in combat" path
+ // ranged_ignores_vision mobs can still shoot
+ if(target && (environment_smash || can_open_doors || ranged_ignores_vision))
if(target.loc != null && get_dist(targets_from, target.loc) <= vision_range)
if(ranged_ignores_vision && ranged_cooldown <= world.time)
OpenFire(target)
- if((environment_smash & ENVIRONMENT_SMASH_WALLS) || (environment_smash & ENVIRONMENT_SMASH_RWALLS))
- Goto(target,move_to_delay,minimum_distance)
- FindHidden()
- return 1
- else
- if(FindHidden())
+
+ var/target_dist = get_dist(targets_from, target)
+ if(target_dist <= 3)
+ if((environment_smash & ENVIRONMENT_SMASH_WALLS) || (environment_smash & ENVIRONMENT_SMASH_RWALLS))
+ Goto(target, move_to_delay, minimum_distance)
+ FindHidden()
return 1
+ else
+ if(FindHidden())
+ return 1
+
+ // If we can open doors and target is nearby but not visible, move toward them
+ // Don't immediately give up after firing - pursue through the door
+ if(can_open_doors && target_dist <= vision_range)
+ Goto(target, move_to_delay, minimum_distance)
+ return 1
+
LoseTarget()
return 0
/mob/living/simple_animal/hostile/proc/Goto(target, delay, minimum_distance)
if(target == src.target)
approaching_target = TRUE
+ // FIX: Turn to face target while pursuing
+ var/face_dir = get_dir(src, target)
+ if(face_dir && face_dir != dir)
+ setDir(face_dir)
else
approaching_target = FALSE
if(CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE))
set_glide_size(DELAY_TO_GLIDE_SIZE(move_to_delay))
+ // Try pathfinding with walk_to - it handles basic pathing
walk_to(src, target, minimum_distance, delay)
if(variation_list[MOB_MINIMUM_DISTANCE_CHANCE] && LAZYLEN(variation_list[MOB_MINIMUM_DISTANCE]) && prob(variation_list[MOB_MINIMUM_DISTANCE_CHANCE]))
minimum_distance = vary_from_list(variation_list[MOB_MINIMUM_DISTANCE])
@@ -643,129 +2101,2133 @@
/mob/living/simple_animal/hostile/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
. = ..()
- if(!ckey && !stat && search_objects < 3 && . > 0)//Not unconscious, and we don't ignore mobs
- if(search_objects)//Turn off item searching and ignore whatever item we were looking at, we're more concerned with fight or flight
+ if(!ckey && !stat && search_objects < 3 && . > 0)
+ if(searching)
+ exit_search_mode(FALSE, found_target = TRUE)
+ visible_message(span_danger("[src] snaps to attention!"))
+ var/list/possible_targets = ListTargets()
+ var/new_target = null
+ // FIXED: Only acquire target we can actually see
+ for(var/atom/T in possible_targets)
+ if(can_see(src, T, get_effective_vision_range(T)) && CanAttack(T))
+ new_target = T
+ break
+ if(new_target)
+ GiveTarget(new_target)
+ remembered_target = new_target
+ last_target_sighting = world.time
+ last_known_location = get_turf(new_target)
+ else
+ // Was hurt but can't see attacker - keep searching, update memory
+ remembered_target = remembered_target // unchanged
+ // search_for_target() will keep running
+
+ else if(rallying)
+ rallying = FALSE
+ rally_point = null
+ visible_message(span_danger("[src] cancels retreat and fights back!"))
+ var/list/possible_targets = ListTargets()
+ var/new_target = null
+ // FIXED: LOS check here too
+ for(var/atom/T in possible_targets)
+ if(can_see(src, T, get_effective_vision_range(T)) && CanAttack(T))
+ new_target = T
+ break
+ if(new_target)
+ GiveTarget(new_target)
+ remembered_target = new_target
+ last_target_sighting = world.time
+ last_known_location = get_turf(new_target)
+ else
+ FindTarget()
+
+ else if(search_objects)
LoseTarget()
LoseSearchObjects()
+
if(AIStatus != AI_ON && AIStatus != AI_OFF)
toggle_ai(AI_ON)
FindTarget()
- else if(target != null && prob(40))//No more pulling a mob forever and having a second player attack it, it can switch targets now if it finds a more suitable one
+ else if(target != null && prob(40))
FindTarget()
+/// DOOR KITING PREVENTION: Track if target is passing through same door repeatedly
+/mob/living/simple_animal/hostile/proc/check_target_door_kiting()
+ if(!target || !isliving(target))
+ return FALSE
+
+ // Reset if it's been more than 5 seconds
+ if((world.time - target_last_door_pass_time) > 50)
+ target_door_pass_count = 0
+ target_last_door = null
+ committed_to_door_side = FALSE
+ committed_door_location = null
+ return FALSE
+
+ // Get target's current position
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf || !target_last_position)
+ target_last_position = target_turf
+ return FALSE
+
+ // Check if target moved
+ if(target_turf == target_last_position)
+ return committed_to_door_side
+
+ // Check if target passed through a door between last position and current position
+ var/atom/door_passed = null
+
+ // Check for doors at old position
+ for(var/obj/structure/simple_door/SD in target_last_position)
+ if(!SD.density) // Door is open
+ door_passed = SD
+ break
+ if(!door_passed)
+ for(var/obj/machinery/door/D in target_last_position)
+ if(!D.density) // Door is open
+ door_passed = D
+ break
+
+ // Check for doors at new position
+ if(!door_passed)
+ for(var/obj/structure/simple_door/SD in target_turf)
+ if(!SD.density)
+ door_passed = SD
+ break
+ if(!door_passed)
+ for(var/obj/machinery/door/D in target_turf)
+ if(!D.density)
+ door_passed = D
+ break
+
+ // Track door passage
+ if(door_passed)
+ if(door_passed == target_last_door)
+ // Target passed through same door again
+ if((world.time - target_last_door_pass_time) < 50) // Within 5 seconds
+ target_door_pass_count++
+ if(target_door_pass_count >= 3)
+ // Target is kiting! Commit to this side
+ if(!committed_to_door_side)
+ committed_to_door_side = TRUE
+ committed_door_location = get_turf(src)
+ visible_message(span_warning("[src] stops chasing through the doorway!"))
+ else
+ target_door_pass_count = 1 // Reset if too much time passed
+ else
+ // Different door
+ target_last_door = door_passed
+ target_door_pass_count = 1
+
+ target_last_door_pass_time = world.time
+
+ // Update last position
+ target_last_position = target_turf
+ return committed_to_door_side
+
+/// CORNER KITING PREVENTION: Track if target is peeking around same corner repeatedly
+/mob/living/simple_animal/hostile/proc/check_corner_kiting()
+ if(!target || !isliving(target))
+ return FALSE
+
+ // Reset if it's been more than 5 seconds
+ if((world.time - last_corner_peek_time) > 50)
+ corner_peek_count = 0
+ corner_peek_location = null
+ committed_to_corner = FALSE
+ return FALSE
+
+ // Check if we lost sight of target
+ var/has_los = can_see(src, target, get_effective_vision_range(target))
+
+ if(!has_los)
+ // Target disappeared - record as potential peek
+ var/turf/target_turf = get_turf(target)
+ if(target_turf)
+ // Check if this is same area as last peek
+ if(corner_peek_location && get_dist(corner_peek_location, target_turf) <= 2)
+ // Same corner area!
+ if((world.time - last_corner_peek_time) < 50) // Within 5 seconds
+ corner_peek_count++
+ if(corner_peek_count >= 3)
+ // Target is corner peeking! Commit to rushing
+ if(!committed_to_corner)
+ committed_to_corner = TRUE
+ visible_message(span_warning("[src] stops reacting to the corner peeks!"))
+ else
+ corner_peek_count = 1
+ else
+ // New corner location
+ corner_peek_location = target_turf
+ corner_peek_count = 1
+
+ last_corner_peek_time = world.time
+
+ return committed_to_corner
+
+/// BARRICADE FIX: Check if target is blocked by structure
+/mob/living/simple_animal/hostile/proc/is_target_blocked_by_structure(atom/A)
+ if(!A)
+ return FALSE
+
+ var/turf/source = get_turf(src)
+ var/turf/target_turf = get_turf(A)
+
+ if(!source || !target_turf)
+ return FALSE
+
+ // Get line to target
+ var/list/line_to_target = getline(source, target_turf)
+
+ // Check for blocking structures (NOT doors - doors can be opened)
+ for(var/turf/T in line_to_target)
+ if(T == source || T == target_turf)
+ continue
+
+ // Check for barricades, tables, windows, etc
+ for(var/obj/structure/S in T)
+ if(S.density)
+ // Skip doors - we handle those separately
+ if(istype(S, /obj/structure/simple_door))
+ continue
+ if(istype(S, /obj/machinery/door))
+ continue
+
+ // Found a blocking structure!
+ return TRUE
+
+ return FALSE
+
+/// LIGHT PATROL: Check for nearby players and start patrol
+/mob/living/simple_animal/hostile/proc/check_light_patrol()
+ // Don't patrol if already have target, searching, or currently patrolling
+ if(target || searching || AIStatus != AI_IDLE || light_patrolling || patrol_resting)
+ return
+
+ // Check if resting cooldown expired
+ if(patrol_resting)
+ if((world.time - patrol_rest_start) > patrol_rest_duration)
+ patrol_resting = FALSE
+ patrol_door_visits = list() // Reset visit counts
+ else
+ return // Still resting
+
+ // Look for players within 14 tiles
+ var/found_player = FALSE
+ for(var/mob/living/carbon/human/H in range(14, src))
+ if(H.stat == DEAD || H.ckey && !H.client)
+ continue
+
+ // Check if we can attack them (faction check)
+ if(!CanAttack(H))
+ continue
+
+ // Found a valid player!
+ found_player = TRUE
+ break
+
+ if(!found_player)
+ return
+
+ // Start light patrol!
+ start_light_patrol()
+
+/// LIGHT PATROL: Start patrolling
+/mob/living/simple_animal/hostile/proc/start_light_patrol()
+ light_patrolling = TRUE
+ patrol_home = get_turf(src)
+
+ // Find doors within 14 tiles
+ var/list/available_doors = list()
+
+ for(var/obj/structure/simple_door/SD in range(14, src))
+ // Skip doors we've visited too many times
+ var/visits = patrol_door_visits[SD] || 0
+ if(visits >= patrol_max_visits)
+ continue
+ available_doors += SD
+
+ for(var/obj/machinery/door/D in range(14, src))
+ var/visits = patrol_door_visits[D] || 0
+ if(visits >= patrol_max_visits)
+ continue
+ available_doors += D
+
+ // No doors available?
+ if(available_doors.len == 0)
+ // Move to furthest point within 7 tiles
+ patrol_to_furthest_point()
+ return
+
+ // Pick a random door
+ patrol_target_door = pick(available_doors)
+
+ // Record this visit
+ if(!patrol_door_visits[patrol_target_door])
+ patrol_door_visits[patrol_target_door] = 0
+ patrol_door_visits[patrol_target_door]++
+
+ visible_message(span_notice("[src] perks up and starts patrolling..."))
+
+ // Move toward the door (max 7 tiles)
+ patrol_to_target()
+
+/// LIGHT PATROL: Move toward patrol target
+/mob/living/simple_animal/hostile/proc/patrol_to_target()
+ if(!patrol_target_door || !patrol_home)
+ end_light_patrol()
+ return
+
+ // Calculate how far we can move (max 7 tiles from home)
+ var/dist_from_home = get_dist(src, patrol_home)
+ var/dist_to_door = get_dist(src, patrol_target_door)
+ var/tiles_remaining = 7 - dist_from_home
+
+ if(tiles_remaining <= 0)
+ // Reached max range - return home
+ return_to_patrol_home()
+ return
+
+ if(dist_to_door <= 1)
+ // Reached the door!
+ return_to_patrol_home()
+ return
+
+ // Move toward door with SLOW speed (patrol is slower)
+ var/patrol_delay = move_to_delay * 1.5 // 50% slower than normal
+ Goto(patrol_target_door, patrol_delay, 1)
+
+ // Check again in 2 seconds
+ addtimer(CALLBACK(src, PROC_REF(patrol_to_target)), 2 SECONDS, TIMER_DELETE_ME)
+
+/// LIGHT PATROL: Return to starting position
+/mob/living/simple_animal/hostile/proc/return_to_patrol_home()
+ if(!patrol_home)
+ end_light_patrol()
+ return
+
+ // Move back to home position
+ var/patrol_delay = move_to_delay * 1.5
+ Goto(patrol_home, patrol_delay, 0)
+
+ // Check if we're home
+ if(get_dist(src, patrol_home) <= 1)
+ end_light_patrol()
+ else
+ // Check again in 2 seconds
+ addtimer(CALLBACK(src, PROC_REF(return_to_patrol_home)), 2 SECONDS, TIMER_DELETE_ME)
+
+/// LIGHT PATROL: Move to furthest point when no doors available
+/mob/living/simple_animal/hostile/proc/patrol_to_furthest_point()
+ if(!patrol_home)
+ end_light_patrol()
+ return
+
+ // Find furthest open turf within 7 tiles
+ var/list/candidate_turfs = list()
+ for(var/turf/open/T in range(7, patrol_home))
+ if(!T.density)
+ candidate_turfs += T
+
+ if(candidate_turfs.len == 0)
+ end_light_patrol()
+ return
+
+ // Pick furthest turf
+ var/turf/furthest = null
+ var/furthest_dist = 0
+ for(var/turf/T in candidate_turfs)
+ var/dist = get_dist(patrol_home, T)
+ if(dist > furthest_dist)
+ furthest_dist = dist
+ furthest = T
+
+ if(!furthest)
+ end_light_patrol()
+ return
+
+ visible_message(span_notice("[src] patrols to a distant point..."))
+
+ // Move to furthest point
+ var/patrol_delay = move_to_delay * 1.5
+ Goto(furthest, patrol_delay, 0)
+
+ // Return home after reaching it
+ addtimer(CALLBACK(src, PROC_REF(return_to_patrol_home)), 5 SECONDS, TIMER_DELETE_ME)
+
+/// LIGHT PATROL: End patrol and possibly rest
+/mob/living/simple_animal/hostile/proc/end_light_patrol()
+ light_patrolling = FALSE
+ patrol_target_door = null
+ walk(src, 0) // Stop moving
+
+ // Check if we should start resting
+ var/all_doors_visited = TRUE
+ for(var/obj/structure/simple_door/SD in range(14, patrol_home))
+ var/visits = patrol_door_visits[SD] || 0
+ if(visits < patrol_max_visits)
+ all_doors_visited = FALSE
+ break
+
+ if(all_doors_visited)
+ for(var/obj/machinery/door/D in range(14, patrol_home))
+ var/visits = patrol_door_visits[D] || 0
+ if(visits < patrol_max_visits)
+ all_doors_visited = FALSE
+ break
+
+ if(all_doors_visited)
+ // All doors visited - rest at home
+ patrol_resting = TRUE
+ patrol_rest_start = world.time
+ visible_message(span_notice("[src] returns to its post."))
+
+// ========================================
+// THROWN ITEM DETECTION SYSTEM
+// ========================================
+// Track who's throwing items, investigate source if spammed
+
+/// Called when a thrown item lands near the mob
+/mob/living/simple_animal/hostile/proc/detect_thrown_item(obj/item/thrown_item, atom/thrower)
+ if(!thrown_item || !thrower)
+ return
+
+ // Don't react if we already have a target
+ if(target)
+ return
+
+ // Clear old entries (older than 10 seconds)
+ if((world.time - last_thrown_clear) > 100)
+ recent_thrown_items = list()
+ last_thrown_clear = world.time
+
+ // Add this throw to our memory
+ if(!recent_thrown_items[thrower])
+ recent_thrown_items[thrower] = 0
+ recent_thrown_items[thrower]++
+
+ // Check if this thrower is spamming
+ if(recent_thrown_items[thrower] >= thrown_spam_threshold)
+ // SPAM DETECTED - investigate the source (thrower)
+ if(CanAttack(thrower))
+ visible_message(span_warning("[src] notices someone throwing things!"))
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Enter search mode targeting the thrower
+ last_known_location = get_turf(thrower)
+ remembered_target = thrower
+ last_target_sighting = world.time
+
+ if(!searching)
+ enter_search_mode()
+
+ // Reset spam counter for this thrower
+ recent_thrown_items[thrower] = 0
+ else
+ // First or second throw - investigate the ITEM, not the thrower
+ var/turf/item_location = get_turf(thrown_item)
+
+ if(item_location && !searching)
+ visible_message(span_notice("[src] looks toward [thrown_item]..."))
+
+ // End light patrol if active
+ if(light_patrolling)
+ end_light_patrol()
+
+ // Wake up
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Set investigation state to prevent LoseTarget() from stopping movement
+ last_known_location = item_location
+ remembered_target = thrower // Remember who threw it
+ last_target_sighting = world.time // Mark as recent
+
+ // Move toward the item to investigate (adjacent)
+ Goto(item_location, move_to_delay, 0)
+
+// ========================================
+// LIGHT DESTRUCTION DETECTION SYSTEM
+// ========================================
+// Investigate shot-out lights, or the shooter if spamming
+
+/// Called when a light is destroyed nearby
+/mob/living/simple_animal/hostile/proc/detect_light_destruction(turf/destruction_location, atom/shooter, silenced = FALSE)
+ if(!destruction_location)
+ return
+
+ // Don't react if we already have a target
+ if(target)
+ return
+
+ // Clear old entries (older than 10 seconds)
+ if((world.time - last_light_shot_clear) > 100)
+ recent_light_shots = list()
+ last_light_shot_clear = world.time
+
+ // Add this destruction to our memory (if we know the shooter)
+ if(shooter)
+ if(!recent_light_shots[shooter])
+ recent_light_shots[shooter] = 0
+ recent_light_shots[shooter]++
+
+ // Check if shooter is spamming (shot 3+ lights)
+ if(shooter && recent_light_shots[shooter] >= light_spam_threshold)
+ // SPAM DETECTED - investigate the SHOOTER
+ if(CanAttack(shooter))
+ visible_message(span_warning("[src] notices someone shooting out lights!"))
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Enter search mode targeting the shooter
+ last_known_location = get_turf(shooter)
+ remembered_target = shooter
+ last_target_sighting = world.time
+
+ if(!searching)
+ enter_search_mode()
+
+ // Reset spam counter
+ recent_light_shots[shooter] = 0
+ else
+ // First, second shot, OR we don't know who shot it
+
+ if(silenced)
+ // Silenced weapon - only investigate if very close (3 tiles)
+ // Investigate the LIGHT LOCATION since we can't hear the shooter
+ if(get_dist(src, destruction_location) > 3)
+ return
+
+ visible_message(span_notice("[src] notices a light going out..."))
+
+ // End light patrol if active
+ if(light_patrolling)
+ end_light_patrol()
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Set investigation state to prevent LoseTarget() from stopping movement
+ last_known_location = destruction_location
+ if(shooter)
+ remembered_target = shooter
+ last_target_sighting = world.time
+
+ // Move to investigate the light location (adjacent)
+ if(!searching)
+ Goto(destruction_location, move_to_delay, 0)
+ else
+ // NOT silenced - heard the gunshot!
+ // Natural response: investigate the SHOOTER, not the light
+ if(shooter && CanAttack(shooter))
+ var/turf/shooter_location = get_turf(shooter)
+
+ visible_message(span_warning("[src] heard that shot!"))
+
+ // End light patrol if active
+ if(light_patrolling)
+ end_light_patrol()
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Set investigation state to prevent LoseTarget() from stopping movement
+ last_known_location = shooter_location
+ remembered_target = shooter
+ last_target_sighting = world.time
+
+ // Move directly toward the shooter (adjacent)
+ if(!searching)
+ Goto(shooter_location, move_to_delay, 0)
+ else
+ // Don't know the shooter - investigate the light location as fallback
+ visible_message(span_notice("[src] notices a light going out..."))
+
+ // End light patrol if active
+ if(light_patrolling)
+ end_light_patrol()
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // Set investigation state to prevent LoseTarget() from stopping movement
+ last_known_location = destruction_location
+ last_target_sighting = world.time
+
+ // Move to investigate the light location (adjacent)
+ if(!searching)
+ Goto(destruction_location, move_to_delay, 0)
+
/mob/living/simple_animal/hostile/proc/AttackingTarget()
+ // VISION CHECK: Don't attack if we can't see the target
+ if(target)
+ var/effective_range = get_effective_vision_range(target)
+ // If effective range is 0, target is completely out of vision (behind us)
+ if(effective_range == 0)
+ if(isliving(target))
+ var/mob/living/L = target
+ if(L.client)
+ to_chat(L, span_notice("DEBUG: [src] cannot attack - you are behind them (range=0)"))
+ return FALSE
+
+ // Check if target is within our effective vision range
+ var/dist = get_dist(src, target)
+ if(dist > effective_range)
+ if(isliving(target))
+ var/mob/living/L = target
+ if(L.client)
+ to_chat(L, span_notice("DEBUG: [src] cannot attack - beyond vision range (dist=[dist], range=[effective_range])"))
+ return FALSE
+
SEND_SIGNAL(src, COMSIG_HOSTILE_ATTACKINGTARGET, target)
in_melee = TRUE
if(prob(alternate_attack_prob) && AlternateAttackingTarget(target))
return FALSE
return target.attack_animal(src)
-/// Does an extra *thing* when attacking. Return TRUE to not do the standard attack
-/mob/living/simple_animal/hostile/proc/AlternateAttackingTarget(atom/the_target)
- return
+/// Does an extra *thing* when attacking. Return TRUE to not do the standard attack
+/mob/living/simple_animal/hostile/proc/AlternateAttackingTarget(atom/the_target)
+ return
+
+/mob/living/simple_animal/hostile/proc/Aggro()
+ if(ckey)
+ return TRUE
+
+ // Face the target when we acquire them
+ if(target && !QDELETED(target))
+ var/face_dir = get_dir(src, target)
+ if(face_dir && face_dir != dir)
+ setDir(face_dir)
+
+ vision_range = aggro_vision_range
+ if(target && LAZYLEN(emote_taunt) && prob(taunt_chance))
+ INVOKE_ASYNC(src, PROC_REF(emote), "me", EMOTE_VISIBLE, "[pick(emote_taunt)] at [target].")
+ taunt_chance = max(taunt_chance-7,2)
+ if(LAZYLEN(emote_taunt_sound))
+ var/taunt_choice = pick(emote_taunt_sound)
+ playsound(loc, taunt_choice, 50, 0, vary = FALSE, frequency = SOUND_FREQ_NORMALIZED(sound_pitch, vary_pitches[1], vary_pitches[2]))
+
+
+/mob/living/simple_animal/hostile/proc/LoseAggro()
+ stop_automated_movement = 0
+ vision_range = initial(vision_range)
+ taunt_chance = initial(taunt_chance)
+
+/mob/living/simple_animal/hostile/proc/LoseTarget()
+ // Check if we should clear memory due to target being crit/dead
+ if(remembered_target && !QDELETED(remembered_target) && isliving(remembered_target))
+ var/mob/living/L = remembered_target
+ if(L.stat == DEAD || L.stat == UNCONSCIOUS)
+ // Target went crit/dead - clear memory immediately
+ remembered_target = null
+ last_target_sighting = 0
+ if(searching)
+ exit_search_mode(give_up_message = TRUE, found_target = FALSE)
+
+ // Z-pursuit check
+ if(target && can_z_move && isliving(target))
+ var/mob/living/L = target
+ if(L.z != z)
+ remembered_target = L
+ last_target_sighting = world.time
+ last_known_location = get_turf(L)
+
+ if((world.time - last_z_move_attempt) < 50)
+ return
+
+ if(attempt_z_pursuit())
+ return
+
+ // Memory expired or no memory - truly give up
+ if(remembered_target && (world.time - last_target_sighting) > target_memory_duration && !searching)
+ remembered_target = null
+
+ // If memory still valid, enter search instead of fully giving up
+ if(remembered_target && (world.time - last_target_sighting) < target_memory_duration)
+ if(!searching)
+ enter_search_mode()
+ // Still clear the combat target
+ GiveTarget(null)
+ approaching_target = FALSE
+ in_melee = FALSE
+ walk(src, 0)
+ LoseAggro()
+ return
+
+ // Full give-up
+ if(searching)
+ exit_search_mode(give_up_message = FALSE, found_target = FALSE)
+
+ if(rallying)
+ rallying = FALSE
+ rally_point = null
+
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ remembered_target = null
+ last_known_location = null
+ is_stuck = FALSE
+ stuck_position = null
+ stuck_time = 0
+ path_attempts = 0
+
+ GiveTarget(null)
+ approaching_target = FALSE
+ in_melee = FALSE
+ walk(src, 0)
+ LoseAggro()
+
+// Z-LEVEL PURSUIT SYSTEM
+// This proc handles mobs chasing targets up/down stairs and ladders
+// Called when target is on a different Z-level than the mob
+/mob/living/simple_animal/hostile/proc/attempt_z_pursuit()
+ if(!target || !can_z_move) // Master toggle
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ // Check Z-level commitment - don't pursue if committed to current Z
+ if(!should_change_z_level())
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ // Prevent stair loop - don't pursue if we just changed Z-levels
+ if((world.time - last_successful_z_move) < z_move_success_cooldown)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ var/mob/living/L = target
+ if(!istype(L))
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ var/went_up = (L.z > z)
+ var/went_down = (L.z < z)
+
+ // Don't pursue if target is on same Z (prevents loop when they just climbed)
+ if(L.z == z)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ // STEP 1: Check if we're standing ON a ladder right now
+ // If yes, climb immediately without pathfinding (if we can climb ladders)
+ if(can_climb_ladders)
+ var/obj/structure/ladder/unbreakable/current_ladder = locate() in loc
+ if(current_ladder)
+ if(went_up && current_ladder.up)
+ visible_message(span_warning("[src] climbs up the ladder after [target]!"))
+ zMove(target = get_turf(current_ladder.up), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED)
+ last_z_move_attempt = world.time
+ last_successful_z_move = world.time
+ pursuing_z_target = FALSE // We climbed! Clear pursuit state
+ z_pursuit_structure = null
+ record_z_level_change()
+ return TRUE
+ else if(went_down && current_ladder.down)
+ visible_message(span_warning("[src] climbs down the ladder after [target]!"))
+ zMove(target = get_turf(current_ladder.down), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED)
+ last_z_move_attempt = world.time
+ last_successful_z_move = world.time
+ pursuing_z_target = FALSE // We climbed! Clear pursuit state
+ z_pursuit_structure = null
+ record_z_level_change()
+ return TRUE
+
+ // STEP 2: Check if we're near stairs (reduces cooldown)
+ var/near_stairs = FALSE
+ if(can_climb_stairs && went_up)
+ for(var/obj/structure/stairs/S in view(vision_range, src))
+ if(S.isTerminator())
+ var/dist = get_dist(src, S)
+ if(dist <= 2) // Within 2 tiles of stairs
+ near_stairs = TRUE
+ break
+
+ // STEP 3: Cooldown check (can be bypassed if near stairs)
+ if(world.time < last_z_move_attempt + z_move_delay)
+ if(near_stairs)
+ // Very short cooldown when near stairs
+ if(world.time < last_z_move_attempt + 2)
+ return pursuing_z_target // Return current state
+ else
+ return pursuing_z_target // Return current state
+
+ last_z_move_attempt = world.time
+ target_last_z = L.z
+
+ if(!went_up && !went_down)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ // STEP 4: Find Z-level transition structures (stairs, ladders, openspace)
+ // Only include structures we're actually allowed to use
+ var/list/z_structures = list()
+
+ if(went_up)
+ // Target went UP - look for stairs and ladders going up
+ if(can_climb_stairs)
+ for(var/obj/structure/stairs/S in view(vision_range, src))
+ if(S.isTerminator())
+ z_structures += S
+
+ if(can_climb_ladders)
+ for(var/obj/structure/ladder/LD in view(vision_range, src))
+ if(LD.up)
+ z_structures += LD
+
+ else if(went_down)
+ // Target went DOWN - look for openspace and ladders going down
+ if(can_jump_down)
+ var/turf/our_turf = get_turf(src)
+
+ if(istype(our_turf, /turf/open/transparent/openspace))
+ z_structures += our_turf
+
+ for(var/turf/open/transparent/openspace/OS in view(vision_range, src))
+ z_structures += OS
+
+ if(can_climb_ladders)
+ for(var/obj/structure/ladder/LD in view(vision_range, src))
+ if(LD.down)
+ z_structures += LD
+
+ if(!z_structures.len)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ // STEP 5: Pick nearest structure and enter Z-pursuit state
+ var/atom/nearest = get_closest_atom(/atom, z_structures, src)
+
+ // Enter Z-pursuit state - this locks the mob into climbing mode
+ pursuing_z_target = TRUE
+ z_pursuit_structure = nearest
+ z_pursuit_started = world.time
+
+ visible_message(span_danger("[src] pursues [target] [went_up ? "upward" : "downward"]!"))
+
+ // CALL FOR BACKUP - alert allies about Z-level pursuit
+ call_for_backup(target, "found")
+
+ LosePatience()
+ GainPatience()
+ COOLDOWN_RESET(src, sight_shoot_delay)
+
+ // STEP 6: Handle different structure types
+
+ // STAIRS - special handling for multi-tile climb (only if we can climb stairs)
+ if(went_up && can_climb_stairs && istype(nearest, /obj/structure/stairs))
+ var/obj/structure/stairs/S = nearest
+ var/turf/our_turf = get_turf(src)
+ var/turf/stairs_turf = get_turf(S)
+ var/dist = get_dist(src, S)
+
+ if(our_turf == stairs_turf)
+ // Already on stairs - climb
+ var/step_result = step(src, S.dir)
+ if(!step_result)
+ // Climb blocked, set micro-cooldown
+ last_z_move_attempt = world.time - (z_move_delay - 2)
+ else if(dist <= 1)
+ // Adjacent - try to step on
+ walk(src, 0)
+ var/dir_to_stairs = get_dir(src, S)
+ var/step_result = step(src, dir_to_stairs)
+
+ if(step_result && get_turf(src) == stairs_turf)
+ addtimer(CALLBACK(src, PROC_REF(climb_stairs), S), 1, TIMER_DELETE_ME)
+ else if(!step_result)
+ last_z_move_attempt = world.time - (z_move_delay - 2)
+ return TRUE
+ else
+ // Use Goto for pathfinding
+ Goto(S, move_to_delay, 0)
+
+ // LADDERS - aggressive stepping to ensure mob gets ON the ladder tile (only if we can climb ladders)
+ else if(can_climb_ladders && istype(nearest, /obj/structure/ladder))
+ var/obj/structure/ladder/L_target = nearest
+ var/dist = get_dist(src, L_target)
+
+ // Check if already on ladder tile
+ var/obj/structure/ladder/on_ladder = locate() in loc
+ if(on_ladder)
+ // Already on ladder, next tick will detect and climb
+ return TRUE
+
+ if(dist == 0)
+ // Somehow on ladder tile without locate() finding it - next tick will climb
+ return TRUE
+ else if(dist == 1)
+ // Adjacent to ladder - AGGRESSIVELY step onto it
+ walk(src, 0) // Stop any pathfinding
+ step(src, get_dir(src, L_target)) // Force step onto ladder
+ // Next AI tick will detect we're on ladder and climb
+ else
+ // Still far away - pathfind toward ladder
+ Goto(L_target, move_to_delay, 0)
+
+ // OPENSPACE/OTHER - generic pathfinding (only if we can jump down)
+ else if(can_jump_down)
+ Goto(nearest, move_to_delay, 0)
+ else
+ // We can't use this structure type, give up
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ return FALSE
+
+ return TRUE
+
+// Helper proc for delayed stair climbing
+/mob/living/simple_animal/hostile/proc/climb_stairs(obj/structure/stairs/S)
+ if(!S || QDELETED(S) || QDELETED(src))
+ return
+
+ var/turf/our_turf = get_turf(src)
+ var/turf/stairs_turf = get_turf(S)
+
+ if(our_turf != stairs_turf)
+ return
+
+ step(src, S.dir)
+
+ // Mark this as a successful Z-move to prevent immediate loop
+ record_z_level_change() // Changed from last_successful_z_move = world.time
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+
+// SEARCH MODE - actively patrol and look for lost target
+/mob/living/simple_animal/hostile/proc/enter_search_mode()
+ if(searching)
+ return
+
+ // Don't start searching if the remembered target is dead OR CRIT
+ if(remembered_target && !QDELETED(remembered_target) && isliving(remembered_target))
+ var/mob/living/L = remembered_target
+ if(L.stat == DEAD || L.stat == UNCONSCIOUS) // FIX: Include UNCONSCIOUS (crit)
+ // Target is dead/crit, no point searching
+ remembered_target = null
+ last_target_sighting = 0
+ return
+
+ if(last_search_exit_time > 0 && (world.time - last_search_exit_time) < search_entry_cooldown)
+ if(!remembered_target || (world.time - last_target_sighting) > target_memory_duration)
+ return
+
+ searching = TRUE
+
+ if(target)
+ last_known_location = get_turf(target)
+ else if(remembered_target && !QDELETED(remembered_target))
+ last_known_location = get_turf(remembered_target)
+
+ searched_turfs = list()
+ searched_doors = list()
+ searched_containers = list()
+ search_expansion = 0
+ search_start_time = world.time
+ recently_opened_door = null
+ investigating_container = null
+
+ visible_message(span_warning("[src] looks around suspiciously..."))
+
+ if(is_alpha_alerter)
+ if(target)
+ call_for_backup(target, "lost")
+ else if(remembered_target && !QDELETED(remembered_target))
+ call_for_backup(remembered_target, "lost")
+
+ // Schedule search asynchronously to avoid blocking call in signal handlers
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 0.5 SECONDS, TIMER_DELETE_ME)
+
+/mob/living/simple_animal/hostile/proc/exit_search_mode(give_up_message = TRUE, found_target = FALSE)
+ if(!searching)
+ return
+
+ searching = FALSE
+ last_search_exit_time = world.time
+
+ searched_turfs = list()
+ searched_doors = list()
+ searched_containers = list()
+ search_expansion = 0
+ recently_opened_door = null
+ investigating_container = null
+ search_start_time = 0
+
+ if(!found_target)
+ // Gave up - no longer tracking this threat
+ is_alpha_alerter = FALSE
+ remembered_target = null
+ last_target_sighting = 0
+ if(give_up_message)
+ visible_message(span_notice("[src] gives up the search."))
+ LoseTarget()
+ else
+ visible_message(span_danger("[src] resumes the hunt!"))
+ // is_alpha_alerter stays as-is - if we found the target we may now broadcast
+
+/mob/living/simple_animal/hostile/proc/search_for_target()
+ if(!searching || QDELETED(src))
+ return
+
+ // Check if remembered_target is dead/invalid - if so, give up search
+ if(remembered_target)
+ if(QDELETED(remembered_target))
+ // Target deleted entirely
+ visible_message(span_notice("[src] gives up the search."))
+ exit_search_mode(FALSE)
+ return
+
+ if(isliving(remembered_target))
+ var/mob/living/L = remembered_target
+ if(L.stat == DEAD)
+ // Target died - no point searching for a corpse
+ visible_message(span_notice("[src] gives up the search."))
+ exit_search_mode(FALSE)
+ return
+
+ // HARD CHECK: Valid, visible, ATTACKABLE target right now?
+ var/list/immediate_targets = ListTargets()
+ if(immediate_targets && immediate_targets.len > 0)
+ for(var/atom/possible_target in immediate_targets)
+ // BUG 1 FIX: CanAttack() gates faction member targeting
+ // BUG 3 FIX: Explicit living+stat check catches ghosts that slip past CanAttack
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client) // Dead or ghosted
+ continue
+ if(!can_see(src, possible_target, get_effective_vision_range(possible_target)))
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ visible_message(span_danger("[src] spots [possible_target]!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ // TIME-BASED TIMEOUT
+ if(search_start_time > 0)
+ var/elapsed_time = world.time - search_start_time
+ var/timeout_threshold = search_timeout_base
+
+ if(is_low_health || (last_target_sighting > 0 && (world.time - last_target_sighting) < 600))
+ timeout_threshold = search_timeout_aggressive
+
+ if(elapsed_time > timeout_threshold)
+ visible_message(span_notice("[src] gives up the search."))
+ exit_search_mode(FALSE)
+ return
+
+ if(!last_known_location || !CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE))
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME)
+ return
+
+ var/turf/current_pos = get_turf(src)
+ var/dist_to_last_known = get_dist(current_pos, last_known_location)
+
+ // PRIORITY 0: Move to last_known_location if we're far from it
+ // This ensures we search where the player was last seen, not random areas
+ if(dist_to_last_known > 3)
+ // Check for doors blocking the path to last_known_location
+ if(can_open_doors)
+ var/dir_to_target = get_dir(src, last_known_location)
+ var/turf/T = get_step(src, dir_to_target)
+ if(T)
+ // Check for doors in the next step toward target
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density && !(SD in searched_doors))
+ searched_doors += SD
+ if(try_open_door(SD))
+ visible_message(span_notice("[src] opens [SD] to continue pursuit..."))
+
+ // Check if target is now visible after opening door
+ sleep(5)
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!can_see(src, possible_target, get_effective_vision_range(possible_target)))
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ visible_message(span_danger("[src] spots [possible_target]!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME)
+ return
+ for(var/obj/machinery/door/D in T)
+ if(D.density && !(D in searched_doors))
+ searched_doors += D
+ if(try_open_door(D))
+ visible_message(span_notice("[src] opens [D] to continue pursuit..."))
+
+ // Check if target is now visible after opening door
+ sleep(5)
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!can_see(src, possible_target, get_effective_vision_range(possible_target)))
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ visible_message(span_danger("[src] spots [possible_target]!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME)
+ return
+
+ // We're too far from where we last saw them - move closer first
+ if(!(last_known_location in searched_turfs))
+ searched_turfs += last_known_location
+ visible_message(span_warning("[src] moves to investigate the last known position..."))
+ Goto(last_known_location, move_to_delay, 0)
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME)
+ return
+
+ // Special case: Very close to last_known_location - do thorough immediate area check
+ // This catches targets standing right at/near the door or last known position
+ if(dist_to_last_known <= 1)
+ var/list/close_range_targets = ListTargets()
+ if(close_range_targets && close_range_targets.len > 0)
+ for(var/atom/possible_target in close_range_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+
+ // More lenient check when very close - check actual distance
+ var/dist_to_target = get_dist(src, possible_target)
+ if(dist_to_target <= 3) // Within close range
+ if(CanAttack(possible_target))
+ visible_message(span_danger("[src] spots [possible_target] nearby!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ // Now we're close to last_known_location - search systematically around it
+ var/current_radius = search_radius + (search_expansion * 2)
+ if(current_radius > 15)
+ current_radius = 15
+
+ // PRIORITY 1: Check BOTH doors AND containers, investigate closest one
+ // This fixes the issue where mobs would check all doors before any containers
+ if(can_open_doors && last_known_location)
+ // Ensure last_known_location is a proper turf
+ if(!isturf(last_known_location))
+ last_known_location = get_turf(last_known_location)
+
+ if(last_known_location)
+ var/list/hiding_spots = list() // Combined list of doors and containers
+
+ // Collect unchecked doors
+ for(var/obj/structure/simple_door/SD in range(current_radius, last_known_location))
+ if(!(SD in searched_doors) && SD.density)
+ hiding_spots += SD
+ for(var/obj/machinery/door/D in range(current_radius, last_known_location))
+ if(!(D in searched_doors) && D.density)
+ hiding_spots += D
+
+ // Collect unchecked containers
+ for(var/obj/structure/closet/C in range(current_radius, last_known_location))
+ if(!(C in searched_containers) && !C.opened && !C.welded && !C.locked)
+ hiding_spots += C
+
+ // Find CLOSEST hiding spot (door or container)
+ if(hiding_spots.len > 0)
+ var/atom/closest_spot = null
+ var/closest_dist = 999
+ for(var/atom/spot in hiding_spots)
+ var/dist = get_dist(src, spot)
+ if(dist < closest_dist)
+ closest_dist = dist
+ closest_spot = spot
+
+ // Handle the closest hiding spot
+ if(closest_spot)
+ // Is it a container?
+ if(istype(closest_spot, /obj/structure/closet))
+ var/obj/structure/closet/C = closest_spot
+
+ if(get_dist(src, C) <= 1) // Adjacent - open it NOW
+ searched_containers += C
+ visible_message(span_warning("[src] searches [C]..."))
+
+ // Check container contents BEFORE opening
+ for(var/atom/A in C.contents)
+ if(isliving(A))
+ var/mob/living/L = A
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(CanAttack(L))
+ // Found someone hiding!
+ C.open(src)
+ sleep(5)
+ visible_message(span_danger("[src] found [L] hiding in [C]!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = L
+ GiveTarget(L)
+ last_known_location = get_turf(L)
+ walk(src, 0)
+ call_for_backup(L, "found")
+ return
+
+ // No one inside - open and check nearby
+ C.open(src)
+ sleep(5)
+
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ if(get_dist(possible_target, C) <= 1)
+ visible_message(span_danger("[src] found [possible_target] near [C]!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ // Container was empty - continue searching
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME)
+ return
+ else // Not adjacent - move toward it
+ Goto(C, move_to_delay, 0)
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1.5 SECONDS, TIMER_DELETE_ME)
+ return
+
+ // Is it a door?
+ else
+ if(get_dist(src, closest_spot) <= 1) // Adjacent - open it
+ searched_doors += closest_spot
+ if(try_open_door(closest_spot))
+ recently_opened_door = closest_spot
+ sleep(5)
+
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!can_see(src, possible_target, get_effective_vision_range(possible_target)))
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ visible_message(span_danger("[src] spots [possible_target] behind the door!"))
+ exit_search_mode(FALSE, found_target = TRUE)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ return
+
+ // No visible target - continue searching
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME)
+ return
+ else // Not adjacent - move toward it
+ Goto(closest_spot, move_to_delay, 0)
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1.5 SECONDS, TIMER_DELETE_ME)
+ return
+
+ // All hiding spots checked
+
+ // PRIORITY 2: Explore unsearched tiles around last_known_location
+ var/list/unexplored_turfs = list()
+
+ // Search around last_known_location, not our current position
+ for(var/turf/T in range(current_radius, last_known_location))
+ if(T in searched_turfs)
+ continue
+ if(!istype(T, /turf/open))
+ continue
+
+ // Prioritize tiles near recently opened doors
+ if(recently_opened_door)
+ var/turf/door_turf = get_turf(recently_opened_door)
+ if(get_dist(T, door_turf) <= 2)
+ unexplored_turfs.Insert(1, T)
+ continue
+
+ // PERFORMANCE: Direct door check instead of turf iteration
+ var/near_unopened_door = FALSE
+ for(var/obj/structure/simple_door/SD in orange(1, T))
+ if(SD.density && !(SD in searched_doors))
+ near_unopened_door = TRUE
+ break
+ if(!near_unopened_door)
+ for(var/obj/machinery/door/D in orange(1, T))
+ if(D.density && !(D in searched_doors))
+ near_unopened_door = TRUE
+ break
+
+ if(near_unopened_door)
+ unexplored_turfs.Insert(1, T)
+ else
+ unexplored_turfs += T
+
+ // Move to unexplored area
+ if(unexplored_turfs.len)
+ var/turf/search_target = null
+
+ // Pick from top candidates, preferring ones closer to last_known_location
+ var/candidates_count = min(5, unexplored_turfs.len)
+ var/list/candidate_turfs = unexplored_turfs.Copy(1, candidates_count + 1)
+
+ // Score by distance to last_known_location (closer = better)
+ var/best_score = 999
+ for(var/turf/T in candidate_turfs)
+ var/score = get_dist(T, last_known_location)
+
+ // Small randomization to avoid perfectly synchronized movement
+ score += rand(-2, 2)
+
+ if(score < best_score)
+ best_score = score
+ search_target = T
+
+ if(!search_target)
+ search_target = pick(candidate_turfs)
+
+ // Clear recently_opened_door if we've moved past it
+ if(recently_opened_door)
+ var/turf/door_turf = get_turf(recently_opened_door)
+ if(door_turf && get_dist(current_pos, door_turf) > 3)
+ recently_opened_door = null
+
+ // Move to target
+ searched_turfs += search_target
+ Goto(search_target, move_to_delay, 0)
+ else
+ // Exhausted current radius - expand search
+ search_expansion++
+ if(search_expansion <= 8)
+ visible_message(span_notice("[src] expands the search..."))
+
+ addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME)
+
+// BACKUP SYSTEM - alert nearby allies about target location
+/mob/living/simple_animal/hostile/proc/call_for_backup(atom/found_target, alert_type = "found")
+ if(!found_target)
+ return
+
+ // Cooldown check to prevent spam
+ if(!COOLDOWN_FINISHED(src, backup_call_cooldown))
+ return
+
+ COOLDOWN_START(src, backup_call_cooldown, backup_call_delay)
+
+ // Alert message
+ switch(alert_type)
+ if("found")
+ visible_message(span_danger("[src] alerts nearby allies!"))
+ if("lost")
+ visible_message(span_warning("[src] signals they've lost sight of the target!"))
+
+ // Find allies in range, INCLUDING ACROSS Z-LEVELS
+ var/list/allies_to_alert = list()
+
+ // Same Z-level
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, src))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ allies_to_alert += M
+
+ // Check Z-level above
+ var/turf/above_us = get_step_multiz(get_turf(src), UP)
+ if(above_us)
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, above_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ allies_to_alert += M
+
+ // Check Z-level below
+ var/turf/below_us = get_step_multiz(get_turf(src), DOWN)
+ if(below_us)
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, below_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ allies_to_alert += M
+
+ // Alert each ally
+ for(var/mob/living/simple_animal/hostile/ally in allies_to_alert)
+ switch(alert_type)
+ if("found")
+ ally.receive_backup_alert(found_target, get_turf(found_target))
+ if("lost")
+ ally.receive_lost_alert(found_target, get_turf(found_target))
+
+// Receive backup alert from ally
+/mob/living/simple_animal/hostile/proc/receive_backup_alert(atom/alert_target, turf/target_location)
+ if(stat == DEAD) // Dead mobs can't respond to alerts
+ return
+ if(!alert_target || QDELETED(alert_target))
+ return
+
+ if(last_give_up_time && (world.time - last_give_up_time) < give_up_cooldown)
+ return
+
+ // Ghost/dead check on the alert target itself
+ if(isliving(alert_target))
+ var/mob/living/L = alert_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ return // Don't chase ghosts or corpses
+
+ // Can we see them directly?
+ if(can_see(src, alert_target, get_effective_vision_range(alert_target)) && CanAttack(alert_target))
+ if(searching)
+ exit_search_mode(found_target = TRUE)
+ // Direct confirmation → becomes alpha
+ GiveTarget(alert_target) // GiveTarget sets is_alpha_alerter = TRUE
+ remembered_target = alert_target
+ last_target_sighting = world.time
+ last_known_location = target_location
+ if(AIStatus != AI_ON)
+ toggle_ai(AI_ON)
+ visible_message(span_danger("[src] responds to the alert!"))
+ return
+
+ // Can't see them - search quietly, no cascade
+ is_alpha_alerter = FALSE // Explicitly secondary
+ remembered_target = alert_target
+ last_target_sighting = world.time
+ last_known_location = target_location
+
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ if(!searching && !target)
+ // enter_search_mode() will see is_alpha_alerter=FALSE and NOT broadcast
+ enter_search_mode()
+ if(target_location && get_dist(src, target_location) > 3)
+ visible_message(span_danger("[src] responds to the alert and advances!"))
+ Goto(target_location, move_to_delay, 2)
+ else
+ visible_message(span_danger("[src] responds to the alert!"))
+ else if(target && target != alert_target)
+ // Already have a target - only update memory if alert target is much closer
+ if(get_dist(src, alert_target) < get_dist(src, target) - 3)
+ last_known_location = target_location
+
+// RETREAT TO ALLIES - move toward nearby allies while calling for backup
+/mob/living/simple_animal/hostile/proc/retreat_to_allies()
+ if(!target)
+ return
+
+ // Find nearby allies
+ var/list/nearby_allies = list()
+ var/turf/my_turf = get_turf(src)
+
+ // Same Z-level
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, src))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ nearby_allies += M
+
+ // Check Z-level above
+ var/turf/above_us = get_step_multiz(my_turf, UP)
+ if(above_us)
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, above_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ nearby_allies += M
+
+ // Check Z-level below
+ var/turf/below_us = get_step_multiz(my_turf, DOWN)
+ if(below_us)
+ for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, below_us))
+ if(M == src || M.stat == DEAD || M.ckey)
+ continue
+ if(faction_check_mob(M, TRUE))
+ nearby_allies += M
+
+ if(nearby_allies.len)
+ // Found allies! Call for backup and enter rally mode
+ visible_message(span_danger("[src] retreats toward reinforcements, calling for backup!"))
+ call_for_backup(target, "found")
+
+ // Enter rally mode
+ rallying = TRUE
+ rally_start_time = world.time
+ rally_point = get_turf(target) // Remember where we saw the target
+
+ // Move toward closest ally
+ var/mob/living/simple_animal/hostile/closest_ally = null
+ var/shortest_dist = INFINITY
+ for(var/mob/living/simple_animal/hostile/ally in nearby_allies)
+ var/dist = get_dist(src, ally)
+ if(dist < shortest_dist)
+ shortest_dist = dist
+ closest_ally = ally
+
+ if(closest_ally && get_dist(src, closest_ally) > 2) // Only move if not already adjacent
+ Goto(closest_ally, move_to_delay, 2) // Stop at distance 2 from ally
+
+ // Schedule advance back to target after rally duration
+ addtimer(CALLBACK(src, PROC_REF(advance_after_rally)), rally_duration, TIMER_DELETE_ME)
+ else
+ // No allies nearby, just alert them anyway and keep fighting
+ call_for_backup(target, "found")
+
+// Advance back to target after rallying with allies
+/mob/living/simple_animal/hostile/proc/advance_after_rally()
+ if(!rallying || QDELETED(src))
+ return
+
+ // Exit rally mode
+ rallying = FALSE
+
+ // If we still have a target, advance toward them
+ if(target && !QDELETED(target))
+ visible_message(span_danger("[src] advances with reinforcements!"))
+
+ // Move toward target if they're far away
+ var/dist = get_dist(src, target)
+ if(dist > 3)
+ Goto(target, move_to_delay, 1)
+
+ // Fallback: If no target but we have rally point, move there
+ else if(rally_point && !QDELETED(rally_point))
+ visible_message(span_danger("[src] advances toward the last sighting!"))
+ Goto(rally_point, move_to_delay, 2)
+
+ // Clear rally point
+ rally_point = null
+
+// Receive lost target alert from ally
+/mob/living/simple_animal/hostile/proc/receive_lost_alert(atom/lost_target, turf/last_location)
+ // If we're searching for the same target, update our search location
+ if(searching && remembered_target == lost_target)
+ last_known_location = last_location
+ visible_message(span_notice("[src] updates their search pattern..."))
+
+// DOOR OPENING - smart mobs can open doors instead of destroying them
+/mob/living/simple_animal/hostile/proc/try_open_door(atom/D)
+ if(!can_open_doors)
+ return FALSE
+
+ if(!D || QDELETED(D))
+ return FALSE
+
+ // Check cooldown
+ if(!COOLDOWN_FINISHED(src, door_open_cooldown))
+ return FALSE
+
+ COOLDOWN_START(src, door_open_cooldown, door_open_delay)
+
+ // Handle simple doors (wooden, metal, etc)
+ if(istype(D, /obj/structure/simple_door))
+ var/obj/structure/simple_door/SD = D
+
+ // Check if door has a padlock
+ if(SD.padlock && SD.padlock.locked)
+ return FALSE // Can't open locked doors
+
+ // Check if door is already open
+ if(!SD.density)
+ return FALSE
+
+ // Check if door is moving
+ if(SD.moving)
+ return FALSE
+
+ // Open the door using the proper method
+ visible_message(span_notice("[src] opens [SD]."))
+ SD.SwitchState(TRUE) // Use SwitchState with animate = TRUE
+ return TRUE
+
+ // Handle airlocks
+ if(istype(D, /obj/machinery/door/airlock))
+ if(!can_open_airlocks)
+ return FALSE
+
+ var/obj/machinery/door/airlock/A = D
+ if(A.locked || A.welded)
+ return FALSE // Can't open locked/welded doors
+
+ // Try to open it
+ if(A.density) // Door is closed
+ visible_message(span_notice("[src] opens [A]."))
+ INVOKE_ASYNC(A, TYPE_PROC_REF(/obj/machinery/door, open))
+ return TRUE
+
+ return FALSE
+
+ // Handle other machinery doors
+ if(istype(D, /obj/machinery/door))
+ var/obj/machinery/door/MD = D
+
+ // Try to open it
+ if(MD.density) // Door is closed
+ visible_message(span_notice("[src] opens [MD]."))
+ INVOKE_ASYNC(MD, TYPE_PROC_REF(/obj/machinery/door, open))
+ return TRUE
+
+ return FALSE
+
+// CURIOSITY - Check door during idle investigation
+/mob/living/simple_animal/hostile/proc/curiosity_check_door(atom/door)
+ if(!door || QDELETED(door) || stat == DEAD || AIStatus == AI_OFF)
+ return
+
+ // Only proceed if we're still idle and near the door
+ if(AIStatus != AI_IDLE || get_dist(src, door) > 2)
+ return
+
+ // Try to open the door
+ if(get_dist(src, door) <= 1 && try_open_door(door))
+ visible_message(span_notice("[src] peeks through [door]..."))
+
+ // Wait a moment for door to open
+ sleep(5)
+
+ // Look for targets on the other side
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ // Found someone! Wake up and aggro properly
+ visible_message(span_danger("[src] spots [possible_target] through the door!"))
+ toggle_ai(AI_ON)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ COOLDOWN_RESET(src, sight_shoot_delay)
+ LosePatience()
+ return
+
+ // Didn't see anyone - check nearby containers (within 5 tiles beyond the door)
+ var/turf/door_turf = get_turf(door)
+ if(door_turf)
+ var/list/nearby_containers = list()
+ for(var/turf/T in range(5, door_turf))
+ for(var/obj/structure/closet/C in T)
+ if(!C.opened && !C.welded && !C.locked)
+ nearby_containers += C
+
+ if(nearby_containers.len > 0)
+ // Pick closest container
+ var/obj/structure/closet/closest_container = null
+ var/closest_dist = 999
+ for(var/obj/structure/closet/C in nearby_containers)
+ var/dist = get_dist(src, C)
+ if(dist < closest_dist)
+ closest_dist = dist
+ closest_container = C
+
+ if(closest_container)
+ visible_message(span_notice("[src] investigates [closest_container]..."))
+ // Move toward it
+ Goto(closest_container, move_to_delay, 0)
+ // Check it after getting close
+ addtimer(CALLBACK(src, PROC_REF(curiosity_check_container), closest_container), 3 SECONDS, TIMER_DELETE_ME)
+ return
+
+// CURIOSITY - Check container during idle investigation
+/mob/living/simple_animal/hostile/proc/curiosity_check_container(obj/structure/closet/container)
+ if(!container || QDELETED(container) || stat == DEAD || AIStatus == AI_OFF)
+ return
+
+ // Only proceed if we're still idle and near the container
+ if(AIStatus != AI_IDLE || get_dist(src, container) > 1)
+ return
+
+ // Check if container is still closed
+ if(container.opened || container.welded || container.locked)
+ return
+
+ visible_message(span_warning("[src] searches [container]..."))
+
+ // Check container contents BEFORE opening (to catch hiding mobs)
+ for(var/atom/A in container.contents)
+ if(isliving(A))
+ var/mob/living/L = A
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(CanAttack(L))
+ // Found someone hiding! Open and aggro!
+ container.open(src)
+ sleep(5) // Wait for animation
+ visible_message(span_danger("[src] found [L] hiding in [container]!"))
+ toggle_ai(AI_ON)
+ last_target_sighting = world.time
+ remembered_target = L
+ GiveTarget(L)
+ last_known_location = get_turf(L)
+ walk(src, 0)
+ call_for_backup(L, "found")
+ COOLDOWN_RESET(src, sight_shoot_delay)
+ LosePatience()
+ return
+
+ // No one inside - just open it
+ container.open(src)
+ sleep(5)
+
+ // Check if anyone visible nearby now (they might have stepped out)
+ var/list/check_targets = ListTargets()
+ if(check_targets && check_targets.len > 0)
+ for(var/atom/possible_target in check_targets)
+ if(isliving(possible_target))
+ var/mob/living/L = possible_target
+ if(L.stat == DEAD || L.ckey && !L.client)
+ continue
+ if(!CanAttack(possible_target))
+ continue
+
+ // Found someone nearby - aggro!
+ visible_message(span_danger("[src] spots [possible_target]!"))
+ toggle_ai(AI_ON)
+ last_target_sighting = world.time
+ remembered_target = possible_target
+ GiveTarget(possible_target)
+ last_known_location = get_turf(possible_target)
+ walk(src, 0)
+ call_for_backup(possible_target, "found")
+ COOLDOWN_RESET(src, sight_shoot_delay)
+ LosePatience()
+ return
+
+// Z-LEVEL COMMITMENT - prevents constant up/down movement
+/mob/living/simple_animal/hostile/proc/commit_to_z_level()
+ committed_z_level = z
+ z_commit_time = world.time
+ recent_climbs = 0
-/mob/living/simple_animal/hostile/proc/Aggro()
- if(ckey)
- return TRUE
- vision_range = aggro_vision_range
- if(target && LAZYLEN(emote_taunt) && prob(taunt_chance))
- INVOKE_ASYNC(src, PROC_REF(emote), "me", EMOTE_VISIBLE, "[pick(emote_taunt)] at [target].")
- taunt_chance = max(taunt_chance-7,2)
- if(LAZYLEN(emote_taunt_sound))
- var/taunt_choice = pick(emote_taunt_sound)
- playsound(loc, taunt_choice, 50, 0, vary = FALSE, frequency = SOUND_FREQ_NORMALIZED(sound_pitch, vary_pitches[1], vary_pitches[2]))
+/mob/living/simple_animal/hostile/proc/should_change_z_level()
+ // Reset climb counter every 30 seconds
+ if((world.time - last_climb_reset) > 300)
+ recent_climbs = 0
+ last_climb_reset = world.time
+
+ // If we've climbed 3+ times recently, commit to current Z
+ if(recent_climbs >= 3)
+ commit_to_z_level()
+ visible_message(span_notice("[src] decides to stay on this level for now."))
+ return FALSE
+
+ // If we just committed to a Z-level, don't change
+ if(committed_z_level == z && (world.time - z_commit_time) < z_commit_duration)
+ return FALSE
+
+ return TRUE
+/mob/living/simple_animal/hostile/proc/record_z_level_change()
+ recent_climbs++
+ last_climb_reset = world.time
+ last_successful_z_move = world.time
-/mob/living/simple_animal/hostile/proc/LoseAggro()
- stop_automated_movement = 0
- vision_range = initial(vision_range)
- taunt_chance = initial(taunt_chance)
+// SOUND DETECTION - hear gunfire and move toward it (WITH MUFFLING)
+/mob/living/simple_animal/hostile/proc/hear_combat_sound(turf/sound_location, atom/sound_source)
+ if(!can_hear_combat)
+ return
+
+ if(!sound_location || QDELETED(sound_location))
+ return
+
+ // Calculate muffled range
+ var/effective_range = calculate_muffled_sound_range(sound_location, combat_hearing_range)
+
+ // Ignore sounds beyond effective range
+ var/distance = get_dist(src, sound_location)
+ var/z_distance = abs(z - sound_location.z)
+
+ if(distance > effective_range)
+ return
+
+ if(z_distance > 1) // Only hear sounds 1 Z-level away max
+ return
+
+ last_combat_sound = world.time
+
+ // Don't give exact location - pick a random spot near the sound (simulates hearing direction)
+ var/list/investigation_turfs = list()
+ for(var/turf/T in range(3, sound_location)) // Smaller radius for more accurate investigation
+ if(istype(T, /turf/open))
+ investigation_turfs += T
+
+ var/turf/investigation_spot = sound_location
+ if(investigation_turfs.len)
+ investigation_spot = pick(investigation_turfs)
+
+ // WAKE UP if idle - sounds should always wake mobs
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+ visible_message(span_warning("[src] perks up at the sound of combat!"))
+
+ // If we're stuck on stairs or in a loop, this breaks us out
+ if(pursuing_z_target || (recent_climbs >= 2))
+ // Commit to the Z-level the sound came from
+ if(sound_location.z != z && can_z_move)
+ committed_z_level = 0 // Clear commitment
+ recent_climbs = 0
+ z_commit_time = 0
+
+ if(remembered_target)
+ last_known_location = investigation_spot // Use approximate location
+
+ if(searching)
+ last_known_location = investigation_spot
+ else if(sound_location.z == z)
+ commit_to_z_level()
+
+ if(searching || !target)
+ last_known_location = investigation_spot
+ if(!searching)
+ enter_search_mode()
+
+ // ACTIVELY INVESTIGATE - move toward the sound!
+ if(!target) // Only investigate if we don't have a target
+ last_known_location = investigation_spot
+
+ if(!searching)
+ enter_search_mode()
+ visible_message(span_notice("[src] starts investigating the commotion..."))
+ else
+ // Already searching, update location
+ visible_message(span_notice("[src] adjusts their search toward the gunfire..."))
+ else if(searching)
+ // We have a remembered target but are searching - update search location
+ last_known_location = investigation_spot
+ visible_message(span_notice("[src] adjusts their search toward the gunfire..."))
-/mob/living/simple_animal/hostile/proc/LoseTarget()
- // Check if target just changed Z-levels before giving up
- if(target && can_z_move && isliving(target))
- var/mob/living/L = target
- if(L.z != z) // Target is on different Z!
- if(attempt_z_pursuit())
- return // Don't lose target, we're pursuing
+// Hear impact sounds (bullet hitting wall/target) - smaller radius
+/mob/living/simple_animal/hostile/proc/hear_impact_sound(turf/impact_location, atom/sound_source)
+ if(!can_hear_combat)
+ return
- GiveTarget(null)
- approaching_target = FALSE
- in_melee = FALSE
- walk(src, 0)
- LoseAggro()
+ if(!impact_location || QDELETED(impact_location))
+ return
+
+ // Calculate muffled range (now 7 tiles to match combat sounds)
+ var/effective_range = calculate_muffled_sound_range(impact_location, impact_hearing_range)
+
+ // Ignore sounds beyond effective range
+ var/distance = get_dist(src, impact_location)
+ var/z_distance = abs(z - impact_location.z)
+
+ if(distance > effective_range)
+ return
+
+ if(z_distance > 1) // Only hear sounds 1 Z-level away max
+ return
+
+ last_combat_sound = world.time
+
+ // Use impact location directly for CPU efficiency (no random picking)
+ var/turf/investigation_spot = impact_location
+
+ // ALWAYS WAKE UP - impacts should alert idle mobs
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+ visible_message(span_warning("[src] hears an impact nearby!"))
+
+ // AGGRESSIVE DOOR CHECKING - open doors between us and impact (ONLY ADJACENT)
+ if(can_open_doors)
+ var/list/path = getline(get_turf(src), impact_location)
+ for(var/turf/T in path)
+ // REALISM FIX: Only open doors within melee range (adjacent)
+ var/door_distance = get_dist(src, T)
+ if(door_distance > 1)
+ continue // Too far to open
+
+ // Check for simple doors
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density && try_open_door(SD))
+ visible_message(span_notice("[src] opens [SD] to investigate!"))
+ break // Only open one door per check for performance
+
+ // Check for machinery doors
+ for(var/obj/machinery/door/D in T)
+ if(D.density && try_open_door(D))
+ visible_message(span_notice("[src] opens [D] to investigate!"))
+ break // Only open one door per check for performance
+
+ // INVESTIGATE - move toward the impact
+ if(!target)
+ last_known_location = investigation_spot
+
+ if(!searching)
+ enter_search_mode()
+ visible_message(span_notice("[src] goes to investigate the noise..."))
+ else
+ last_known_location = investigation_spot
+ else if(searching)
+ // Update search location to be closer to impact
+ last_known_location = investigation_spot
-/mob/living/simple_animal/hostile/proc/attempt_z_pursuit()
- if(!target || !can_z_move)
- return FALSE
+/mob/living/simple_animal/hostile/proc/hear_gunshot(turf/shot_location, atom/shooter)
+ if(!can_hear_combat)
+ return
- if(world.time < last_z_move_attempt + z_move_delay)
- return FALSE
+ if(!shot_location || QDELETED(shot_location))
+ return
- last_z_move_attempt = world.time
+ // Check if shooter is an enemy
+ if(shooter && faction_check_mob(shooter))
+ return // Don't care about friendly fire
- var/mob/living/L = target
- if(!istype(L))
- return FALSE
+ // Calculate muffled range
+ var/effective_range = calculate_muffled_sound_range(shot_location, combat_hearing_range)
- target_last_z = L.z
+ var/distance = get_dist(src, shot_location)
+ var/z_distance = abs(z - shot_location.z)
- var/went_up = (L.z > z)
- var/went_down = (L.z < z)
+ if(distance > effective_range)
+ return
- if(!went_up && !went_down)
- return FALSE
+ if(z_distance > 1)
+ return
- var/list/z_structures = list()
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
- if(went_up)
- for(var/obj/structure/stairs/S in view(vision_range, src))
- if(S.isTerminator())
- z_structures += S
+ // If we don't have a target, investigate the sound
+ if(!target && !searching)
+ last_known_location = shot_location
+ enter_search_mode()
+ visible_message(span_warning("[src] perks up at the sound of gunfire!"))
+
+// LIGHT DETECTION - detect and investigate light sources in darkness
+/mob/living/simple_animal/hostile/proc/detect_light_source()
+ if(!can_detect_light)
+ return
+
+ // Check cooldown
+ if(world.time < last_light_detected + light_detection_cooldown)
+ return
+
+ // Check if we're in darkness (if required)
+ if(only_detect_lights_in_darkness)
+ var/turf/my_turf = get_turf(src)
+ if(!my_turf)
+ return
- for(var/obj/structure/ladder/L2 in view(vision_range, src))
- if(L2.up)
- z_structures += L2
+ // Check the ambient light level at our location
+ var/light_level = my_turf.get_lumcount()
+ if(light_level > darkness_threshold)
+ return // Too bright, don't bother detecting lights
- else if(went_down)
- for(var/turf/open/transparent/openspace/OS in view(vision_range, src))
- z_structures += OS
+ // ONLY check for living mobs with lights - not objects
+ var/list/potential_lights = list()
+
+ // FIX: Use the INTEGRATED vision system instead of raw view()
+ // Check all mobs in range, but filter by effective vision range
+ for(var/mob/living/M in view(light_detection_range, src))
+ if(M == src)
+ continue
+
+ // Don't detect friendly faction members
+ if(faction_check_mob(M))
+ continue
- for(var/obj/structure/ladder/L2 in view(vision_range, src))
- if(L2.down)
- z_structures += L2
+ // Check if they're emitting enough light
+ if(M.light_range < light_detection_threshold)
+ continue
+
+ // FIX: Apply the integrated vision system
+ // This checks BOTH lighting and direction
+ var/effective_range = get_effective_vision_range(M)
+ var/actual_distance = get_dist(src, M)
+
+ // Only detect if within our effective vision range
+ if(actual_distance <= effective_range)
+ potential_lights += M
- if(!z_structures.len)
+ // If we found any lights, investigate the closest one
+ if(potential_lights.len > 0)
+ var/atom/closest_light = null
+ var/closest_distance = INFINITY
+
+ for(var/atom/light_source in potential_lights)
+ var/distance = get_dist(src, light_source)
+ if(distance < closest_distance)
+ closest_distance = distance
+ closest_light = light_source
+
+ if(closest_light)
+ var/turf/light_location = get_turf(closest_light)
+
+ // Update cooldown
+ last_light_detected = world.time
+ last_light_location = light_location
+
+ // Wake up if idle
+ if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF)
+ toggle_ai(AI_ON)
+
+ // If it's a mob and we don't have a target, acquire it
+ if(ismob(closest_light) && !target)
+ GiveTarget(closest_light)
+ visible_message(span_warning("[src] spots a light source in the darkness!"))
+ return
+
+ // Otherwise investigate the location
+ if(!target && !searching)
+ last_known_location = light_location
+ enter_search_mode()
+ visible_message(span_warning("[src] notices a light in the darkness..."))
+ else if(searching && !target)
+ // Update search location if already searching
+ last_known_location = light_location
+
+// CORNER NAVIGATION - try alternate paths when stuck at corners
+/mob/living/simple_animal/hostile/proc/navigate_around_corner()
+ if(!target || !CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE))
return FALSE
- var/atom/nearest = get_closest_atom(/atom, z_structures, src)
+ var/turf/my_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
- visible_message(span_danger("[src] pursues [target] [went_up ? "upward" : "downward"]!"))
+ if(!my_turf || !target_turf)
+ return FALSE
- LosePatience()
- GainPatience()
- COOLDOWN_RESET(src, sight_shoot_delay)
+ // Get direction to target
+ var/target_dir = get_dir(my_turf, target_turf)
- if(went_up && istype(nearest, /obj/structure/stairs))
- var/obj/structure/stairs/S = nearest
+ // Try moving perpendicular to the target direction to navigate around corners
+ // This helps when stuck at walls/corners
+ var/list/sidestep_dirs = list()
+
+ // Build list of perpendicular directions based on primary direction
+ switch(target_dir)
+ if(NORTH)
+ sidestep_dirs = list(EAST, WEST)
+ if(SOUTH)
+ sidestep_dirs = list(EAST, WEST)
+ if(EAST)
+ sidestep_dirs = list(NORTH, SOUTH)
+ if(WEST)
+ sidestep_dirs = list(NORTH, SOUTH)
+ if(NORTHEAST)
+ sidestep_dirs = list(NORTH, EAST, SOUTHEAST, NORTHWEST)
+ if(NORTHWEST)
+ sidestep_dirs = list(NORTH, WEST, SOUTHWEST, NORTHEAST)
+ if(SOUTHEAST)
+ sidestep_dirs = list(SOUTH, EAST, NORTHEAST, SOUTHWEST)
+ if(SOUTHWEST)
+ sidestep_dirs = list(SOUTH, WEST, NORTHWEST, SOUTHEAST)
+
+ // Shuffle to avoid predictable patterns
+ sidestep_dirs = shuffle(sidestep_dirs)
+
+ // Try each sidestep direction
+ for(var/step_dir in sidestep_dirs)
+ var/turf/test_turf = get_step(my_turf, step_dir)
- // Check if we're already on the stairs
- var/turf/our_turf = get_turf(src)
- var/turf/stairs_turf = get_turf(S)
+ if(!test_turf)
+ continue
- if(our_turf == stairs_turf)
- // We're ON the stairs, just try to step in the stairs' direction
- step(src, S.dir)
- else
- // Path to the stairs themselves, not beyond them
- // Use Goto with minimum_distance 0 to actually step onto them
- Goto(S, move_to_delay, 0)
- else
- // For going down or ladders, just path to them
- Goto(nearest, move_to_delay, 0)
+ // Check if this turf is passable
+ if(test_turf.density)
+ continue
+
+ // Check for blocking objects
+ var/blocked = FALSE
+ for(var/atom/movable/AM in test_turf)
+ if(AM.density && AM != target)
+ // Check if we can open it (door)
+ if(can_open_doors && (istype(AM, /obj/structure/simple_door) || istype(AM, /obj/machinery/door)))
+ // Try opening the door
+ try_open_door(AM)
+ blocked = FALSE
+ break
+ else
+ blocked = TRUE
+ break
+
+ if(blocked)
+ continue
+
+ // Check if this position gets us closer or provides better angle
+ var/current_dist = get_dist(my_turf, target_turf)
+ var/new_dist = get_dist(test_turf, target_turf)
+
+ // Accept if same distance or closer (allows sidestepping around corners)
+ if(new_dist <= current_dist + 1)
+ // Try to move there
+ if(Move(test_turf, step_dir))
+ // Success! Clear stuck state
+ return TRUE
- return TRUE
+ // Couldn't find alternate path
+ return FALSE
//////////////END HOSTILE MOB TARGETTING AND AGGRESSION////////////
@@ -817,10 +4279,120 @@
return FALSE
+// Helper function to find a nearby position with clear line of sight to target
+/mob/living/simple_animal/hostile/proc/find_firing_position(atom/target)
+ if(!target)
+ return null
+
+ // Get current distance to target
+ var/current_dist = get_dist(src, target)
+
+ // Try adjacent tiles that maintain roughly the same distance
+ var/list/candidate_positions = list()
+ var/turf/my_turf = get_turf(src)
+
+ for(var/direction in GLOB.cardinals + GLOB.diagonals)
+ var/turf/T = get_step(my_turf, direction)
+ if(!T || T.density)
+ continue
+
+ // Check if we can move there
+ var/blocked = FALSE
+ for(var/atom/A in T)
+ if(A.density && A != src)
+ blocked = TRUE
+ break
+ if(blocked)
+ continue
+
+ // Check if this position has clear shot line (not just vision)
+ var/has_clear_shot = TRUE
+ var/turf/target_turf = get_turf(target)
+ var/list/shot_check = getline(T, target_turf)
+
+ for(var/turf/check_turf in shot_check)
+ if(check_turf == T || check_turf == target_turf)
+ continue
+
+ if(check_turf.density)
+ has_clear_shot = FALSE
+ break
+
+ for(var/atom/A in check_turf)
+ if(A.density && !ismob(A))
+ has_clear_shot = FALSE
+ break
+
+ if(!has_clear_shot)
+ break
+
+ if(!has_clear_shot)
+ continue
+
+ // Check distance - prefer positions at similar distance
+ var/new_dist = get_dist(T, target)
+ var/dist_diff = abs(new_dist - current_dist)
+
+ // Prioritize positions that maintain our distance
+ if(dist_diff <= 1)
+ candidate_positions[T] = 1 // High priority
+ else if(dist_diff <= 2)
+ candidate_positions[T] = 2 // Medium priority
+ else
+ candidate_positions[T] = 3 // Low priority (distance changed too much)
+
+ // Return best position, or null if none found
+ if(candidate_positions.len > 0)
+ // Sort by priority and return best
+ var/turf/best_pos = null
+ var/best_priority = 999
+ for(var/turf/T in candidate_positions)
+ if(candidate_positions[T] < best_priority)
+ best_priority = candidate_positions[T]
+ best_pos = T
+ return best_pos
+
+ return null
+
/mob/living/simple_animal/hostile/proc/OpenFire(atom/A)
+ // CRITICAL: Check if we actually have line of sight before shooting
+ // Exception: Allow close-range shooting (within 2 tiles) even without perfect LOS
+ var/dist_to_target = get_dist(src, A)
+ if(dist_to_target > 2 && !can_see(src, A, get_effective_vision_range(A)))
+ return FALSE // Can't see target and not close enough, don't shoot
+
if(COOLDOWN_TIMELEFT(src, sight_shoot_delay))
return FALSE
+ // BARRICADE FIX: Don't shoot at targets blocked by structures
+ // Pathfinding will handle getting around obstacles
+ if(dist_to_target > 2 && is_target_blocked_by_structure(A))
+ return FALSE // Target behind barricade/table/window - path around instead
+
+ // TACTICAL: Check shot line for doors - prefer opening over shooting (ONLY ADJACENT)
+ if(can_open_doors && A.z == z) // Only check same Z-level
+ var/list/shot_line = getline(src, A)
+ for(var/turf/T in shot_line)
+ if(T == get_turf(src)) // Skip our own tile
+ continue
+
+ // REALISM FIX: Only open doors within melee range (adjacent tiles)
+ var/door_distance = get_dist(src, T)
+ if(door_distance > 1)
+ continue // Too far to open, skip this door
+
+ // Check for simple doors
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density && try_open_door(SD))
+ visible_message(span_notice("[src] opens [SD] to get a clear shot!"))
+ return FALSE // Don't shoot this turn, door is opening
+
+ // Check for machinery doors
+ for(var/obj/machinery/door/D in T)
+ if(D.density && try_open_door(D))
+ visible_message(span_notice("[src] opens [D] to get a clear shot!"))
+ return FALSE // Don't shoot this turn, door is opening
+
// Allow shooting through openspace
if(A.z != z)
var/can_shoot = FALSE
@@ -851,11 +4423,11 @@
if(rapid > 1)
var/datum/callback/cb = CALLBACK(src, PROC_REF(Shoot), A)
for(var/i in 1 to rapid)
- addtimer(cb, (i - 1)*rapid_fire_delay)
+ addtimer(cb, (i - 1)*rapid_fire_delay, TIMER_DELETE_ME)
else
Shoot(A)
for(var/i in 1 to extra_projectiles)
- addtimer(CALLBACK(src, PROC_REF(Shoot), A), i * auto_fire_delay)
+ addtimer(CALLBACK(src, PROC_REF(Shoot), A), i * auto_fire_delay, TIMER_DELETE_ME)
ranged_cooldown = world.time + ranged_cooldown_time
if(sound_after_shooting)
addtimer(CALLBACK(GLOBAL_PROC,GLOBAL_PROC_REF(playsound), src, sound_after_shooting, 100, 0, 0), sound_after_shooting_delay, TIMER_STOPPABLE)
@@ -915,12 +4487,116 @@
/mob/living/simple_animal/hostile/proc/CanSmashTurfs(turf/T)
return iswallturf(T) || ismineralturf(T)
+// CHECK SHOT LINE - detect if projectiles will hit walls even with diagonal vision
+/mob/living/simple_animal/hostile/proc/is_shot_blocked(atom/target)
+ if(!target)
+ return TRUE
+
+ // Get the shot line from us to target
+ var/turf/source_turf = get_turf(src)
+ var/turf/target_turf = get_turf(target)
+
+ if(!source_turf || !target_turf)
+ return TRUE
+
+ // Use getline to get the actual path a projectile would take
+ var/list/shot_line = getline(source_turf, target_turf)
+
+ // Check each turf in the shot path
+ for(var/turf/T in shot_line)
+ if(T == source_turf || T == target_turf)
+ continue // Skip source and destination
+
+ // Check if turf itself is dense
+ if(T.density)
+ return TRUE // Wall or dense turf blocks shot
+
+ // Check for dense atoms (walls, windows, etc)
+ for(var/atom/A in T)
+ if(A.density)
+ // Ignore mobs - we can shoot over/past them
+ if(ismob(A))
+ continue
+ // Ignore simple animals - can shoot over them
+ if(istype(A, /mob/living/simple_animal))
+ continue
+ // Dense structure/obj blocks shot
+ return TRUE
+
+ // Shot line is clear
+ return FALSE
+
-/mob/living/simple_animal/hostile/Move(atom/newloc, dir , step_x , step_y)
+/mob/living/simple_animal/hostile/Move(atom/newloc, dir, step_x, step_y)
+ // FIX: Set facing direction when we move (so mobs turn naturally)
+ if(dir && dir != src.dir && !dodging)
+ setDir(dir)
+
if(dodging && approaching_target && prob(dodge_prob) && moving_diagonally == 0 && isturf(loc) && isturf(newloc))
return dodge(newloc,dir)
+
+ // Check if we're stepping onto/off stairs
+ var/was_on_stairs = FALSE
+ for(var/obj/structure/stairs/S in loc)
+ was_on_stairs = TRUE
+ break
+
+ var/moving_to_stairs = FALSE
+ for(var/obj/structure/stairs/S in newloc)
+ moving_to_stairs = TRUE
+ break
+
+ // ANTI-LOOP: Count consecutive stair steps
+ if(was_on_stairs || moving_to_stairs)
+ consecutive_stair_steps++
+
+ if(consecutive_stair_steps >= max_consecutive_stair_steps)
+ // Too many stair steps! Force commit and stop pursuing
+ commit_to_z_level()
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ consecutive_stair_steps = 0
+
+ visible_message(span_notice("[src] stops at the stairs."))
+ return FALSE // Don't move onto stairs
else
- return ..()
+ consecutive_stair_steps = 0
+
+ . = ..() // Do the move
+
+ if(!.) // Move failed
+ return FALSE
+
+ // IMMEDIATE LADDER CLIMBING
+ if(can_climb_ladders && pursuing_z_target && target && target.z != z && !client)
+ if((world.time - last_successful_z_move) < z_move_success_cooldown)
+ return .
+
+ var/obj/structure/ladder/unbreakable/L = locate() in loc
+ if(L)
+ var/went_up = (target.z > z)
+ var/went_down = (target.z < z)
+
+ if(went_up && L.up)
+ visible_message(span_warning("[src] immediately climbs up the ladder!"))
+ zMove(target = get_turf(L.up), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ last_z_move_attempt = world.time
+ last_successful_z_move = world.time
+ record_z_level_change()
+ consecutive_stair_steps = 0
+ return TRUE
+ else if(went_down && L.down)
+ visible_message(span_warning("[src] immediately climbs down the ladder!"))
+ zMove(target = get_turf(L.down), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED)
+ pursuing_z_target = FALSE
+ z_pursuit_structure = null
+ last_z_move_attempt = world.time
+ last_successful_z_move = world.time
+ record_z_level_change()
+ consecutive_stair_steps = 0
+ return TRUE
/mob/living/simple_animal/hostile/proc/dodge(moving_to,move_direction)
//Assuming we move towards the target we want to swerve toward them to get closer
@@ -932,6 +4608,70 @@
. = Move(moving_to,move_direction)
dodging = TRUE
+// SPATIAL AWARENESS - scan for doors we can use
+/mob/living/simple_animal/hostile/proc/scan_for_doors()
+ if(!can_open_doors)
+ return
+
+ // PERFORMANCE: Reduced scan range from vision_range to fixed 5 to reduce lag
+ // Direct object iteration instead of turf iteration
+ var/scan_range = 5
+
+ for(var/obj/machinery/door/D in range(scan_range, src))
+ // Add to known doors if not already there
+ var/door_loc = get_turf(D)
+ if(door_loc && !(door_loc in known_doors))
+ known_doors += door_loc
+
+ for(var/obj/structure/simple_door/SD in range(scan_range, src))
+ // Add to known doors if not already there
+ var/door_loc = get_turf(SD)
+ if(door_loc && !(door_loc in known_doors))
+ known_doors += door_loc
+
+ // Limit memory size to prevent bloat (keep 20 most recent)
+ if(known_doors.len > 20)
+ known_doors.Cut(1, known_doors.len - 20)
+
+// Try alternate paths using door memory
+/mob/living/simple_animal/hostile/proc/try_alternate_path()
+ if(!target || !can_open_doors)
+ return FALSE
+
+ if(path_attempts >= max_path_attempts)
+ return FALSE // Tried enough times
+
+ path_attempts++
+
+ // Look for known doors that might provide alternate routes
+ var/list/potential_doors = list()
+
+ for(var/turf/door_loc in known_doors)
+ if(QDELETED(door_loc))
+ known_doors -= door_loc
+ continue
+
+ // Check if door is in a useful direction
+ var/door_dist = get_dist(src, door_loc)
+ var/target_dist = get_dist(src, target)
+
+ // Door should be closer to us than target, but still reasonably close to target
+ if(door_dist < target_dist && door_dist <= 10)
+ potential_doors += door_loc
+
+ if(!potential_doors.len)
+ return FALSE
+
+ // Pick a random door to try
+ var/turf/chosen_door = pick(potential_doors)
+
+ visible_message(span_notice("[src] looks for another way around..."))
+
+ // Pathfind to the door
+ Goto(chosen_door, move_to_delay, 0)
+
+ return TRUE
+
/mob/living/simple_animal/hostile/proc/DestroyObjectsInDirection(direction)
var/turf/T = get_step(targets_from, direction)
if(T && T.Adjacent(targets_from))
@@ -944,19 +4684,125 @@
/mob/living/simple_animal/hostile/proc/DestroyPathToTarget()
- if(environment_smash)
- EscapeConfinement()
- var/dir_to_target = get_dir(targets_from, target)
- var/dir_list = list()
- if(dir_to_target in GLOB.diagonals) //it's diagonal, so we need two directions to hit
- for(var/direction in GLOB.cardinals)
- if(direction & dir_to_target)
- dir_list += direction
- else
- dir_list += dir_to_target
- for(var/direction in dir_list) //now we hit all of the directions we got in this fashion, since it's the only directions we should actually need
- DestroyObjectsInDirection(direction)
+ // This should only be called when we're REALLY stuck (after 15 seconds of trying)
+
+ // SPECIAL CASE: If searching, mark current location as unreachable and let search continue
+ if(searching)
+ // Mark our stuck position as searched so we don't try to go there again
+ var/turf/stuck_at = get_turf(src)
+ if(stuck_at && !(stuck_at in searched_turfs))
+ searched_turfs += stuck_at
+ // Also mark nearby turfs as searched to help pathfinding avoid this area
+ for(var/turf/T in range(2, stuck_at))
+ if(!(T in searched_turfs))
+ searched_turfs += T
+ // Reset stuck state so we can try a different search location
+ is_stuck = FALSE
+ stuck_position = null
+ stuck_time = 0
+ path_attempts = 0
+ return
+
+ // PRIORITY 1: Try opening doors aggressively first
+ if(can_open_doors)
+ var/opened_something = FALSE
+
+ // Try all adjacent turfs for doors
+ for(var/dir in GLOB.cardinals)
+ var/turf/T = get_step(src, dir)
+ if(!T)
+ continue
+
+ // Try simple doors
+ for(var/obj/structure/simple_door/SD in T)
+ if(SD.density && try_open_door(SD))
+ opened_something = TRUE
+ visible_message(span_notice("[src] forces open [SD]!"))
+
+ // Try machinery doors
+ for(var/obj/machinery/door/D in T)
+ if(D.density && try_open_door(D))
+ opened_something = TRUE
+ visible_message(span_notice("[src] forces open [D]!"))
+
+ if(opened_something)
+ // Give the door time to open
+ return
+
+ // PRIORITY 2: Try alternate paths using door memory
+ if(can_open_doors && try_alternate_path())
+ visible_message(span_notice("[src] tries to find another route..."))
+ return
+
+ // GIVE UP: If exhausted all path attempts and been stuck for 30+ seconds, give up entirely
+ if(path_attempts >= max_path_attempts && is_stuck && (world.time - stuck_time > 300))
+ visible_message(span_notice("[src] gives up the chase."))
+ // Set exhaustion cooldown to prevent immediate re-acquisition
+ last_give_up_time = world.time
+ // Force clear ALL target state
+ searching = FALSE
+ remembered_target = null
+ last_target_sighting = 0
+ LoseTarget()
+ return
+
+ // PRIORITY 3: Only smash if we can't open doors AND we're REALLY stuck
+ if(!environment_smash)
+ // Can't smash - if we've exhausted options and been stuck 20+ seconds, give up
+ if(path_attempts >= max_path_attempts && is_stuck && (world.time - stuck_time > 200))
+ visible_message(span_notice("[src] gives up the chase."))
+ // Set exhaustion cooldown to prevent immediate re-acquisition
+ last_give_up_time = world.time
+ // Force clear ALL target state
+ searching = FALSE
+ remembered_target = null
+ last_target_sighting = 0
+ LoseTarget()
+ return
+
+ // Check if we have direct line of sight to target
+ var/has_direct_los = target && can_see(src, target, get_effective_vision_range(target))
+
+ // Smash immediately with LOS, otherwise wait for delay
+ if(!has_direct_los && (!is_stuck || (world.time - stuck_time < smash_delay)))
+ return // Not stuck long enough yet
+
+ // ANTI-SPAM: Check smash cooldown
+ if((world.time - last_smash_time) < smash_cooldown)
+ return // Too soon since last smash
+
+ // ONE frustration message with cooldown
+ visible_message(span_danger("[src] gets frustrated and starts breaking things!"))
+ last_smash_time = world.time
+
+ // Now we can smash
+ EscapeConfinement()
+ var/dir_to_target = get_dir(targets_from, target)
+ var/dir_list = list()
+ if(dir_to_target in GLOB.diagonals)
+ for(var/direction in GLOB.cardinals)
+ if(direction & dir_to_target)
+ dir_list += direction
+ else
+ dir_list += dir_to_target
+
+ // Only smash if target is close (prevents smashing through entire base)
+ var/target_dist = get_dist(targets_from, target)
+ if(target_dist <= 5) // Only smash if within 5 tiles
+ for(var/direction in dir_list)
+ SmashInDirection(direction)
+// Pure smashing - no door checking (that's handled above now)
+/mob/living/simple_animal/hostile/proc/SmashInDirection(direction)
+ var/turf/T = get_step(targets_from, direction)
+ if(T && T.Adjacent(targets_from))
+ if(environment_smash)
+ if(CanSmashTurfs(T))
+ T.attack_animal(src)
+ for(var/obj/O in T)
+ if(O.density && environment_smash >= ENVIRONMENT_SMASH_STRUCTURES && !O.IsObscured())
+ O.attack_animal(src)
+ return
mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with megafauna destroying everything around them
if(environment_smash)
@@ -1036,6 +4882,8 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega
search_objects = value
/mob/living/simple_animal/hostile/consider_wakeup()
+ if(stat == DEAD) // Dead mobs don't wake up
+ return
..()
var/list/tlist
var/turf/T = get_turf(src)
@@ -1143,7 +4991,7 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega
visible_message(span_green("[src] shudders as the EMP overloads its servos!"))
LoseTarget()
toggle_ai(AI_OFF)
- addtimer(CALLBACK(src, PROC_REF(un_emp_stun)), min(intensity, 3 SECONDS))
+ addtimer(CALLBACK(src, PROC_REF(un_emp_stun)), min(intensity, 3 SECONDS), TIMER_DELETE_ME)
/mob/living/simple_animal/hostile/proc/un_emp_stun()
active_emp_flags -= MOB_EMP_STUN
@@ -1160,7 +5008,7 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega
visible_message(span_green("[src] lets out a burst of static and whips its gun around wildly!"))
var/list/old_faction = faction
faction = null
- addtimer(CALLBACK(src, PROC_REF(un_emp_berserk), old_faction), intensity SECONDS * 0.5)
+ addtimer(CALLBACK(src, PROC_REF(un_emp_berserk), old_faction), intensity SECONDS * 0.5, TIMER_DELETE_ME)
/mob/living/simple_animal/hostile/proc/un_emp_berserk(list/unberserk)
active_emp_flags -= MOB_EMP_BERSERK
diff --git a/code/modules/mob/living/simple_animal/hostile/mushroom.dm b/code/modules/mob/living/simple_animal/hostile/mushroom.dm
index 235c8530e04..15ba70c97d7 100644
--- a/code/modules/mob/living/simple_animal/hostile/mushroom.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mushroom.dm
@@ -89,7 +89,7 @@
/mob/living/simple_animal/hostile/mushroom/adjustHealth(amount, updating_health = TRUE, forced = FALSE) //Possibility to flee from a fight just to make it more visually interesting
if(!retreat_distance && prob(33))
retreat_distance = 5
- addtimer(CALLBACK(src, PROC_REF(stop_retreat)), 30)
+ addtimer(CALLBACK(src, PROC_REF(stop_retreat)), 30, TIMER_DELETE_ME)
. = ..()
/mob/living/simple_animal/hostile/mushroom/proc/stop_retreat()
@@ -138,7 +138,7 @@
revive(full_heal = 1)
UpdateMushroomCap()
recovery_cooldown = 1
- addtimer(CALLBACK(src, PROC_REF(recovery_recharge)), 300)
+ addtimer(CALLBACK(src, PROC_REF(recovery_recharge)), 300, TIMER_DELETE_ME)
/mob/living/simple_animal/hostile/mushroom/proc/recovery_recharge()
recovery_cooldown = 0
diff --git a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm b/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm
index 76b3d8fba84..4c97999f6a4 100644
--- a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm
+++ b/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm
@@ -21,7 +21,7 @@
for(var/turf/T in anchors)
var/datum/beam/B = Beam(T, "vine", time=INFINITY, maxdistance=5, beam_type=/obj/effect/ebeam/vine)
B.sleep_time = 10 //these shouldn't move, so let's slow down updates to 1 second (any slower and the deletion of the vines would be too slow)
- addtimer(CALLBACK(src, PROC_REF(bear_fruit)), growth_time)
+ addtimer(CALLBACK(src, PROC_REF(bear_fruit)), growth_time, TIMER_DELETE_ME)
/**
* Spawns a venus human trap, then qdels itself.
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index bbd116f2731..f238848c6f5 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -39,6 +39,12 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns)
var/wander = 1
///When set to 1 this stops the animal from moving when someone is pulling it.
var/stop_automated_movement_when_pulled = 1
+ ///The current direction the mob is facing while idle wandering
+ var/current_idle_direction = null
+ ///Last time (world.time) the mob changed its idle wander direction
+ var/idle_direction_change_time = 0
+ ///Minimum time (in deciseconds) before changing idle direction (45 seconds = 450 deciseconds)
+ var/idle_direction_change_cooldown = 450
///When someone interacts with the simple animal.
///Help-intent verb in present continuous tense.
@@ -352,13 +358,11 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns)
abilities = null // Clear the list to break references
for(var/obj/effect/proc_holder/ability in abilities_copy)
- // Clear cross-references
- if(ability.ranged_ability_user == src)
- ability.ranged_ability_user = null
+ // DON'T manually clear ranged_ability_user - let proc_holder's Destroy() handle it via remove_ranged_ability()
+ // Just clear the action owner to prevent circular refs
if(ability.action)
if(ability.action.owner == src)
ability.action.owner = null
- // Don't need to remove from actions list since we're clearing it below
qdel(ability)
// Clean up ALL actions
@@ -473,7 +477,18 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns)
return TRUE
if(stop_automated_movement_when_pulled && pulledby) //Some animals don't move when pulled
return TRUE
- var/anydir = pick(GLOB.cardinals)
+
+ // Direction persistence: only change idle direction after cooldown expires
+ var/anydir
+ if(!current_idle_direction || (world.time - idle_direction_change_time) >= idle_direction_change_cooldown)
+ // Cooldown expired or first time - pick a new random direction
+ anydir = pick(GLOB.cardinals)
+ current_idle_direction = anydir
+ idle_direction_change_time = world.time
+ else
+ // Use the persistent idle direction
+ anydir = current_idle_direction
+
if(Process_Spacemove(anydir))
Move(get_step(src, anydir), anydir)
turns_since_move = 0
diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm
index 46756dcee40..744f34bf2a0 100644
--- a/code/modules/mob/login.dm
+++ b/code/modules/mob/login.dm
@@ -53,11 +53,10 @@
mind?.hide_ckey = client?.prefs?.hide_ckey
// BYOND 516: Initialize verbs for the client on login/reconnect
- // Reconnect shorter: joining longer
+ // Only refresh verbs on body switch, not entire statbrowser (statbrowser loads once on client connection)
if(client)
var/delay = client.statbrowser_ready ? 1 : 50
addtimer(CALLBACK(client, TYPE_PROC_REF(/client, init_verbs)), delay)
- addtimer(CALLBACK(client, TYPE_PROC_REF(/client, load_statbrowser)), delay)
log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP)
SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 0caad361d77..d49cc0dd908 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -9,8 +9,12 @@
movespeed_modification = null
// Clear mind reference to break circular references
if(mind)
- mind.current = null
- mind = null
+ // Don't clear mind.current if there's a ghost that can still re-enter
+ // This prevents ghosts from being locked out of revival attempts
+ var/mob/dead/observer/ghost = mind.get_ghost(even_if_they_cant_reenter = FALSE)
+ if(!ghost)
+ mind.current = null
+ mind = null
// Clear held items by destroying them BEFORE parent cleanup
// This prevents item cleanup from trying to re-add items during component destruction
if(held_items && held_items.len)
diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm
index 04592678975..f1f75fcf259 100644
--- a/code/modules/power/lighting.dm
+++ b/code/modules/power/lighting.dm
@@ -575,7 +575,19 @@
. = ..()
if(. && !QDELETED(src))
if(prob(damage_amount * 5))
- break_light_tube()
+ // Check if attacked_by is a projectile to get the firer
+ var/atom/shooter = null
+ var/silenced = FALSE
+ if(istype(attacked_by, /obj/item/projectile))
+ var/obj/item/projectile/P = attacked_by
+ shooter = P.firer
+ silenced = P.suppressed
+ break_light_tube(0, shooter, silenced)
+
+// Override bullet_act to capture projectile info before damage
+/obj/machinery/light/bullet_act(obj/item/projectile/P, def_zone)
+ . = ..()
+ // The take_damage proc will handle the rest
@@ -720,7 +732,7 @@
// break the light and make sparks if was on
-/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = 0)
+/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = 0, atom/shooter = null, silenced = FALSE)
if(status == LIGHT_EMPTY || status == LIGHT_BROKEN)
return
@@ -731,6 +743,18 @@
do_sparks(3, TRUE, src)
status = LIGHT_BROKEN
update()
+
+ // HOSTILE MOB DETECTION: Alert nearby hostile mobs about light destruction
+ var/turf/impact_loc = get_turf(src)
+ if(impact_loc)
+ for(var/mob/living/simple_animal/hostile/H in range(9, impact_loc))
+ if(H.stat == DEAD || H.ckey || !H.can_hear_combat)
+ continue
+ // Only alert mobs that can actually SEE the light being destroyed
+ // Exception: if NOT silenced, sound carries through walls (hearing, not seeing)
+ if(silenced && !can_see(H, impact_loc, 9))
+ continue
+ H.detect_light_destruction(impact_loc, shooter, silenced)
/obj/machinery/light/proc/fix()
if(status == LIGHT_OK)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 4fd6d5067a4..d9daa76fa39 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -350,12 +350,22 @@ ATTACHMENTS
distant_sound = shootprops[CSP_INDEX_DISTANT_SOUND],
distant_range = shootprops[CSP_INDEX_DISTANT_RANGE]
)
+
+ // Alert nearby hostile mobs to gunfire
+ if(!silenced)
+ for(var/mob/living/simple_animal/hostile/H in range(7, user))
+ if(H.stat != DEAD && !H.ckey && H.can_hear_combat)
+ // Don't alert if shooter is same faction
+ if(!H.faction_check_mob(user))
+ H.hear_gunshot(get_turf(user), user)
+
if(!silenced && message && COOLDOWN_FINISHED(src, shoot_message_antispam))
COOLDOWN_START(src, shoot_message_antispam, GUN_SHOOT_MESSAGE_ANTISPAM_TIME)
if(pointblank)
user.visible_message(span_danger("[user] fires [src] point blank at [pbtarget]!"), null, null, COMBAT_MESSAGE_RANGE)
else
user.visible_message(span_danger("[user] fires [src]!"), null, null, COMBAT_MESSAGE_RANGE)
+
kickback(user, P)
//Adds logging to the attack log whenever anyone draws a gun, adds a pause after drawing a gun before you can do anything based on it's size
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index af210bc8ff4..14599974815 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -77,8 +77,21 @@
/obj/item/gun/energy/Destroy()
STOP_PROCESSING(SSobj, src)
+
+ // CRITICAL FIX: Energy guns reuse ammo casings from ammo_type list
+ // Don't let parent delete chambered since it's in ammo_type
+ // We need to clean up ammo_type ourselves
+ chambered = null // Clear reference without deleting
+
+ // Clean up the ammo_type list and delete all the ammo casings
+ if(ammo_type)
+ for(var/obj/item/ammo_casing/energy/E in ammo_type)
+ qdel(E)
+ ammo_type = null
+
if(cell)
QDEL_NULL(cell)
+
return ..()
/obj/item/gun/energy/handle_atom_del(atom/A)
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 5b0ae58174c..b6f61623de8 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -222,6 +222,10 @@
hit_limb = L.check_limb_hit(def_zone)
SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb)
var/turf/target_loca = get_turf(target)
+
+ // Alert hostile mobs about the impact (spawned to avoid blocking, optimized with early exits)
+ spawn(0)
+ notify_mobs_of_projectile_impact(target_loca, firer, src)
var/hitx
var/hity
@@ -930,6 +934,35 @@
var/obj/item/projectile/P = A
return istype(P) && P.armour_penetration
+// GLOBAL PROJECTILE IMPACT HANDLER for hostile.dm
+// CPU OPTIMIZED: Early exits, limited notifications, reduced range
+/proc/notify_mobs_of_projectile_impact(turf/impact_turf, atom/firer, obj/item/projectile/P)
+ if(!impact_turf || !firer)
+ return
+
+ var/notified = 0
+ var/max_notifications = 8 // Limit notifications to prevent spam on large mob groups
+
+ // CPU OPTIMIZATION: Reduced range from 20 to 5 tiles (50 tiles vs 400 tiles to check)
+ for(var/mob/living/simple_animal/hostile/M in range(5, impact_turf))
+ if(M.stat == DEAD || M.ckey)
+ continue
+
+ // Only alert mobs actively engaged (has target or searching)
+ if(!M.target && !M.searching)
+ continue
+
+ // Check if mob can hear the impact (with muffling)
+ var/effective_range = M.calculate_muffled_sound_range(impact_turf, M.impact_hearing_range)
+ var/distance = get_dist(M, impact_turf)
+ var/z_distance = abs(M.z - impact_turf.z)
+
+ if(distance <= effective_range && z_distance <= 1)
+ M.hear_impact_sound(impact_turf, firer)
+ notified++
+ if(notified >= max_notifications) // Early exit after enough notifications
+ return
+
/obj/item/projectile/bullet/F13
name = "bullet"
//Bullets library
diff --git a/code/modules/projectiles/projectile/plasma.dm b/code/modules/projectiles/projectile/plasma.dm
index 5df7b6b0b0b..b8fdade1593 100644
--- a/code/modules/projectiles/projectile/plasma.dm
+++ b/code/modules/projectiles/projectile/plasma.dm
@@ -48,3 +48,10 @@ obj/item/projectile/energy/evebolt
irradiate = 25
stamina = 100
icon_state = "plasma3"
+
+// Radiation projectile used by glowing ghouls
+/obj/item/projectile/radiation_thing
+ name = "radiation"
+ damage = 0
+ irradiate = 20
+ icon_state = "declone"
diff --git a/code/modules/reagents/reagent_containers/hypospray.dm b/code/modules/reagents/reagent_containers/hypospray.dm
index 23d7fd9a83b..813385e1213 100644
--- a/code/modules/reagents/reagent_containers/hypospray.dm
+++ b/code/modules/reagents/reagent_containers/hypospray.dm
@@ -109,6 +109,11 @@
if(!reagents.total_volume)
to_chat(user, span_warning("[src] is empty!"))
return
+
+ // Prevent use on simple animals
+ if(isanimal(M) && !iscarbon(M))
+ to_chat(user, span_warning("[src] is not designed for use on animals!"))
+ return
if(M == user)
M.visible_message(span_notice("[user] attempts to inject themselves with the [src]."))
diff --git a/code/modules/surgery/healing.dm b/code/modules/surgery/healing.dm
index 0d3c75ade0b..fb6cb765928 100644
--- a/code/modules/surgery/healing.dm
+++ b/code/modules/surgery/healing.dm
@@ -56,8 +56,8 @@
break
/datum/surgery_step/heal/success(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
- var/umsg = "You succeed in fixing some of [target]'s wounds" //no period, add initial space to "addons"
- var/tmsg = "[user] fixes some of [target]'s wounds" //see above
+ var/umsg = "You succeed in fixing some of [target]'s wounds"
+ var/tmsg = "[user] fixes some of [target]'s wounds"
var/urhealedamt_brute = brutehealing
var/urhealedamt_burn = burnhealing
var/urhealedamt_bleed = woundhealing
@@ -66,7 +66,7 @@
urhealedamt_brute += round((target.getBruteLoss()/ missinghpbonus),0.1)
urhealedamt_burn += round((target.getFireLoss()/ missinghpbonus),0.1)
urhealedamt_bleed += round((target.getBleedLoss()/ missinghpbonus),0.1)
- else //less healing bonus for the dead since they're expected to have lots of damage to begin with (to make TW into defib not TOO simple)
+ else
urhealedamt_brute += round((target.getBruteLoss()/ (missinghpbonus*5)),0.1)
urhealedamt_burn += round((target.getFireLoss()/ (missinghpbonus*5)),0.1)
urhealedamt_bleed += round((target.getBleedLoss()/ (missinghpbonus*5)),0.1)
@@ -76,6 +76,23 @@
urhealedamt_bleed *= 0.55
umsg += " as best as you can while they have clothing on"
tmsg += " as best as they can while [target] has clothing on"
+
+ // Skill 1 (INT >= 6, no surgery training) caps healing to just make the target revivable
+ // They'll survive but need meds to actually recover
+ if(ishuman(user))
+ var/mob/living/carbon/human/H = user
+ if(H.get_surgery_skill() == 1)
+ var/current_total = target.getBruteLoss() + target.getFireLoss()
+ var/revive_threshold = target.maxHealth * 0.15 // just above crit/death line
+ var/healable_total = max(0, current_total - revive_threshold)
+ var/heal_cap = min(1.0, healable_total / max(1, urhealedamt_brute + urhealedamt_burn))
+ urhealedamt_brute *= heal_cap
+ urhealedamt_burn *= heal_cap
+ urhealedamt_bleed *= heal_cap
+ if(heal_cap < 1.0)
+ umsg = "You crudely stabilize [target] — they'll need proper medical care to fully recover"
+ tmsg = "[user] crudely stabilizes [target]"
+
target.heal_bodypart_damage(urhealedamt_brute, urhealedamt_burn, bleed = urhealedamt_bleed)
display_results(user, target, span_notice("[umsg]."),
"[tmsg].",
diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm
index 5ffc26aca8e..6a49c14fccf 100644
--- a/code/modules/tgui_panel/external.dm
+++ b/code/modules/tgui_panel/external.dm
@@ -24,7 +24,7 @@
// Wait for HTML to load, then reinitialize everything
spawn(15)
- init_verbs() // Restore all verb tabs
+ init_verbs(force = TRUE) // Force rebuild on manual reload
// Restore admin tabs if they're an admin
if(holder)
diff --git a/fallout/code/modules/mob/living/living.dm b/fallout/code/modules/mob/living/living.dm
index 3c863c37a52..3122231c400 100644
--- a/fallout/code/modules/mob/living/living.dm
+++ b/fallout/code/modules/mob/living/living.dm
@@ -24,6 +24,7 @@
if(.)
pseudo_z_axis = newloc.get_fake_z()
pixel_z = pseudo_z_axis
+ last_move_time = world.time // Track movement for sound detection
/mob/living/carbon/update_stamina()
var/total_health = getStaminaLoss()
diff --git a/fallout/obj/stack/f13Cash.dm b/fallout/obj/stack/f13Cash.dm
index 6a9ff47c122..61730ac5aba 100644
--- a/fallout/obj/stack/f13Cash.dm
+++ b/fallout/obj/stack/f13Cash.dm
@@ -37,7 +37,7 @@
max_amount = 15000
throwforce = 0
throw_speed = 2
- throw_range = 2
+ throw_range = 7
w_class = WEIGHT_CLASS_TINY
full_w_class = WEIGHT_CLASS_TINY
resistance_flags = FLAMMABLE