diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm
index 881ba74c27..c45c490381 100644
--- a/code/__defines/mobs.dm
+++ b/code/__defines/mobs.dm
@@ -290,16 +290,19 @@
// More refined version of SA_* ""intelligence"" seperators.
// Now includes bitflags, so to target two classes you just do 'MOB_CLASS_ANIMAL|MOB_CLASS_HUMANOID'
-#define MOB_CLASS_ANIMAL 1 // Simple mobs like saviks and bears.
-#define MOB_CLASS_HUMANOID 2 // Non-robotic humanoids.
-#define MOB_CLASS_CONSTRUCT 4 // Silicons, mechanical simple mobs, and FBPs.
-#define MOB_CLASS_SLIME 8 // Everyone's favorite xenobiology specimen.
-#define MOB_CLASS_ABERRATION 16 // Weird shit.
-#define MOB_CLASS_DEMONIC 32 // Cult stuff.
-#define MOB_CLASS_BOSS 64 // Future megafauna hopefully someday.
-#define MOB_CLASS_ILLUSION 128 // Fake mobs, e.g. Technomancer illusions.
+#define MOB_CLASS_NONE 0 // Default value, and used to invert for _ALL.
-#define MOB_CLASS_ALL (MOB_CLASS_ANIMAL|MOB_CLASS_HUMANOID|MOB_CLASS_CONSTRUCT|MOB_CLASS_SLIME|MOB_CLASS_ABERRATION|MOB_CLASS_DEMONIC|MOB_CLASS_BOSS|MOB_CLASS_ILLUSION)
+#define MOB_CLASS_PLANT 1 // Unused at the moment.
+#define MOB_CLASS_ANIMAL 2 // Animals and beasts like spiders, saviks, and bears.
+#define MOB_CLASS_HUMANOID 4 // Non-robotic humanoids, including /simple_mob and /carbon/humans and their alien variants.
+#define MOB_CLASS_SYNTHETIC 8 // Silicons, mechanical simple mobs, FBPs, and anything else that would pass is_synthetic()
+#define MOB_CLASS_SLIME 16 // Everyone's favorite xenobiology specimen (and maybe prometheans?).
+#define MOB_CLASS_ABERRATION 32 // Weird shit.
+#define MOB_CLASS_DEMONIC 64 // Cult stuff.
+#define MOB_CLASS_BOSS 128 // Future megafauna hopefully someday.
+#define MOB_CLASS_ILLUSION 256 // Fake mobs, e.g. Technomancer illusions.
+
+#define MOB_CLASS_ALL (~MOB_CLASS_NONE)
// For slime commanding. Higher numbers allow for more actions.
#define SLIME_COMMAND_OBEY 1 // When disciplined.
diff --git a/code/controllers/subsystems/ai.dm b/code/controllers/subsystems/ai.dm
index f14a4039b3..b8852a035b 100644
--- a/code/controllers/subsystems/ai.dm
+++ b/code/controllers/subsystems/ai.dm
@@ -21,6 +21,8 @@ SUBSYSTEM_DEF(ai)
// var/mob/living/L = currentrun[currentrun.len]
var/datum/ai_holder/A = currentrun[currentrun.len]
--currentrun.len
+ if(!A || QDELETED(A)) // Doesn't exist or won't exist soon.
+ continue
if(times_fired % 4 == 0 && A.holder.stat != DEAD)
A.handle_strategicals()
if(A.holder.stat != DEAD) // The /TG/ version checks stat twice, presumably in-case processing somehow got the mob killed in that instant.
diff --git a/code/game/gamemodes/technomancer/spell_objs_helpers.dm b/code/game/gamemodes/technomancer/spell_objs_helpers.dm
index f11c027566..c0cf224c91 100644
--- a/code/game/gamemodes/technomancer/spell_objs_helpers.dm
+++ b/code/game/gamemodes/technomancer/spell_objs_helpers.dm
@@ -1,12 +1,17 @@
-//Returns 1 if the turf is dense, or if there's dense objects on it, unless told to ignore them.
-/turf/proc/check_density(var/ignore_objs = 0)
+//Returns 1 if the turf is dense, or if there's dense objects/mobs on it, unless told to ignore them.
+/turf/proc/check_density(var/ignore_objs = FALSE, var/ignore_mobs = FALSE)
if(density)
- return 1
- if(!ignore_objs)
+ return TRUE
+ if(!ignore_objs || !ignore_mobs)
for(var/atom/movable/stuff in contents)
if(stuff.density)
- return 1
- return 0
+ if(ignore_objs && isobj(stuff))
+ continue
+ else if(ignore_mobs && isliving(stuff)) // Ghosts aren't dense but keeping this limited to living type will probably save headaches in the future.
+ continue
+ else
+ return TRUE
+ return FALSE
// Used to distinguish friend from foe.
/obj/item/weapon/spell/proc/is_ally(var/mob/living/L)
diff --git a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
index 206ce2c705..c32140edcc 100644
--- a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
+++ b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
@@ -3,7 +3,7 @@
/datum/ai_holder/simple_mob
hostile = TRUE // The majority of simplemobs are hostile.
cooperative = TRUE
- returns_home = TRUE
+ returns_home = FALSE
can_flee = FALSE
speak_chance = 1 // If the mob's saylist is empty, nothing will happen.
@@ -16,7 +16,7 @@
// Ranged mobs.
/datum/ai_holder/simple_mob/ranged
- ranged = TRUE
+// ranged = TRUE
// Tries to not waste ammo.
/datum/ai_holder/simple_mob/ranged/careful
@@ -37,6 +37,20 @@
holder.IMove(get_step_away(holder, A, run_if_this_close))
holder.face_atom(A)
+// The electric spider's AI.
+/datum/ai_holder/simple_mob/ranged/electric_spider
+
+/datum/ai_holder/simple_mob/ranged/electric_spider/max_range(atom/movable/AM)
+ if(isliving(AM))
+ var/mob/living/L = AM
+ if(L.incapacitated(INCAPACITATION_DISABLED) || L.stat == UNCONSCIOUS) // If our target is stunned, go in for the kill.
+ return 1
+ return ..() // Do ranged if possible otherwise.
+
+
+
+
+
// Melee mobs.
@@ -54,6 +68,7 @@
/datum/ai_holder/simple_mob/melee/hooligan
hostile = FALSE
retaliate = TRUE
+ returns_home = TRUE
max_home_distance = 12
var/random_follow = TRUE // Turn off if you want to bus with crabs.
@@ -101,44 +116,82 @@
if(!O.anchored)
return TRUE
-/*
+// This AI hits something, then runs away for awhile.
+// It will (almost) always flee if they are uncloaked, AND their target is not stunned.
+/datum/ai_holder/simple_mob/melee/hit_and_run
+ can_flee = TRUE
+
+// Used for the 'running' part of hit and run.
+/datum/ai_holder/simple_mob/melee/hit_and_run/special_flee_check()
+ if(!holder.is_cloaked())
+ if(target && isliving(target))
+ var/mob/living/L = target
+ return !L.incapacitated(INCAPACITATION_DISABLED) // Don't flee if our target is stunned in some form, even if uncloaked. This is so the mob keeps attacking a stunned opponent.
+ return TRUE // We're out in the open, uncloaked, and our target isn't stunned, so lets flee.
+ return FALSE
-
-/datum/ai_holder/simple_mob/melee/nurse_spider/list_targets()
- var/list/targets = ..()
-
- if(targets.len) // Do regular targeting if there's actual enemies.
- world << "Returned early."
- world << "targets was [english_list(targets)]."
- return targets
-
- // Otherwise lets target objects to web them.
- var/static/webbable_types = typecacheof(list(/obj/machinery, /obj/item/, /obj/structure))
- for(var/WT in typecache_filter_list(range(vision_range, holder), webbable_types))
- var/obj/O = WT
- if(!O.anchored && can_see(holder, O, vision_range))
- targets += WT
-
- world << "targets was [english_list(targets)]."
- return targets
-*/
-/*
-/datum/ai_holder/simple_mob/melee/nurse_spider/can_attack(atom/movable/the_target)
- . = ..()
- if(!.) // Parent returned FALSE.
- if(istype(the_target, /obj))
- var/obj/O = the_target
- if(!O.anchored)
- return TRUE
-*/
+// This AI isolates people it stuns with its 'leap' attack, by dragging them away.
+/datum/ai_holder/simple_mob/melee/hunter_spider
/*
- . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
- var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
+/datum/ai_holder/simple_mob/melee/hunter_spider/post_special_attack(mob/living/L)
+ drag_away(L)
- for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
- if(can_see(holder, HM, vision_range))
- . += HM
+// Called after a successful leap.
+/datum/ai_holder/simple_mob/melee/hunter_spider/proc/drag_away(mob/living/L)
+ world << "Doing drag_away attack on [L]"
+ if(!istype(L))
+ world << "Invalid type."
+ return FALSE
+
+ // If they didn't get stunned, then don't bother.
+ if(!L.incapacitated(INCAPACITATION_DISABLED))
+ world << "Not incapcitated."
+ return FALSE
+
+ // Grab them.
+ if(!holder.start_pulling(L))
+ world << "Failed to pull."
+ return FALSE
+
+ holder.visible_message(span("danger","\The [holder] starts to drag \the [L] away!"))
+
+ var/list/allies = list()
+ var/list/enemies = list()
+ for(var/mob/living/thing in hearers(vision_range, holder))
+ if(thing == holder || thing == L) // Don't count ourselves or the thing we just started pulling.
+ continue
+ if(holder.IIsAlly(thing))
+ allies += thing
+ else
+ enemies += thing
+
+ // First priority: Move our victim to our friends.
+ if(allies.len)
+ world << "Going to move to ally"
+ give_destination(get_turf(pick(allies)), min_distance = 2, combat = TRUE) // This will switch our stance.
+
+ // Second priority: Move our victim away from their friends.
+ // There's a chance of it derping and pulling towards enemies if there's more than two people.
+ // Preventing that will likely be both a lot of effort for developers and the CPU.
+ else if(enemies.len)
+ world << "Going to move away from enemies"
+ var/mob/living/hostile = pick(enemies)
+ var/turf/move_to = get_turf(hostile)
+ for(var/i = 1 to vision_range) // Move them this many steps away from their friend.
+ move_to = get_step_away(move_to, L, 7)
+ if(move_to)
+ give_destination(move_to, min_distance = 2, combat = TRUE) // This will switch our stance.
+
+ // Third priority: Move our victim SOMEWHERE away from where they were.
+ else
+ world << "Going to move away randomly"
+ var/turf/move_to = get_turf(L)
+ move_to = get_step(move_to, pick(cardinal))
+ for(var/i = 1 to vision_range) // Move them this many steps away from where they were before.
+ move_to = get_step_away(move_to, L, 7)
+ if(move_to)
+ give_destination(move_to, min_distance = 2, combat = TRUE) // This will switch our stance.
*/
\ No newline at end of file
diff --git a/code/modules/ai/ai_holder_combat.dm b/code/modules/ai/ai_holder_combat.dm
index 1f904c1f24..0a2aea14ab 100644
--- a/code/modules/ai/ai_holder_combat.dm
+++ b/code/modules/ai/ai_holder_combat.dm
@@ -4,8 +4,8 @@
var/firing_lanes = FALSE // If ture, tries to refrain from shooting allies or the wall.
var/conserve_ammo = FALSE // If true, the mob will avoid shooting anything that does not have a chance to hit a mob. Requires firing_lanes to be true.
- var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly.
- var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy.
+// var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly.
+ var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy, if the mob is capable of ranged attacks.
var/pointblank = FALSE // If ranged is true, and this is true, people adjacent to the mob will suffer the ranged instead of using a melee attack.
var/special_attack_prob = 0 // The chance to ATTEMPT a special_attack(). If it fails, it will do a regular attack instead.
@@ -54,7 +54,8 @@
request_help() // Call our allies.
// Do a 'special' attack, if one is allowed.
- if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range))
+// if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range))
+ if(holder.ICheckSpecialAttack(target))
ai_log("engage_target() : Attempting a special attack.", AI_LOG_TRACE)
on_engagement(target)
if(special_attack(target)) // If this fails, then we try a regular melee/ranged attack.
@@ -68,7 +69,7 @@
melee_attack(target)
// Shoot them.
- else if(ranged && (distance <= shoot_range) )
+ else if(holder.ICheckRangedAttack(target) && (distance <= max_range(target)) )
on_engagement(target)
if(firing_lanes && !test_projectile_safety(target))
// Nudge them a bit, maybe they can shoot next time.
@@ -99,7 +100,10 @@
// Most mobs probably won't have this defined but we don't care.
/datum/ai_holder/proc/special_attack(atom/movable/AM)
- return holder.ISpecialAttack(AM)
+ . = holder.ISpecialAttack(AM)
+ world << "special_attack result is [.]"
+ if(.)
+ post_special_attack(AM)
// Called when within striking/shooting distance, however cooldown is not considered.
// Override to do things like move in a random step for evasiveness.
@@ -107,11 +111,15 @@
/datum/ai_holder/proc/on_engagement(atom/A)
// Called after a successful (IE not on cooldown) ranged attack.
+// Note that this is not whether the projectile actually hit, just that one was launched.
/datum/ai_holder/proc/post_ranged_attack(atom/A)
// Ditto but for melee.
/datum/ai_holder/proc/post_melee_attack(atom/A)
+// And one more for special snowflake attacks.
+/datum/ai_holder/proc/post_special_attack(atom/A)
+
// Used to make sure projectiles will probably hit the target and not the wall or a friend.
/datum/ai_holder/proc/test_projectile_safety(atom/movable/AM)
var/mob/living/L = check_trajectory(AM, holder) // This isn't always reliable but its better than the previous method.
@@ -137,13 +145,17 @@
var/distance = get_dist(holder, AM)
if(distance <= 1)
return TRUE // Can melee.
- else if(ranged && distance <= shoot_range)
+ else if(holder.ICheckRangedAttack(AM) && distance <= max_range(AM))
return TRUE // Can shoot.
return FALSE
+// Determines how close the AI will move to its target.
+/datum/ai_holder/proc/closest_distance(atom/movable/AM)
+ return max(max_range(AM) - 1, 1) // Max range -1 just because we don't want to constantly get kited
+
// Can be used to conditionally do a ranged or melee attack.
-/datum/ai_holder/proc/closest_distance()
- return ranged ? shoot_range - 1 : 1 // Shoot range -1 just because we don't want to constantly get kited
+/datum/ai_holder/proc/max_range(atom/movable/AM)
+ return holder.ICheckRangedAttack(AM) ? 7 : 1
// Goes to the target, to attack them.
// Called when in STANCE_APPROACH.
@@ -160,18 +172,21 @@
ai_log("walk_to_target() : Found new target ([target]).", AI_LOG_INFO)
// Find out where we're going.
- var/get_to = closest_distance()
+ var/get_to = closest_distance(target)
var/distance = get_dist(holder, target)
ai_log("walk_to_target() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
- if(distance <= get_to)
+ // Special case: Our holder has a special attack that is ranged, but normally the holder uses melee.
+ // If that happens, we'll switch to STANCE_FIGHT so they can use it. If the special attack is limited, they'll likely switch back next tick.
+ if(distance <= get_to || holder.ICheckSpecialAttack(target))
ai_log("walk_to_target() : Within range.", AI_LOG_INFO)
forget_path()
set_stance(STANCE_FIGHT)
ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG)
return
+
// Otherwise keep walking.
walk_path(target, get_to)
diff --git a/code/modules/ai/ai_holder_combat_unseen.dm b/code/modules/ai/ai_holder_combat_unseen.dm
index 545281af9e..0cb518f08c 100644
--- a/code/modules/ai/ai_holder_combat_unseen.dm
+++ b/code/modules/ai/ai_holder_combat_unseen.dm
@@ -17,7 +17,7 @@
/datum/ai_holder/proc/shoot_near_turf(turf/targeted_turf)
if(!ranged)
return // Can't shoot.
- if(get_dist(holder, targeted_turf) > shoot_range)
+ if(get_dist(holder, targeted_turf) > max_range(targeted_turf))
return // Too far to shoot.
var/turf/T = pick(RANGE_TURFS(2, targeted_turf)) // The turf we're actually gonna shoot at.
diff --git a/code/modules/ai/ai_holder_debug.dm b/code/modules/ai/ai_holder_debug.dm
index c4b2f990a0..a49af50399 100644
--- a/code/modules/ai/ai_holder_debug.dm
+++ b/code/modules/ai/ai_holder_debug.dm
@@ -95,7 +95,7 @@
use_astar = TRUE
/datum/ai_holder/hostile/ranged
- ranged = TRUE
+// ranged = TRUE
cooperative = TRUE
firing_lanes = TRUE
conserve_ammo = TRUE
diff --git a/code/modules/ai/ai_holder_fleeing.dm b/code/modules/ai/ai_holder_fleeing.dm
index b9c45ace93..83a7be94b8 100644
--- a/code/modules/ai/ai_holder_fleeing.dm
+++ b/code/modules/ai/ai_holder_fleeing.dm
@@ -16,6 +16,8 @@
return TRUE
if(can_flee)
+ if(special_flee_check())
+ return TRUE
if(!hostile && !retaliate)
return TRUE // We're not hostile and someone attacked us first.
if(flee_when_dying && (holder.health / holder.getMaxHealth()) <= dying_threshold)
@@ -24,12 +26,17 @@
return TRUE // We're fighting something way way stronger then us.
return FALSE
+// Override for special fleeing conditionally.
+/datum/ai_holder/proc/special_flee_check()
+ return FALSE
+
/datum/ai_holder/proc/flee_from_target()
ai_log("flee_from_target() : Entering.", AI_LOG_DEBUG)
- if(!target || !can_attack(target)) // can_attack() is used since it checks the same things we would need to anyways.
+ if(!target || !should_flee() || !can_attack(target)) // can_attack() is used since it checks the same things we would need to anyways.
ai_log("flee_from_target() : Lost target to flee from.", AI_LOG_INFO)
lose_target()
+ set_stance(STANCE_IDLE)
ai_log("flee_from_target() : Exiting.", AI_LOG_DEBUG)
return
diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm
index 3eaa33f449..a621af2051 100644
--- a/code/modules/ai/ai_holder_targeting.dm
+++ b/code/modules/ai/ai_holder_targeting.dm
@@ -5,9 +5,12 @@
var/retaliate = FALSE // Attacks whatever struck it first. Mobs will still attack back if this is false but hostile is true.
var/atom/movable/target = null // The thing (mob or object) we're trying to kill.
+ var/atom/movable/preferred_target = null// If set, and if given the chance, we will always prefer to target this over other options.
var/turf/target_last_seen_turf = null // Where the mob last observed the target being, used if the target disappears but the mob wants to keep fighting.
var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting.
+ var/respect_alpha = TRUE // If true, mobs with a sufficently low alpha will be treated as invisible.
+ var/alpha_vision_threshold = 127 // Targets with an alpha less or equal to this will be considered invisible. Requires above var to be true.
var/lose_target_time = 0 // world.time when a target was lost.
var/lose_target_timeout = 5 SECONDS // How long until a mob 'times out' and stops trying to find the mob that disappeared.
@@ -56,7 +59,12 @@
// targets -= A
if(!targets.len) // We found nothing.
return
- var/chosen_target = pick(targets)
+
+ var/chosen_target
+ if(preferred_target && preferred_target in targets)
+ chosen_target = preferred_target
+ else
+ chosen_target = pick(targets)
return chosen_target
// Step 4, give us our selected target.
@@ -87,7 +95,7 @@
if(isliving(the_target))
var/mob/living/L = the_target
- if(L.stat)
+ if(L.stat == DEAD)
return FALSE
if(holder.IIsAlly(L))
return FALSE
@@ -138,10 +146,14 @@
ai_log("can_see_target() : There is no target. Exiting.", AI_LOG_WARNING)
return FALSE
- if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well.
+ if(holder.see_invisible < the_target.invisibility) // Real invis.
ai_log("can_see_target() : Target ([the_target]) was invisible to holder. Exiting.", AI_LOG_TRACE)
return FALSE
+ if(respect_alpha && the_target.alpha <= alpha_vision_threshold) // Fake invis.
+ ai_log("can_see_target() : Target ([the_target]) was sufficently transparent to holder and is hidden. Exiting.", AI_LOG_TRACE)
+ return FALSE
+
if(get_dist(holder, the_target) > view_range) // Too far away.
ai_log("can_see_target() : Target ([the_target]) was too far from holder. Exiting.", AI_LOG_TRACE)
return FALSE
@@ -195,7 +207,13 @@
ai_log("react_to_attack() : Was attacked by [attacker].", AI_LOG_INFO)
return give_target(attacker) // Also handles setting the appropiate stance.
-/*
- if(ai_inactive || stat || M == target_mob) return //Not if we're dead or already hitting them
- if(M in friends || M.faction == faction) return //I'll overlook it THIS time...
-*/
\ No newline at end of file
+// Causes targeting to prefer targeting the taunter if possible.
+// This generally occurs if more than one option is within striking distance, including the taunter.
+// Otherwise the default filter will prefer the closest target.
+/datum/ai_holder/proc/receive_taunt(atom/movable/taunter)
+ ai_log("receive_taunt() : Was taunted by [taunter].", AI_LOG_INFO)
+ preferred_target = taunter
+
+/datum/ai_holder/proc/lose_taunt()
+ ai_log("lose_taunt() : Resetting preferred_target.", AI_LOG_INFO)
+ preferred_target = null
\ No newline at end of file
diff --git a/code/modules/ai/interfaces.dm b/code/modules/ai/interfaces.dm
index 9ef507ceeb..a864471a43 100644
--- a/code/modules/ai/interfaces.dm
+++ b/code/modules/ai/interfaces.dm
@@ -7,20 +7,38 @@
return FALSE
/mob/living/simple_mob/IAttack(atom/A)
- return attack_target(A)
+ if(!canClick()) // Still on cooldown from a "click".
+ return FALSE
+ return attack_target(A) // This will set click cooldown.
/mob/living/proc/IRangedAttack(atom/A)
return FALSE
/mob/living/simple_mob/IRangedAttack(atom/A)
+ if(!canClick()) // Still on cooldown from a "click".
+ return FALSE
return shoot_target(A)
+// Test if the AI is allowed to use to attempt a ranged attack.
+/mob/living/proc/ICheckRangedAttack(atom/A)
+ return FALSE
+
+/mob/living/simple_mob/ICheckRangedAttack(atom/A)
+ return projectiletype ? TRUE : FALSE
+
/mob/living/proc/ISpecialAttack(atom/A)
return FALSE
/mob/living/simple_mob/ISpecialAttack(atom/A)
return special_attack_target(A)
+// Is the AI allowed to attempt to do it?
+/mob/living/proc/ICheckSpecialAttack(atom/A)
+ return FALSE
+
+/mob/living/simple_mob/ICheckSpecialAttack(atom/A)
+ return can_special_attack(A)
+
/mob/living/proc/ISay(message)
/mob/living/proc/IIsAlly(mob/living/L)
diff --git a/code/modules/mob/animations.dm b/code/modules/mob/animations.dm
index 7f082e1ef0..9577d98900 100644
--- a/code/modules/mob/animations.dm
+++ b/code/modules/mob/animations.dm
@@ -176,8 +176,33 @@ note dizziness decrements automatically in the mob's Life() proc.
pixel_z = default_pixel_z
alpha = initial_alpha
-/atom/movable/proc/do_attack_animation(atom/A)
+// Similar to attack animations, but in reverse and is longer to act as a telegraph.
+/atom/movable/proc/do_windup_animation(atom/A, windup_time)
+ var/pixel_x_diff = 0
+ var/pixel_y_diff = 0
+ var/direction = get_dir(src, A)
+ if(direction & NORTH)
+ pixel_y_diff = -8
+ else if(direction & SOUTH)
+ pixel_y_diff = 8
+ if(direction & EAST)
+ pixel_x_diff = -8
+ else if(direction & WEST)
+ pixel_x_diff = 8
+
+ var/default_pixel_x = initial(pixel_x)
+ var/default_pixel_y = initial(pixel_y)
+ var/mob/mob = src
+ if(istype(mob))
+ default_pixel_x = mob.default_pixel_x
+ default_pixel_y = mob.default_pixel_y
+
+ animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, time = windup_time - 2)
+ animate(pixel_x = default_pixel_x, pixel_y = default_pixel_y, time = 2)
+
+
+/atom/movable/proc/do_attack_animation(atom/A)
var/pixel_x_diff = 0
var/pixel_y_diff = 0
var/direction = get_dir(src, A)
diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm
index 9eec76bdb7..cd80358b31 100644
--- a/code/modules/mob/living/carbon/human/human_helpers.dm
+++ b/code/modules/mob/living/carbon/human/human_helpers.dm
@@ -42,6 +42,17 @@
if(cloaker.active)
cloaker.deactivate()
+/mob/living/carbon/human/is_cloaked()
+ if(mind && mind.changeling) // Ling camo.
+ return mind.changeling.cloaked
+ else if(istype(back, /obj/item/weapon/rig)) //Ninja cloak
+ var/obj/item/weapon/rig/suit = back
+ for(var/obj/item/rig_module/stealth_field/cloaker in suit.installed_modules)
+ if(cloaker.active)
+ return TRUE
+ else
+ return ..()
+
/mob/living/carbon/human/get_ear_protection()
var/sum = 0
if(istype(l_ear, /obj/item/clothing/ears))
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index 88eda79b2e..f2281823a1 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -675,9 +675,9 @@
//SA vs SA basically
/mob/living/simple_animal/attack_generic(var/mob/attacker)
- ..()
if(attacker)
react_to_attack(attacker)
+ return ..()
/mob/living/simple_animal/movement_delay()
var/tally = 0 //Incase I need to add stuff other than "speed" later
diff --git a/code/modules/mob/living/simple_mob/combat.dm b/code/modules/mob/living/simple_mob/combat.dm
index d7f784c2be..08ed8a4d83 100644
--- a/code/modules/mob/living/simple_mob/combat.dm
+++ b/code/modules/mob/living/simple_mob/combat.dm
@@ -1,24 +1,40 @@
// Does a melee attack.
/mob/living/simple_mob/proc/attack_target(atom/A)
- set waitfor = FALSE // For attack animations, if they're ever added. Don't want the AI processor to get held up.
+ set waitfor = FALSE // For attack animations. Don't want the AI processor to get held up.
if(!A.Adjacent(src))
return FALSE
- if(!canClick()) // Still on cooldown from a "click".
- return FALSE
+ var/turf/their_T = get_turf(A)
+ if(attack_delay)
+ // their_T.color = "#FF0000"
+ melee_pre_animation(A)
+ handle_attack_delay(A) // This will sleep this proc for a bit, which is why waitfor is false.
+
+ // Cooldown testing is done at click code (for players) and interface code (for AI).
setClickCooldown(get_attack_speed())
- return do_attack(A)
+ . = do_attack(A, their_T)
+
+ if(attack_delay)
+ melee_post_animation(A)
+ // their_T.color = "#FFFFFF"
+
// This does the actual attack.
// This is a seperate proc for the purposes of attack animations.
-/mob/living/simple_mob/proc/do_attack(atom/A)
- if(!A.Adjacent(src)) // They could've moved in the meantime.
+// A is the thing getting attacked, T is the turf A is/was on when attack_target was called.
+/mob/living/simple_mob/proc/do_attack(atom/A, turf/T)
+// if(!A.Adjacent(src))
+ if((!(A in T)) || !T.Adjacent(src)) // Most likely we have a slow attack and they dodged it or we somehow got moved.
+ add_attack_logs(src, A, "Animal-attacked (dodged)", admin_notify = FALSE)
+ playsound(src, 'sound/weapons/punchmiss.ogg', 75, 1)
return FALSE
var/damage_to_do = rand(melee_damage_lower, melee_damage_upper)
+ damage_to_do = apply_bonus_melee_damage(A, damage_to_do)
+
for(var/datum/modifier/M in modifiers)
if(!isnull(M.outgoing_melee_damage_percent))
damage_to_do *= M.outgoing_melee_damage_percent
@@ -36,27 +52,40 @@
if(H.check_shields(damage = damage_to_do, damage_source = src, attacker = src, def_zone = null, attack_text = "the attack"))
return FALSE // We were blocked.
- if(A.attack_generic(src, damage_to_do, pick(attacktext)) && attack_sound)
+ if(A.attack_generic(src, damage_to_do, pick(attacktext)))
apply_melee_effects(A)
- playsound(src, attack_sound, 75, 1)
+ if(attack_sound)
+ playsound(src, attack_sound, 75, 1)
return TRUE
-// Override for special effects after a successful attack.
+// Override for special effects after a successful attack, like injecting poison or stunning the target.
/mob/living/simple_mob/proc/apply_melee_effects(atom/A)
+ return
+
+// Override to modify the amount of damage the mob does conditionally.
+// This must return the amount of outgoing damage.
+// Note that this is done before mob modifiers scale the damage.
+/mob/living/simple_mob/proc/apply_bonus_melee_damage(atom/A, damage_amount)
+ return damage_amount
//The actual top-level ranged attack proc
/mob/living/simple_mob/proc/shoot_target(atom/A)
- if(!canClick())
- return FALSE
-
+ set waitfor = FALSE
setClickCooldown(get_attack_speed())
+ if(attack_delay)
+ ranged_pre_animation(A)
+ handle_attack_delay(A) // This will sleep this proc for a bit, which is why waitfor is false.
+
visible_message("\The [src] fires at \the [A]!")
shoot(A, src.loc, src)
if(casingtype)
new casingtype
+ if(attack_delay)
+ ranged_post_animation(A)
+
return TRUE
@@ -71,7 +100,73 @@
return
P.launch(A)
+// if(distance >= special_attack_min_range && distance <= special_attack_max_range)
+// return TRUE
-//Special attacks, like grenades or blinding spit or whatever
+// Can we currently do a special attack?
+/mob/living/simple_mob/proc/can_special_attack(atom/A)
+ // Validity check.
+ if(!istype(A))
+ return FALSE
+
+ // Ability check.
+ if(isnull(special_attack_min_range) || isnull(special_attack_max_range))
+ return FALSE
+
+ // Distance check.
+ var/distance = get_dist(src, A)
+ if(distance < special_attack_min_range || distance > special_attack_max_range)
+ return FALSE
+
+ // Cooldown check.
+ if(!isnull(special_attack_cooldown) && last_special_attack + special_attack_cooldown > world.time)
+ return FALSE
+
+ // Charge check.
+ if(!isnull(special_attack_charges) && special_attack_charges <= 0)
+ return FALSE
+
+ return TRUE
+
+
+// Special attacks, like grenades or blinding spit or whatever.
+// Don't override this, override do_special_attack() for your blinding spit/etc.
/mob/living/simple_mob/proc/special_attack_target(atom/A)
- return FALSE
\ No newline at end of file
+ last_special_attack = world.time
+ if(do_special_attack(A))
+ return TRUE
+ return FALSE
+
+// Override this for the actual special attack.
+/mob/living/simple_mob/proc/do_special_attack(atom/A)
+ return FALSE
+
+// Sleeps the proc that called it for the correct amount of time.
+// Also makes sure the AI doesn't do anything stupid in the middle of the delay.
+/mob/living/simple_mob/proc/handle_attack_delay(atom/A)
+ set_AI_busy(TRUE)
+
+ // Click delay modifiers also affect telegraphing time.
+ // This means berserked enemies will leave less time to dodge.
+ var/true_attack_delay = attack_delay
+ for(var/datum/modifier/M in modifiers)
+ if(!isnull(M.attack_speed_percent))
+ attack_delay *= M.attack_speed_percent
+
+ setClickCooldown(true_attack_delay) // Insurance against a really long attack being longer than default click delay.
+
+ sleep(true_attack_delay)
+
+ set_AI_busy(FALSE)
+
+
+// Override these four for special custom animations (like the GOLEM).
+/mob/living/simple_mob/proc/melee_pre_animation(atom/A)
+ do_windup_animation(A, attack_delay)
+
+/mob/living/simple_mob/proc/melee_post_animation(atom/A)
+
+/mob/living/simple_mob/proc/ranged_pre_animation(atom/A)
+ do_windup_animation(A, attack_delay) // Semi-placeholder.
+
+/mob/living/simple_mob/proc/ranged_post_animation(atom/A)
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/on_click.dm b/code/modules/mob/living/simple_mob/on_click.dm
index be162bc230..95c1ea876e 100644
--- a/code/modules/mob/living/simple_mob/on_click.dm
+++ b/code/modules/mob/living/simple_mob/on_click.dm
@@ -17,9 +17,8 @@
custom_emote(1,"[pick(friendly)] [A]!")
if(I_HURT)
- if(prob(special_attack_prob))
- if(special_attack_min_range <= 1)
- special_attack_target(A)
+ if(can_special_attack(A) && special_attack_target(A))
+ return
else if(melee_damage_upper == 0 && istype(A,/mob/living))
custom_emote(1,"[pick(friendly)] [A]!")
@@ -37,10 +36,8 @@
/mob/living/simple_mob/RangedAttack(var/atom/A)
// setClickCooldown(get_attack_speed())
- var/distance = get_dist(src, A)
- if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range))
- special_attack_target()
+ if(can_special_attack(A) && special_attack_target(A))
return
if(projectiletype)
diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm
index cc38d0f6b4..499efd8314 100644
--- a/code/modules/mob/living/simple_mob/simple_mob.dm
+++ b/code/modules/mob/living/simple_mob/simple_mob.dm
@@ -83,17 +83,23 @@
var/melee_damage_upper = 6 // Upper bound of randomized melee damage
var/list/attacktext = list("attacked") // "You are [attacktext] by the mob!"
var/list/friendly = list("nuzzles") // "The mob [friendly] the person."
- var/attack_sound = null // Sound to play when I attack
- var/melee_miss_chance = 15 // percent chance to miss a melee attack.
+ var/attack_sound = null // Sound to play when I attack
+ var/melee_miss_chance = 0 // percent chance to miss a melee attack.
var/attack_armor_type = "melee" // What armor does this check?
var/attack_armor_pen = 0 // How much armor pen this attack has.
- var/attack_sharp = 0 // Is the attack sharp?
- var/attack_edge = 0 // Does the attack have an edge?
+ var/attack_sharp = FALSE // Is the attack sharp?
+ var/attack_edge = FALSE // Does the attack have an edge?
+ var/attack_delay = null // If set, the mob will do a windup animation and can miss if the target moves out of the way.
//Special attacks
- var/special_attack_prob = 0 // Chance of the mob doing a special attack (0 for never)
- var/special_attack_min_range = 0 // Min range to perform the special attacks from
- var/special_attack_max_range = 0 // Max range to perform special attacks from
+// var/special_attack_prob = 0 // The chance to ATTEMPT a special_attack_target(). If it fails, it will do a regular attack instead.
+ // This is commented out to ease the AI attack logic by being (a bit more) determanistic.
+ // You should instead limit special attacks using the below vars instead.
+ var/special_attack_min_range = null // The minimum distance required for an attempt to be made.
+ var/special_attack_max_range = null // The maximum for an attempt.
+ var/special_attack_charges = null // If set, special attacks will work off of a charge system, and won't be usable if all charges are expended. Good for grenades.
+ var/special_attack_cooldown = null // If set, special attacks will have a cooldown between uses.
+ var/last_special_attack = null // world.time when a special attack occured last, for cooldown calculations.
//Damage resistances
var/grab_resist = 0 // Chance for a grab attempt to fail. Note that this is not a true resist and is just a prob() of failure.
@@ -151,7 +157,7 @@
//Client attached
/mob/living/simple_mob/Login()
. = ..()
- to_chat(src,"You are \the [src]. [player_msg]")
+ to_chat(src,"You are \the [src]. [player_msg]")
/mob/living/simple_mob/emote(var/act, var/type, var/desc)
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/carrier.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/carrier.dm
new file mode 100644
index 0000000000..28daa5b4ae
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/carrier.dm
@@ -0,0 +1,67 @@
+// Carriers are not too dangerous on their own, but they create more spiders when dying.
+
+/mob/living/simple_mob/animal/giant_spider/carrier
+ desc = "Furry, beige, and red, it makes you shudder to look at it. This one has luminous green eyes."
+ icon_state = "carrier"
+ icon_living = "carrier"
+ icon_dead = "carrier_dead"
+
+ maxHealth = 100
+ health = 100
+
+ melee_damage_lower = 8
+ melee_damage_upper = 25
+
+ poison_per_bite = 3
+ poison_type = "chloralhydrate"
+
+ movement_cooldown = 5
+
+ player_msg = "Upon dying, you will release a swarm of spiderlings or young hunter spiders.
\
+ If a spider emerges, you will be placed in control of it."
+
+ var/spiderling_count = 0
+ var/spiderling_type = /obj/effect/spider/spiderling
+ var/swarmling_type = /mob/living/simple_mob/animal/giant_spider/hunter
+ var/swarmling_faction = "spiders"
+ var/swarmling_prob = 10 // Odds that a spiderling will be a swarmling instead.
+
+/mob/living/simple_mob/animal/giant_spider/carrier/initialize()
+ spiderling_count = rand(5, 10)
+ adjust_scale(1.2)
+ return ..()
+
+/mob/living/simple_mob/animal/giant_spider/carrier/death()
+ visible_message(span("warning", "\The [src]'s abdomen splits as it rolls over, spiderlings crawling from the wound.") )
+ spawn(1)
+ var/list/new_spiders = list()
+ for(var/i = 1 to spiderling_count)
+ if(prob(swarmling_prob) && src)
+ var/mob/living/simple_mob/animal/giant_spider/swarmling = new swarmling_type(src.loc)
+ var/swarm_health = Floor(swarmling.maxHealth * 0.4)
+ var/swarm_dam_lower = Floor(melee_damage_lower * 0.4)
+ var/swarm_dam_upper = Floor(melee_damage_upper * 0.4)
+ swarmling.name = "spiderling"
+ swarmling.maxHealth = swarm_health
+ swarmling.health = swarm_health
+ swarmling.melee_damage_lower = swarm_dam_lower
+ swarmling.melee_damage_upper = swarm_dam_upper
+ swarmling.faction = swarmling_faction
+ swarmling.adjust_scale(0.75)
+ new_spiders += swarmling
+ else if(src)
+ var/obj/effect/spider/spiderling/child = new spiderling_type(src.loc)
+ child.skitter()
+ else // We might've gibbed or got deleted.
+ break
+ // Transfer our player to their new body, if RNG provided one.
+ if(new_spiders.len && client)
+ var/mob/living/simple_mob/animal/giant_spider/new_body = pick(new_spiders)
+ new_body.key = src.key
+ return ..()
+
+/mob/living/simple_mob/animal/giant_spider/carrier/recursive
+ desc = "Furry, beige, and red, it makes you shudder to look at it. This one has luminous green eyes. \
+ You have a distinctly bad feeling about this."
+
+ swarmling_type = /mob/living/simple_animal/hostile/giant_spider/carrier/recursive
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/electric.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/electric.dm
new file mode 100644
index 0000000000..2e526c15f5
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/electric.dm
@@ -0,0 +1,33 @@
+// Electric spiders fire taser-like beams at their enemies.
+// TODO: AI
+
+/mob/living/simple_mob/animal/giant_spider/electric
+ desc = "Spined and yellow, it makes you shudder to look at it. This one has flickering gold eyes."
+ icon_state = "spark"
+ icon_living = "spark"
+ icon_dead = "spark_dead"
+
+ maxHealth = 210
+ health = 210
+
+ taser_kill = 0 //It -is- the taser.
+
+ base_attack_cooldown = 10
+ projectilesound = 'sound/weapons/taser2.ogg'
+ projectiletype = /obj/item/projectile/beam/stun/electric_spider
+
+ melee_damage_lower = 10
+ melee_damage_upper = 25
+
+ poison_chance = 15
+ poison_per_bite = 3
+ poison_type = "stimm"
+
+ player_msg = "You can fire a taser-like ranged attack by clicking on an enemy or tile at a distance."
+
+ ai_holder_type = /datum/ai_holder/simple_mob/ranged/electric_spider
+
+
+/obj/item/projectile/beam/stun/electric_spider
+ name = "stun beam"
+ agony = 20
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/frost.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/frost.dm
new file mode 100644
index 0000000000..365e40c5e2
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/frost.dm
@@ -0,0 +1,18 @@
+// Frost spiders inject cryotoxin, slowing people down (which is very bad if trying to run from spiders).
+
+/mob/living/simple_mob/animal/giant_spider/frost
+ desc = "Icy and blue, it makes you shudder to look at it. This one has brilliant blue eyes."
+ icon_state = "frost"
+ icon_living = "frost"
+ icon_dead = "frost_dead"
+
+ maxHealth = 175
+ health = 175
+
+ poison_per_bite = 5
+ poison_type = "cryotoxin"
+
+// Sif variant with a somewhat different desc.
+/mob/living/simple_mob/animal/giant_spider/frost/sif
+ desc = "Icy and blue, it makes you shudder to look at it. This one has brilliant blue eyes. \
+ It isn't native to Sif."
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/giant_spider.dm
similarity index 93%
rename from code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm
rename to code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/giant_spider.dm
index efa0dc77bc..052ee0017c 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/giant_spider.dm
@@ -30,9 +30,11 @@
melee_damage_upper = 30
attack_sharp = 1
attack_edge = 1
+ attack_sound = 'sound/weapons/bite.ogg'
heat_damage_per_tick = 20
cold_damage_per_tick = 20
+ minbodytemp = 175 // So they can all survive Sif without having to be classed under /sif subtype.
speak_emote = list("chitters")
@@ -59,6 +61,3 @@
to_chat(L, "You feel a tiny prick.")
L.reagents.add_reagent(poison_type, poison_per_bite)
-// Subtype
-
-
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/hunter.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/hunter.dm
new file mode 100644
index 0000000000..ea10759ffe
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/hunter.dm
@@ -0,0 +1,87 @@
+// Hunters are fast, fragile, and possess a leaping attack.
+// The leaping attack is somewhat telegraphed and can be dodged if one is paying attention.
+// The AI would've followed up after a successful leap with dragging the downed victim away, but the dragging code was too janky.
+
+/mob/living/simple_mob/animal/giant_spider/hunter
+ desc = "Furry and black, it makes you shudder to look at it. This one has sparkling purple eyes."
+ icon_state = "hunter"
+ icon_living = "hunter"
+ icon_dead = "hunter_dead"
+
+ maxHealth = 120
+ health = 120
+
+ poison_per_bite = 5
+
+ movement_cooldown = 0 // Hunters are FAST.
+
+ ai_holder_type = /datum/ai_holder/simple_mob/melee/hunter_spider
+
+ player_msg = "You are very fast, and can perform a leaping attack by clicking on someone from a short distance away.
\
+ If the leap succeeds, the target will be knocked down briefly and you will be on top of them.
\
+ Note that there is a short delay before you leap!"
+
+ // Leaping is a special attack, so these values determine when leap can happen.
+ // Leaping won't occur if its on cooldown.
+ special_attack_min_range = 2
+ special_attack_max_range = 4
+ special_attack_cooldown = 10 SECONDS
+
+ var/leap_warmup = 1 SECOND // How long the leap telegraphing is.
+
+// The actual leaping attack.
+/mob/living/simple_mob/animal/giant_spider/hunter/do_special_attack(atom/A)
+ set waitfor = FALSE
+ set_AI_busy(TRUE)
+
+ // Telegraph, since getting stunned suddenly feels bad.
+ do_windup_animation(A, leap_warmup)
+ sleep(leap_warmup) // For the telegraphing.
+
+ // Do the actual leap.
+ status_flags |= LEAPING // Lets us pass over everything.
+ visible_message(span("danger","\The [src] leaps at \the [A]!"))
+ throw_at(get_step(get_turf(A), get_turf(src)), special_attack_max_range+1, 1, src)
+ playsound(src, 'sound/weapons/spiderlunge.ogg', 75, 1)
+
+ sleep(5) // For the throw to complete. It won't hold up the AI ticker due to waitfor being false.
+
+ if(status_flags & LEAPING)
+ status_flags &= ~LEAPING // Revert special passage ability.
+
+ var/turf/T = get_turf(src) // Where we landed. This might be different than A's turf.
+
+ . = FALSE
+
+ // Now for the stun.
+ var/mob/living/victim = null
+ for(var/mob/living/L in T) // So player-controlled spiders only need to click the tile to stun them.
+ if(L == src)
+ continue
+
+ if(ishuman(L))
+ var/mob/living/carbon/human/H = L
+ if(H.check_shields(damage = 0, damage_source = src, attacker = src, def_zone = null, attack_text = "the leap"))
+ continue // We were blocked.
+
+ victim = L
+ break
+
+ if(victim)
+ victim.Weaken(2)
+ victim.visible_message(span("danger","\The [src] knocks down \the [victim]!"))
+ to_chat(victim, span("critical", "\The [src] jumps on you!"))
+ . = TRUE
+
+ set_AI_busy(FALSE)
+
+// var/obj/item/weapon/grab/G = new(src, victim)
+// put_in_active_hand(G)
+
+// G.synch()
+// G.affecting = victim
+// victim.LAssailant = src
+
+// visible_message("\The [src] seizes \the [victim] aggressively!")
+// do_attack_animation(victim)
+
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/lurker.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/lurker.dm
new file mode 100644
index 0000000000..1f1ecac678
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/lurker.dm
@@ -0,0 +1,103 @@
+// Lurkers are somewhat similar to Hunters, however the big difference is that Lurkers have an imperfect cloak.
+// Their AI will try to do hit and run tactics, striking the enemy when its "cloaked", for bonus damage and a stun.
+// They keep attacking until the stun ends, then retreat to cloak again and repeat the cycle.
+// Hitting the spider before it does its ambush attack will break the cloak and make the spider flee.
+
+/mob/living/simple_mob/animal/giant_spider/lurker
+ desc = "Translucent and white, it makes you shudder to look at it. This one has incandescent red eyes."
+ icon_state = "lurker"
+ icon_living = "lurker"
+ icon_dead = "lurker_dead"
+
+ maxHealth = 100
+ health = 100
+
+ poison_per_bite = 5
+
+ movement_cooldown = 5
+
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ poison_chance = 30
+ poison_type = "cryptobiolin"
+ poison_per_bite = 1
+
+ player_msg = "You have an imperfect, but automatic stealth. If you attack something while 'hidden', then \
+ you will do bonus damage, stun the target, and unstealth for a period of time.
\
+ Getting attacked will also break your stealth."
+
+ ai_holder_type = /datum/ai_holder/simple_mob/melee/hit_and_run
+
+ var/cloaked = FALSE
+ var/cloaked_alpha = 45 // Lower = Harder to see.
+ var/cloaked_bonus_damage = 30 // This is added on top of the normal melee damage.
+ var/cloaked_weaken_amount = 3 // How long to stun for.
+ var/cloak_cooldown = 10 SECONDS // Amount of time needed to re-cloak after losing it.
+ var/last_uncloak = 0 // world.time
+
+
+/mob/living/simple_mob/animal/giant_spider/lurker/proc/cloak()
+ if(cloaked)
+ return
+ animate(src, alpha = cloaked_alpha, time = 1 SECOND)
+ cloaked = TRUE
+
+
+/mob/living/simple_mob/animal/giant_spider/lurker/proc/uncloak()
+ last_uncloak = world.time // This is assigned even if it isn't cloaked already, to 'reset' the timer if the spider is continously getting attacked.
+ if(!cloaked)
+ return
+ animate(src, alpha = initial(alpha), time = 1 SECOND)
+ cloaked = FALSE
+
+
+// Check if cloaking if possible.
+/mob/living/simple_mob/animal/giant_spider/lurker/proc/can_cloak()
+ if(stat)
+ return FALSE
+ if(last_uncloak + cloak_cooldown > world.time)
+ return FALSE
+
+ return TRUE
+
+
+// Called by things that break cloaks, like Technomancer wards.
+/mob/living/simple_mob/animal/giant_spider/lurker/break_cloak()
+ uncloak()
+
+
+/mob/living/simple_mob/animal/giant_spider/lurker/is_cloaked()
+ return cloaked
+
+
+// Cloaks the spider automatically, if possible.
+/mob/living/simple_mob/animal/giant_spider/lurker/handle_special()
+ if(!cloaked && can_cloak())
+ cloak()
+
+
+// Applies bonus base damage if cloaked.
+/mob/living/simple_mob/animal/giant_spider/lurker/apply_bonus_melee_damage(atom/A, damage_amount)
+ if(cloaked)
+ return damage_amount + cloaked_bonus_damage
+ return ..()
+
+// Applies stun, then uncloaks.
+/mob/living/simple_mob/animal/giant_spider/lurker/apply_melee_effects(atom/A)
+ if(cloaked)
+ if(isliving(A))
+ var/mob/living/L = A
+ L.Weaken(cloaked_weaken_amount)
+ to_chat(L, span("danger", "\The [src] ambushes you!"))
+ playsound(L, 'sound/weapons/spiderlunge.ogg', 75, 1)
+ uncloak()
+ ..() // For the poison.
+
+// Force uncloaking if attacked.
+/mob/living/simple_mob/animal/giant_spider/lurker/bullet_act(obj/item/projectile/P)
+ . = ..()
+ break_cloak()
+
+/mob/living/simple_mob/animal/giant_spider/lurker/hit_with_weapon(obj/item/O, mob/living/user, effective_force, hit_zone)
+ . = ..()
+ break_cloak()
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/pepper.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/pepper.dm
new file mode 100644
index 0000000000..1c2eaa6dda
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/pepper.dm
@@ -0,0 +1,21 @@
+// Pepper spiders inject condensed capsaicin into their victims.
+
+/mob/living/simple_mob/animal/giant_spider/pepper
+ desc = "Red and brown, it makes you shudder to look at it. This one has glinting red eyes."
+ icon_state = "pepper"
+ icon_living = "pepper"
+ icon_dead = "pepper_dead"
+
+ maxHealth = 210
+ health = 210
+
+ melee_damage_lower = 8
+ melee_damage_upper = 15
+
+ poison_chance = 20
+ poison_per_bite = 5
+ poison_type = "condensedcapsaicin_v"
+
+/mob/living/simple_mob/animal/giant_spider/pepper/initialize()
+ adjust_scale(1.1)
+ return ..()
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/phorogenic.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/phorogenic.dm
new file mode 100644
index 0000000000..d9508a05d8
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/phorogenic.dm
@@ -0,0 +1,56 @@
+// Phorogenic spiders explode when they die.
+// You really shouldn't melee them.
+
+/mob/living/simple_mob/animal/giant_spider/phorogenic
+ desc = "Crystalline and purple, it makes you shudder to look at it. This one has haunting purple eyes."
+ icon_state = "phoron"
+ icon_living = "phoron"
+ icon_dead = "phoron_dead"
+
+ maxHealth = 225
+ health = 225
+ taser_kill = FALSE //You will need more than a peashooter to kill the juggernaut.
+
+ melee_damage_lower = 25
+ melee_damage_upper = 40
+ attack_armor_pen = 15
+
+ movement_cooldown = 15
+
+ poison_chance = 30
+ poison_per_bite = 0.5
+ poison_type = "phoron"
+
+ var/exploded = FALSE
+ var/explosion_dev_range = 1
+ var/explosion_heavy_range = 2
+ var/explosion_light_range = 4
+ var/explosion_flash_range = 6 // This doesn't do anything iirc.
+
+ var/explosion_delay_lower = 1 SECOND // Lower bound for explosion delay.
+ var/explosion_delay_upper = 2 SECONDS // Upper bound.
+
+/mob/living/simple_mob/animal/giant_spider/phorogenic/initialize()
+ adjust_scale(1.25)
+ return ..()
+
+/mob/living/simple_mob/animal/giant_spider/phorogenic/death()
+ visible_message(span("critical", "\The [src]'s body begins to rupture!"))
+ var/delay = rand(explosion_delay_lower, explosion_delay_upper)
+ spawn(0)
+ // Flash black and red as a warning.
+ for(var/i = 1 to delay)
+ if(i % 2 == 0)
+ color = "#000000"
+ else
+ color = "#FF0000"
+ sleep(1)
+
+ spawn(delay)
+ // The actual boom.
+ if(src && !exploded)
+ visible_message(span("danger", "\The [src]'s body detonates!"))
+ exploded = TRUE
+ explosion(src.loc, explosion_dev_range, explosion_heavy_range, explosion_light_range, explosion_flash_range)
+ return ..()
+
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/thermic.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/thermic.dm
new file mode 100644
index 0000000000..f24e92be7b
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/thermic.dm
@@ -0,0 +1,17 @@
+// Thermic spiders inject a special variant of thermite that burns someone from the inside.
+
+/mob/living/simple_mob/animal/giant_spider/thermic
+ desc = "Mirage-cloaked and orange, it makes you shudder to look at it. This one has simmering orange eyes."
+ icon_state = "pit"
+ icon_living = "pit"
+ icon_dead = "pit_dead"
+
+ maxHealth = 175
+ health = 175
+
+ melee_damage_lower = 10
+ melee_damage_upper = 25
+
+ poison_chance = 30
+ poison_per_bite = 1
+ poison_type = "thermite_v"
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/tunneler.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/tunneler.dm
new file mode 100644
index 0000000000..bf95f40b6c
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/tunneler.dm
@@ -0,0 +1,208 @@
+// Tunnelers have a special ability that allows them to charge at an enemy by tunneling towards them.
+// Any mobs inbetween the tunneler's path and the target will be stunned if the tunneler hits them.
+// The target will suffer a stun as well, if the tunneler hits them at the end. A successful hit will stop the tunneler.
+// If the target moves fast enough, the tunneler can miss, causing it to overshoot.
+// If the tunneler hits a solid wall, the tunneler will suffer a stun.
+
+/mob/living/simple_mob/animal/giant_spider/tunneler
+ desc = "Sandy and brown, it makes you shudder to look at it. This one has glittering yellow eyes."
+ icon_state = "tunneler"
+ icon_living = "tunneler"
+ icon_dead = "tunneler_dead"
+
+ maxHealth = 120
+ health = 120
+
+ melee_damage_lower = 10
+ melee_damage_upper = 20
+
+ poison_chance = 15
+ poison_per_bite = 3
+ poison_type = "serotrotium_v"
+
+// ai_holder_type = /datum/ai_holder/simple_mob/melee/tunneler
+
+ player_msg = "You can perform a tunneling attack by clicking on someone from a distance while on natural terrain.
\
+ There is a noticable travel delay as you tunnel towards the tile the target was at when you started the tunneling attack.
\
+ Any entities inbetween you and the targeted tile will be stunned for a brief period of time.
\
+ Whatever is on the targeted tile when you arrive will suffer a potent stun.
\
+ If nothing is on the targeted tile, you will overshoot and keep going for a few more tiles.
\
+ If you hit a wall or other solid structure during that time, you will suffer a lengthy stun."
+
+ // Tunneling is a special attack, similar to the hunter's Leap.
+ special_attack_min_range = 2
+ special_attack_max_range = 6
+ special_attack_cooldown = 10 SECONDS
+
+ var/tunnel_warning = 0.5 SECONDS // How long the dig telegraphing is.
+ var/tunnel_tile_speed = 2 // How long to wait between each tile. Higher numbers result in an easier to dodge tunnel attack.
+
+/mob/living/simple_mob/animal/giant_spider/tunneler/frequent
+ special_attack_cooldown = 5 SECONDS
+
+/mob/living/simple_mob/animal/giant_spider/tunneler/fast
+ tunnel_tile_speed = 1
+
+/mob/living/simple_mob/animal/giant_spider/tunneler/do_special_attack(atom/A)
+ set waitfor = FALSE
+ set_AI_busy(TRUE)
+
+ // Save where we're gonna go soon.
+ var/turf/destination = get_turf(A)
+ var/turf/starting_turf = get_turf(src)
+
+ // Telegraph to give a small window to dodge if really close.
+ do_windup_animation(A, tunnel_warning)
+ sleep(tunnel_warning) // For the telegraphing.
+
+ // Do the dig!
+ visible_message(span("danger","\The [src] tunnels towards \the [A]!"))
+
+ if(handle_tunnel(destination) == FALSE)
+ set_AI_busy(FALSE)
+ return FALSE
+
+ // Did we make it?
+ if(!(src in destination))
+ set_AI_busy(FALSE)
+ return FALSE
+
+ var/overshoot = TRUE
+
+ // Test if something is at destination.
+ for(var/mob/living/L in destination)
+ if(L == src)
+ continue
+
+ visible_message(span("danger","\The [src] erupts from underneath, and hits \the [L]!"))
+ L.Weaken(3)
+ overshoot = FALSE
+
+ if(!overshoot) // We hit the target, or something, at destination, so we're done.
+ set_AI_busy(FALSE)
+ return TRUE
+
+ // Otherwise we need to keep going.
+ to_chat(src, span("warning", "You overshoot your target!"))
+ playsound(src, 'sound/weapons/punchmiss.ogg', 75, 1)
+ var/dir_to_go = get_dir(starting_turf, destination)
+ for(var/i = 1 to rand(2, 4))
+ destination = get_step(destination, dir_to_go)
+
+ if(handle_tunnel(destination) == FALSE)
+ set_AI_busy(FALSE)
+ return FALSE
+
+ set_AI_busy(FALSE)
+ return FALSE
+
+
+
+// Does the tunnel movement, stuns enemies, etc.
+/mob/living/simple_mob/animal/giant_spider/tunneler/proc/handle_tunnel(turf/destination)
+ var/turf/T = get_turf(src) // Hold our current tile.
+
+ // Regular tunnel loop.
+ for(var/i = 1 to get_dist(src, destination))
+ if(stat)
+ return FALSE // We died or got knocked out on the way.
+ if(loc == destination)
+ break // We somehow got there early.
+
+ // Update T.
+ T = get_step(src, get_dir(src, destination))
+ if(T.check_density(ignore_mobs = TRUE))
+ to_chat(src, span("critical", "You hit something really solid!"))
+ playsound(src, "punch", 75, 1)
+ Weaken(5)
+ return FALSE // Hit a wall.
+
+ // Stun anyone in our way.
+ for(var/mob/living/L in T)
+ L.Weaken(2)
+
+ // Get into the tile.
+ forceMove(T)
+
+ // Visuals and sound.
+ new /obj/item/weapon/ore/glass(T)
+ playsound(src, 'sound/effects/break_stone.ogg', 75, 1)
+ sleep(tunnel_tile_speed)
+
+
+
+/*
+// The actual leaping attack.
+/mob/living/simple_mob/animal/giant_spider/hunter/do_special_attack(atom/A)
+ set waitfor = FALSE
+ set_AI_busy(TRUE)
+
+ // Telegraph, since getting stunned suddenly feels bad.
+ do_windup_animation(A, leap_warmup)
+ sleep(leap_warmup) // For the telegraphing.
+
+ // Do the actual leap.
+ status_flags |= LEAPING // Lets us pass over everything.
+ visible_message(span("danger","\The [src] leaps at \the [A]!"))
+ throw_at(get_step(get_turf(A), get_turf(src)), special_attack_max_range+1, 1, src)
+ playsound(src, 'sound/weapons/spiderlunge.ogg', 75, 1)
+
+ sleep(5) // For the throw to complete. It won't hold up the AI ticker due to waitfor being false.
+
+ if(status_flags & LEAPING)
+ status_flags &= ~LEAPING // Revert special passage ability.
+
+ var/turf/T = get_turf(src) // Where we landed. This might be different than A's turf.
+
+ . = FALSE
+
+ // Now for the stun.
+ var/mob/living/victim = null
+ for(var/mob/living/L in T) // So player-controlled spiders only need to click the tile to stun them.
+ if(L == src)
+ continue
+
+ if(ishuman(L))
+ var/mob/living/carbon/human/H = L
+ if(H.check_shields(damage = 0, damage_source = src, attacker = src, def_zone = null, attack_text = "the leap"))
+ continue // We were blocked.
+
+ victim = L
+ break
+
+ if(victim)
+ victim.Weaken(2)
+ victim.visible_message(span("danger","\The [src] knocks down \the [victim]!"))
+ to_chat(victim, span("critical", "\The [src] jumps on you!"))
+ . = TRUE
+
+ set_AI_busy(FALSE)
+*/
+
+/*
+/mob/living/simple_animal/hostile/giant_spider/tunneler
+ desc = "Sandy and brown, it makes you shudder to look at it. This one has glittering yellow eyes."
+ icon_state = "tunneler"
+ icon_living = "tunneler"
+ icon_dead = "tunneler_dead"
+
+ maxHealth = 120
+ health = 120
+ move_to_delay = 4
+
+ melee_damage_lower = 10
+ melee_damage_upper = 20
+
+ poison_chance = 15
+ poison_per_bite = 3
+ poison_type = "serotrotium_v"
+
+/mob/living/simple_animal/hostile/giant_spider/tunneler/death()
+ spawn(1)
+ for(var/I = 1 to rand(3,6))
+ if(src)
+ new/obj/item/weapon/ore/glass(src.loc)
+ else
+ break
+ return ..()
+*/
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm
index fd0f5a8fbc..04afd25459 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm
@@ -3,11 +3,16 @@
whether the people want them to or not, and sometimes causing vandalism by accident.
They're pretty strong and have strong melee armor, but won't attack first.
They unknowingly play a role in keeping the shoreline fairly safe, by killing whatever would attack other people.
+
+ They also have a slow, but very strong attack that is telegraphed. If it hits, it will briefly stun whatever got hit
+ and inflict a very large chunk of damage. If the thing was already stunned, the crab will 'throw' them away, to
+ hopefully prevent chainstuns forever.
*/
/mob/living/simple_mob/animal/sif/hooligan_crab
name = "hooligan crab"
- desc = "A large, hard-shelled crustacean. This one is mostly grey."
+ desc = "A large, hard-shelled crustacean. This one is mostly grey. \
+ You probably shouldn't mess with it."
icon_state = "sif_crab"
icon_living = "sif_crab"
icon_dead = "sif_crab_dead"
@@ -19,6 +24,7 @@
health = 200
movement_cooldown = 10
movement_sound = 'sound/weapons/heavysmash.ogg'
+ taser_kill = 0
armor = list(
"melee" = 40,
"bullet" = 20,
@@ -45,6 +51,7 @@
attack_armor_pen = 35
attack_sharp = 1
attack_edge = 1
+ attack_delay = 1 SECOND
meat_type = /obj/item/weapon/reagent_containers/food/snacks/meat
response_help = "pets"
@@ -57,3 +64,19 @@
ai_holder_type = /datum/ai_holder/simple_mob/melee/hooligan
say_list_type = /datum/say_list/crab
+ var/weaken_amount = 2 // Be careful with this number. High values will equal a permastun.
+
+// Stuns the thing that got hit briefly.
+/mob/living/simple_mob/animal/sif/hooligan_crab/apply_melee_effects(atom/A)
+ if(isliving(A))
+ var/mob/living/L = A
+ var/was_stunned = L.incapacitated(INCAPACITATION_DISABLED)
+ L.Weaken(weaken_amount)
+
+ playsound(L, 'sound/effects/break_stone.ogg', 75, 1)
+ if(was_stunned) // Try to prevent chain-stuns by having them thrown.
+ var/throwdir = get_dir(src, L)
+ L.throw_at(get_edge_target_turf(L, throwdir), 5, 1, src)
+ visible_message(span("danger", "\The [src] hurls \the [L] away!"))
+ else
+ visible_message(span("danger", "\The [src] crushes \the [L]!"))
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm
index 7fd21b43c7..7a800056f6 100644
--- a/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm
@@ -1,6 +1,5 @@
/mob/living/simple_mob/mechanical
- mob_class = MOB_CLASS_CONSTRUCT
-
+ mob_class = MOB_CLASS_SYNTHETIC
min_oxy = 0
max_oxy = 0
min_tox = 0
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 10be531825..81ced7daa3 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -54,6 +54,9 @@ proc/isdeaf(A)
/mob/proc/break_cloak()
return
+/mob/proc/is_cloaked()
+ return FALSE
+
proc/hasorgans(A) // Fucking really??
return ishuman(A)
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 1bbb1d7d76..c1b5a8963a 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/polaris.dme b/polaris.dme
index 2064763b89..203a9d0529 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1946,8 +1946,17 @@
#include "code\modules\mob\living\simple_mob\simple_hud.dm"
#include "code\modules\mob\living\simple_mob\simple_mob.dm"
#include "code\modules\mob\living\simple_mob\subtypes\animal\animal.dm"
-#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\carrier.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\electric.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\frost.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\giant_spider.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\hunter.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\lurker.dm"
#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\nurse.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\pepper.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\phorogenic.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\thermic.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\tunneler.dm"
#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\hooligan_crab.dm"
#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\sif.dm"
#include "code\modules\mob\living\simple_mob\subtypes\mechanical\combat_drone.dm"