diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm
index f610c0c2a3..881ba74c27 100644
--- a/code/__defines/mobs.dm
+++ b/code/__defines/mobs.dm
@@ -288,6 +288,19 @@
#define SA_ROBOTIC 3
#define SA_HUMANOID 4
+// 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_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)
+
// For slime commanding. Higher numbers allow for more actions.
#define SLIME_COMMAND_OBEY 1 // When disciplined.
#define SLIME_COMMAND_FACTION 2 // When in the same 'faction'.
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index d3b9853e06..5f0a1e7f45 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -56,9 +56,11 @@
/obj/effect/spider/stickyweb
icon_state = "stickyweb1"
- New()
- if(prob(50))
- icon_state = "stickyweb2"
+
+/obj/effect/spider/stickyweb/initialize()
+ if(prob(50))
+ icon_state = "stickyweb2"
+ return ..()
/obj/effect/spider/stickyweb/CanPass(atom/movable/mover, turf/target, height=0, air_group=0)
if(air_group || (height==0)) return 1
@@ -80,10 +82,12 @@
var/spiders_min = 6
var/spiders_max = 24
var/spider_type = /obj/effect/spider/spiderling
- New()
- pixel_x = rand(3,-3)
- pixel_y = rand(3,-3)
- processing_objects |= src
+
+/obj/effect/spider/eggcluster/initialize()
+ pixel_x = rand(3,-3)
+ pixel_y = rand(3,-3)
+ processing_objects |= src
+ return ..()
/obj/effect/spider/eggcluster/New(var/location, var/atom/parent)
get_light_and_color(parent)
diff --git a/code/modules/ai/_defines.dm b/code/modules/ai/_defines.dm
index 354f428e88..c72158134e 100644
--- a/code/modules/ai/_defines.dm
+++ b/code/modules/ai/_defines.dm
@@ -14,6 +14,12 @@
#define AI_LOG_DEBUG 4 // More detailed information about the flow of execution.
#define AI_LOG_TRACE 5 // Even more detailed than the last.
+// Results of pre-movement checks.
+// Todo: Move outside AI code?
+#define MOVEMENT_ON_COOLDOWN -1 // Recently moved and needs to try again soon.
+#define MOVEMENT_FAILED 0 // Move() returned false for whatever reason and the mob didn't move.
+#define MOVEMENT_SUCCESSFUL 1 // Move() returned true and the mob hopefully moved.
+
// Reasons for targets to not be valid. Based on why, the AI responds differently.
#define AI_TARGET_VALID 0 // We can fight them.
#define AI_TARGET_INVIS 1 // They were in field of view but became invisible. Switch to STANCE_BLINDFIGHT if no other viable targets exist.
diff --git a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
new file mode 100644
index 0000000000..206ce2c705
--- /dev/null
+++ b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
@@ -0,0 +1,144 @@
+// AIs for simple mobs.
+
+/datum/ai_holder/simple_mob
+ hostile = TRUE // The majority of simplemobs are hostile.
+ cooperative = TRUE
+ returns_home = TRUE
+ can_flee = FALSE
+ speak_chance = 1 // If the mob's saylist is empty, nothing will happen.
+
+// For animals.
+/datum/ai_holder/simple_mob/passive
+ hostile = FALSE
+ wander = TRUE
+ can_flee = TRUE
+
+// Ranged mobs.
+
+/datum/ai_holder/simple_mob/ranged
+ ranged = TRUE
+
+// Tries to not waste ammo.
+/datum/ai_holder/simple_mob/ranged/careful
+ conserve_ammo = TRUE
+
+// Runs away from its target if within a certain distance.
+/datum/ai_holder/simple_mob/ranged/kiting
+ pointblank = TRUE // So we don't need to copypaste post_melee_attack().
+ var/run_if_this_close = 4 // If anything gets within this range, it'll try to move away.
+
+/datum/ai_holder/simple_mob/ranged/kiting/threatening
+ threaten = TRUE
+ threaten_delay = 1 SECOND // Less of a threat and more of pre-attack notice.
+ threaten_timeout = 30 SECONDS
+
+/datum/ai_holder/simple_mob/ranged/kiting/post_ranged_attack(atom/A)
+ if(get_dist(holder, A) < run_if_this_close)
+ holder.IMove(get_step_away(holder, A, run_if_this_close))
+ holder.face_atom(A)
+
+
+// Melee mobs.
+
+/datum/ai_holder/simple_mob/melee
+
+// Dances around the enemy its fighting, making it harder to fight back.
+/datum/ai_holder/simple_mob/melee/evasive
+
+/datum/ai_holder/simple_mob/melee/evasive/post_melee_attack(atom/A)
+ if(holder.Adjacent(A))
+ holder.IMove(get_step(holder, pick(alldirs)))
+ holder.face_atom(A)
+
+// The AI for hooligan crabs. Follows people for awhile.
+/datum/ai_holder/simple_mob/melee/hooligan
+ hostile = FALSE
+ retaliate = TRUE
+ max_home_distance = 12
+ var/random_follow = TRUE // Turn off if you want to bus with crabs.
+
+/datum/ai_holder/simple_mob/melee/hooligan/handle_stance_strategical()
+ ..()
+ if(random_follow && stance == STANCE_IDLE && !leader)
+ if(prob(10))
+ for(var/mob/living/L in hearers(holder))
+ if(!istype(L, holder)) // Don't follow other hooligan crabs.
+ holder.visible_message("\The [holder] starts to follow \the [L].")
+ set_follow(L, rand(20 SECONDS, 40 SECONDS))
+
+
+// The AI for nurse spiders. Wraps things in webs by 'attacking' them.
+/datum/ai_holder/simple_mob/melee/nurse_spider
+ wander = TRUE
+ base_wander_delay = 8
+
+// Get us unachored objects as an option as well.
+/datum/ai_holder/simple_mob/melee/nurse_spider/list_targets()
+ . = ..()
+
+ var/static/alternative_targets = typecacheof(list(/obj/item, /obj/structure))
+
+ for(var/AT in typecache_filter_list(range(vision_range, holder), alternative_targets))
+ var/obj/O = AT
+ if(can_see(holder, O, vision_range) && !O.anchored)
+ . += O
+
+// Select an obj if no mobs are around.
+/datum/ai_holder/melee/nurse_spider/pick_target(list/targets)
+ var/mobs_only = locate(/mob/living) in targets // If a mob is in the list of targets, then ignore objects.
+ if(mobs_only)
+ for(var/A in targets)
+ if(!isliving(A))
+ targets -= A
+
+ 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
+
+/*
+
+
+
+/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
+*/
+
+/*
+ . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
+
+ var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
+
+ for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
+ if(can_see(holder, HM, vision_range))
+ . += HM
+*/
\ No newline at end of file
diff --git a/code/modules/ai/aI_holder_subtypes/slime_ai.dm b/code/modules/ai/aI_holder_subtypes/slime_ai.dm
new file mode 100644
index 0000000000..905bb4b109
--- /dev/null
+++ b/code/modules/ai/aI_holder_subtypes/slime_ai.dm
@@ -0,0 +1,102 @@
+// Specialized AI for slime simplemobs.
+// Unlike the parent AI code, this will probably break a lot of things if you put it on something that isn't /mob/living/simple_animal/slime
+
+/datum/ai_holder/normal/slime
+
+/datum/ai_holder/normal/slime/New()
+ ..()
+ ASSERT(istype(holder, /mob/living/simple_animal/slime))
+
+/datum/ai_holder/normal/slime/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
+ var/mob/living/simple_animal/slime/me = holder
+ if(me.victim) // Don't worry about finding another target if we're eatting someone.
+ return
+ if(leader && me.can_command(leader)) // If following someone, don't attack until the leader says so, something hits you, or the leader is no longer worthy.
+ return
+ ..()
+
+/datum/ai_holder/normal/slime/found(mob/living/L)
+ var/mob/living/simple_animal/slime/me = holder
+ if(isliving(L))
+ if(can_attack(L))
+ if(L.faction == me.faction && !attack_same_faction)
+ if(ishuman(L))
+ var/mob/living/carbon/human/H = L
+ if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too.
+ return H // Monkeys are always food.
+ else
+ return
+
+ // if(L in friends)
+ // return
+
+ if(istype(L, /mob/living/simple_animal/slime))
+ var/mob/living/simple_animal/slime/buddy = L
+ if(buddy.slime_color == me.slime_color || me.discipline || me.unity || buddy.unity)
+ return // Don't hurt same colored slimes.
+ else
+ return buddy //do hurt others
+
+ if(ishuman(L))
+ var/mob/living/carbon/human/H = L
+ if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too.
+ return H // Monkeys are always food.
+
+ if(issilicon(L) || isbot(L))
+ if(me.discipline && !me.rabid)
+ return // We're a good slime. For now at least.
+ return
+ return
+
+/datum/ai_holder/normal/slime/closest_distance()
+ if(isliving(target))
+ var/mob/living/L = target
+ if(L.stat)
+ return 1 // Melee (eat) the target if dying, don't shoot it.
+ return ..()
+
+
+/datum/ai_holder/normal/slime/help_requested(var/mob/living/simple_animal/slime/buddy)
+ var/mob/living/simple_animal/slime/me = holder
+ if(istype(buddy))
+ if(buddy.slime_color != me.slime_color && (!me.unity || !buddy.unity)) // We only help slimes of the same color, if it's another slime calling for help.
+ // ai_log("HelpRequested() by [buddy] but they are a [buddy.slime_color] while we are a [src.slime_color].",2)
+ ai_log("help_requested() : Help was requested by [buddy] but they are a [buddy.slime_color] while we are a [me.slime_color].", AI_LOG_INFO)
+ return
+ ..()
+
+
+/datum/ai_holder/normal/slime/handle_resist()
+ var/mob/living/simple_animal/slime/me = holder
+ if(me.buckled && me.victim && isliving(me.buckled) && me.victim == me.buckled) // If it's buckled to a living thing it's probably eating it.
+ return
+ else
+ ..()
+
+
+/datum/ai_holder/normal/slime/melee_attack(atom/A)
+ var/mob/living/simple_animal/slime/me = holder
+ if(isliving(A))
+ var/mob/living/L = A
+ if( (!L.lying && prob(60 + (me.power_charge * 4) ) || (!L.lying && me.optimal_combat) )) // "Smart" slimes always stun first.
+ me.a_intent = I_DISARM // Stun them first.
+ else if(me.can_consume(L) && L.lying)
+ me.a_intent = I_GRAB // Then eat them.
+ else
+ me.a_intent = I_HURT // Otherwise robust them.
+ ..(A)
+
+/*
+/mob/living/simple_animal/slime/PunchTarget()
+ if(victim)
+ return // Already eatting someone.
+ if(!client) // AI controlled.
+ if( (!target_mob.lying && prob(60 + (power_charge * 4) ) || (!target_mob.lying && optimal_combat) )) // "Smart" slimes always stun first.
+ a_intent = I_DISARM // Stun them first.
+ else if(can_consume(target_mob) && target_mob.lying)
+ a_intent = I_GRAB // Then eat them.
+ else
+ a_intent = I_HURT // Otherwise robust them.
+ ai_log("PunchTarget() will [a_intent] [target_mob]",2)
+ ..()
+*/
\ No newline at end of file
diff --git a/code/modules/ai/ai_holder.dm b/code/modules/ai/ai_holder.dm
index 41c5210833..f41627fd4f 100644
--- a/code/modules/ai/ai_holder.dm
+++ b/code/modules/ai/ai_holder.dm
@@ -19,6 +19,10 @@
var/mob/living/holder = null // The mob this datum is going to control.
var/stance = STANCE_IDLE // Determines if the mob should be doing a specific thing, e.g. attacking, following, standing around, etc.
var/intelligence_level = AI_NORMAL // Adjust to make the AI be intentionally dumber, or make it more robust (e.g. dodging grenades).
+ var/autopilot = FALSE // If true, the AI won't be deactivated if a client gets attached to the AI's mob.
+ var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the
+ // mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time,
+ // consider sleeping the AI instead.
@@ -73,11 +77,14 @@
// 'Tactical' processes such as moving a step, meleeing an enemy, firing a projectile, and other fairly cheap actions that need to happen quickly.
/datum/ai_holder/proc/handle_tactics()
+ if(busy)
+ return
handle_stance_tactical()
// 'Strategical' processes that are more expensive on the CPU and so don't get run as often as the above proc, such as A* pathfinding or robust targeting.
/datum/ai_holder/proc/handle_strategicals()
-// world << "[holder.name] Strategicals!"
+ if(busy)
+ return
handle_stance_strategical()
/*
@@ -301,3 +308,23 @@
return FALSE
return TRUE
+
+// Helper proc to turn AI 'busy' mode on or off without having to check if there is an AI, to simplify writing code.
+/mob/living/proc/set_AI_busy(value)
+ if(ai_holder)
+ ai_holder.busy = value
+
+/mob/living/proc/is_AI_busy()
+ if(!ai_holder)
+ return FALSE
+ return ai_holder.busy
+
+// Helper proc to check for the AI's stance.
+// Returns null if there's no AI holder, or the mob has a player and autopilot is not on.
+// Otherwise returns the stance.
+/mob/living/proc/get_AI_stance()
+ if(!ai_holder)
+ return null
+ if(client && !ai_holder.autopilot)
+ return null
+ return ai_holder.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 d749761d1e..1f904c1f24 100644
--- a/code/modules/ai/ai_holder_combat.dm
+++ b/code/modules/ai/ai_holder_combat.dm
@@ -86,12 +86,16 @@
set_stance(STANCE_APPROACH)
// We're not entirely sure how holder will do melee attacks since any /mob/living could be holder, but we don't have to care because Interfaces.
-/datum/ai_holder/proc/melee_attack(atom/movable/AM)
- return holder.IAttack(AM)
+/datum/ai_holder/proc/melee_attack(atom/A)
+ . = holder.IAttack(A)
+ if(.)
+ post_melee_attack(A)
// Ditto.
-/datum/ai_holder/proc/ranged_attack(atom/movable/AM)
- return holder.IRangedAttack(AM)
+/datum/ai_holder/proc/ranged_attack(atom/A)
+ . = holder.IRangedAttack(A)
+ if(.)
+ post_ranged_attack(A)
// Most mobs probably won't have this defined but we don't care.
/datum/ai_holder/proc/special_attack(atom/movable/AM)
@@ -100,7 +104,13 @@
// Called when within striking/shooting distance, however cooldown is not considered.
// Override to do things like move in a random step for evasiveness.
// Note that this is called BEFORE the attack.
-/datum/ai_holder/proc/on_engagement(atom/movable/AM)
+/datum/ai_holder/proc/on_engagement(atom/A)
+
+// Called after a successful (IE not on cooldown) ranged attack.
+/datum/ai_holder/proc/post_ranged_attack(atom/A)
+
+// Ditto but for melee.
+/datum/ai_holder/proc/post_melee_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)
diff --git a/code/modules/ai/ai_holder_communication.dm b/code/modules/ai/ai_holder_communication.dm
index ba49fa29e6..9ed3d53728 100644
--- a/code/modules/ai/ai_holder_communication.dm
+++ b/code/modules/ai/ai_holder_communication.dm
@@ -7,17 +7,9 @@
var/threaten_timeout = 1 MINUTE // If the mob threatens someone, they leave, and then come back before this timeout period, the mob escalates to fighting immediately.
var/last_conflict_time = null // Last occurance of threatening or fighting being used, in world.time.
- var/threaten_sound = null // Sound file played when the mob calls threaten_target() for the first time.
- var/stand_down_sound = null // Sound file played when the mob loses sight of the threatened target.
-
var/speak_chance = 0 // Probability that the mob talks (this is 'X in 200' chance since even 1/100 is pretty noisy)
var/reacts = 0 // Reacts to some things being said.
- var/datum/say_list/say_list = null // Datum containing all of our lines.
- var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent.
-/datum/ai_holder/New()
- ..()
- say_list = new say_list_type()
/datum/ai_holder/proc/should_threaten()
if(!threaten)
@@ -37,25 +29,28 @@
threatening = TRUE
last_conflict_time = world.time
- holder.say(safepick(say_list.say_threaten))
- playsound(holder.loc, threaten_sound, 75, 1) // We do this twice to make the sound -very- noticable to the target.
- playsound(target.loc, threaten_sound, 75, 1) // Actual aim-mode also does that so at least it's consistant.
+ if(holder.say_list)
+ holder.say(safepick(holder.say_list.say_threaten))
+ playsound(holder.loc, holder.say_list.threaten_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
+ playsound(target.loc, holder.say_list.threaten_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
else // Otherwise we are waiting for them to go away or to wait long enough for escalate.
if(target in list_targets()) // Are they still visible?
if(threaten_delay && last_conflict_time + threaten_delay < world.time) // Waited too long.
threatening = FALSE
set_stance(STANCE_APPROACH)
- holder.say(safepick(say_list.say_escalate))
+ if(holder.say_list)
+ holder.say(safepick(holder.say_list.say_escalate))
else
return // Wait a bit.
else // They left, or so we think.
threatening = FALSE
set_stance(STANCE_IDLE)
- holder.say(safepick(say_list.say_stand_down))
- playsound(holder.loc, stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
- playsound(target.loc, stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
+ if(holder.say_list)
+ holder.say(safepick(holder.say_list.say_stand_down))
+ playsound(holder.loc, holder.say_list.stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
+ playsound(target.loc, holder.say_list.stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
// Determines what is deserving of a warning when STANCE_ALERT is active.
/datum/ai_holder/proc/will_threaten(mob/living/the_target)
@@ -75,13 +70,25 @@
/datum/ai_holder/proc/handle_idle_speaking()
if(rand(0,200) < speak_chance)
- var/list/comm_types = list() // What kinds of things can we do?
+ // Check if anyone is around to 'appreciate' what we say.
+ var/alone = TRUE
+ for(var/m in viewers(holder))
+ var/mob/M = m
+ if(M.client)
+ alone = FALSE
+ break
+ if(alone) // Forever alone. No point doing anything else.
+ return
- if(say_list.speak.len)
+ var/list/comm_types = list() // What kinds of things can we do?
+ if(!holder.say_list)
+ return
+
+ if(holder.say_list.speak.len)
comm_types += COMM_SAY
- if(say_list.emote_hear.len)
+ if(holder.say_list.emote_hear.len)
comm_types += COMM_AUDIBLE_EMOTE
- if(say_list.emote_see.len)
+ if(holder.say_list.emote_see.len)
comm_types += COMM_VISUAL_EMOTE
if(!comm_types.len)
@@ -89,11 +96,11 @@
switch(pick(comm_types))
if(COMM_SAY)
- holder.say(safepick(say_list.speak))
+ holder.say(safepick(holder.say_list.speak))
if(COMM_AUDIBLE_EMOTE)
- holder.audible_emote(safepick(say_list.emote_hear))
+ holder.audible_emote(safepick(holder.say_list.emote_hear))
if(COMM_VISUAL_EMOTE)
- holder.visible_emote(safepick(say_list.emote_see))
+ holder.visible_emote(safepick(holder.say_list.emote_see))
#undef COMM_SAY
#undef COMM_AUDIBLE_EMOTE
diff --git a/code/modules/ai/ai_holder_debug.dm b/code/modules/ai/ai_holder_debug.dm
index 538d74e895..c4b2f990a0 100644
--- a/code/modules/ai/ai_holder_debug.dm
+++ b/code/modules/ai/ai_holder_debug.dm
@@ -102,9 +102,6 @@
threaten = TRUE
wander = TRUE
- threaten_sound = 'sound/weapons/TargetOn.ogg'
- stand_down_sound = 'sound/weapons/TargetOff.ogg'
-
/datum/ai_holder/hostile/ranged/debug
wander = FALSE
conserve_ammo = FALSE
@@ -115,8 +112,6 @@
last_turf_display = TRUE
debug_ai = AI_LOG_INFO
- say_list_type = /datum/say_list/pirate
-
/mob/living/simple_animal/hostile/pirate
hostile = FALSE
ai_inactive = TRUE
diff --git a/code/modules/ai/ai_holder_fleeing.dm b/code/modules/ai/ai_holder_fleeing.dm
index bde630ef57..b9c45ace93 100644
--- a/code/modules/ai/ai_holder_fleeing.dm
+++ b/code/modules/ai/ai_holder_fleeing.dm
@@ -16,7 +16,7 @@
return TRUE
if(can_flee)
- if(!hostile)
+ if(!hostile && !retaliate)
return TRUE // We're not hostile and someone attacked us first.
if(flee_when_dying && (holder.health / holder.getMaxHealth()) <= dying_threshold)
return TRUE // We're gonna die!
diff --git a/code/modules/ai/ai_holder_follow.dm b/code/modules/ai/ai_holder_follow.dm
index a9f1433c6b..9329fe53d7 100644
--- a/code/modules/ai/ai_holder_follow.dm
+++ b/code/modules/ai/ai_holder_follow.dm
@@ -57,4 +57,12 @@
ai_log("lose_follow() : Exited.", AI_LOG_DEBUG)
/datum/ai_holder/proc/should_follow_leader()
- return leader && get_dist(holder, leader) > follow_distance
\ No newline at end of file
+ if(!leader)
+ return FALSE
+ if(follow_until_time && world.time > follow_until_time)
+ lose_follow()
+ set_stance(STANCE_IDLE)
+ return FALSE
+ if(get_dist(holder, leader) > follow_distance)
+ return TRUE
+ return FALSE
\ No newline at end of file
diff --git a/code/modules/ai/ai_holder_movement.dm b/code/modules/ai/ai_holder_movement.dm
index 3a15fa5dc1..55b2e2ce34 100644
--- a/code/modules/ai/ai_holder_movement.dm
+++ b/code/modules/ai/ai_holder_movement.dm
@@ -6,7 +6,9 @@
// Home.
var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so.
var/returns_home = FALSE // If true, makes the mob go to its 'home' if it strays too far.
+ var/home_low_priority = FALSE // If true, the mob will not go home unless it has nothing better to do, e.g. its following someone.
var/max_home_distance = 3 // How far the mob can go away from its home before being told to go_home().
+ // Note that there is a 'BYOND cap' of 14 due to limitations of get_/step_to().
// Wandering.
var/wander = FALSE // If true, the mob will randomly move in the four cardinal directions when idle.
@@ -15,7 +17,6 @@
var/wander_when_pulled = FALSE // If the mob will refrain from wandering if someone is pulling it.
-
/datum/ai_holder/proc/walk_to_destination()
ai_log("walk_to_destination() : Entering.",AI_LOG_DEBUG)
if(!destination)
@@ -41,7 +42,15 @@
ai_log("walk_to_destination() : Exiting.",AI_LOG_DEBUG)
/datum/ai_holder/proc/should_go_home()
- return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance)
+ if(!returns_home || !home_turf)
+ return FALSE
+ if(get_dist(holder, home_turf) > max_home_distance)
+ if(!home_low_priority)
+ return TRUE
+ else if(!leader && !target)
+ return TRUE
+ return FALSE
+// return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance)
/datum/ai_holder/proc/go_home()
if(home_turf)
@@ -70,7 +79,6 @@
// Walk towards whatever.
/datum/ai_holder/proc/walk_path(atom/A, get_to = 1)
ai_log("walk_path() : Entered.", AI_LOG_DEBUG)
- var/turf/pre_step_turf = get_turf(holder)
if(use_astar)
if(!path.len) // If we're missing a path, make a new one.
@@ -78,13 +86,14 @@
calculate_path(A, get_to)
if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us. Get as close as we can.
- ai_log("walk_path() : Failed to obtain path to target. Using step_to() instead.", AI_LOG_INFO)
- step_to(holder, A)
- if(get_turf(holder) == pre_step_turf)
+ ai_log("walk_path() : Failed to obtain path to target. Using get_step_to() instead.", AI_LOG_INFO)
+ // step_to(holder, A)
+ if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED)
+ ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_TRACE)
breakthrough(A) // We failed to move, time to smash things.
return
- if(!move_once()) // Start walking the path.
+ if(move_once() == FALSE) // Start walking the path.
ai_log("walk_path() : Failed to step.", AI_LOG_TRACE)
++failed_steps
if(failed_steps > 3) // We're probably stuck.
@@ -93,8 +102,10 @@
failed_steps = 0
else
- step_to(holder, A)
- if(get_turf(holder) == pre_step_turf)
+ // step_to(holder, A)
+ ai_log("walk_path() : Going to IMove().", AI_LOG_TRACE)
+ if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED )
+ ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_TRACE)
breakthrough(A) // We failed to move, time to smash things.
ai_log("walk_path() : Exited.", AI_LOG_DEBUG)
@@ -110,14 +121,17 @@
var/turf/T = src.path[1]
T.overlays -= path_overlay
- step_towards(holder, src.path[1])
- if(holder.loc != src.path[1])
- ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE)
- return FALSE
- else
- path -= src.path[1]
- ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE)
- return TRUE
+// step_towards(holder, src.path[1])
+ if(holder.IMove(get_step_towards(holder, src.path[1])) != MOVEMENT_ON_COOLDOWN)
+ if(holder.loc != src.path[1])
+ ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE)
+ return MOVEMENT_FAILED
+ else
+ path -= src.path[1]
+ ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE)
+ return MOVEMENT_SUCCESSFUL
+ ai_log("move_once() : Mob movement on cooldown. Exiting.", AI_LOG_TRACE)
+ return MOVEMENT_ON_COOLDOWN
/datum/ai_holder/proc/should_wander()
return wander && !leader
@@ -135,6 +149,6 @@
var/moving_to = 0 // Apparently this is required or it always picks 4, according to the previous developer for simplemob AI.
moving_to = pick(cardinal)
holder.set_dir(moving_to)
- holder.Move(get_step(holder,moving_to))
+ holder.IMove(get_step(holder,moving_to))
wander_delay = base_wander_delay
ai_log("handle_wander_movement() : Exited.", AI_LOG_DEBUG)
diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm
index 5de7b2890c..3eaa33f449 100644
--- a/code/modules/ai/ai_holder_targeting.dm
+++ b/code/modules/ai/ai_holder_targeting.dm
@@ -1,8 +1,8 @@
// Used for assigning a target for attacking.
/datum/ai_holder
- var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing.
- var/retaliate = FALSE // Attacks whatever struck it first.
+ var/hostile = FALSE // Do we try to hurt others?
+ 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/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.
@@ -26,6 +26,8 @@
// Step 2, filter down possible targets to things we actually care about.
/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
+ if(!hostile) // So retaliating mobs only attack the thing that hit it.
+ return null
. = list()
if(!has_targets_list)
possible_targets = list_targets()
@@ -45,12 +47,13 @@
// Step 3, pick among the possible, attackable targets.
/datum/ai_holder/proc/pick_target(list/targets)
if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
- for(var/possible_target in targets)
- var/atom/A = possible_target
- var/target_dist = get_dist(holder, target)
- var/possible_target_distance = get_dist(holder, A)
- if(target_dist < possible_target_distance)
- targets -= A
+ targets = target_filter_distance(targets)
+// for(var/possible_target in targets)
+// var/atom/A = possible_target
+// var/target_dist = get_dist(holder, target)
+// var/possible_target_distance = get_dist(holder, A)
+// if(target_dist < possible_target_distance)
+// targets -= A
if(!targets.len) // We found nothing.
return
var/chosen_target = pick(targets)
@@ -66,6 +69,18 @@
set_stance(STANCE_APPROACH)
return TRUE
+// Filters return one or more 'preferred' targets.
+
+// This one is for closest targets.
+/datum/ai_holder/proc/target_filter_distance(list/targets)
+ for(var/possible_target in targets)
+ var/atom/A = possible_target
+ var/target_dist = get_dist(holder, target)
+ var/possible_target_distance = get_dist(holder, A)
+ if(target_dist < possible_target_distance)
+ targets -= A
+ return targets
+
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
if(!can_see_target(the_target))
return FALSE
@@ -156,4 +171,31 @@
if(last_turf_display && target_last_seen_turf)
target_last_seen_turf.overlays -= last_turf_overlay
ai_log("lose_target_position() : Last position is being reset.", AI_LOG_INFO)
- target_last_seen_turf = null
\ No newline at end of file
+ target_last_seen_turf = null
+
+// Responds to a hostile action against its mob.
+/datum/ai_holder/proc/react_to_attack(atom/movable/attacker)
+ if(holder.stat) // We're dead.
+ ai_log("react_to_attack() : Was attacked by [attacker], but we are dead/unconscious.", AI_LOG_TRACE)
+ return FALSE
+ if(!hostile && !retaliate) // Not allowed to defend ourselves.
+ ai_log("react_to_attack() : Was attacked by [attacker], but we are not allowed to attack back.", AI_LOG_TRACE)
+ return FALSE
+ if(holder.IIsAlly(attacker)) // I'll overlook it THIS time...
+ ai_log("react_to_attack() : Was attacked by [attacker], but they were an ally.", AI_LOG_TRACE)
+ return FALSE
+ if(target) // Already fighting someone. Switching every time we get hit would impact our combat performance.
+ ai_log("react_to_attack() : Was attacked by [attacker], but we already have a target.", AI_LOG_TRACE)
+ return FALSE
+
+ if(stance == STANCE_SLEEP) // If we're asleep, try waking up if someone's wailing on us.
+ ai_log("react_to_attack() : AI is asleep. Waking up.", AI_LOG_TRACE)
+ go_wake()
+
+ 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
diff --git a/code/modules/ai/interfaces.dm b/code/modules/ai/interfaces.dm
index b38b1ce7a3..9ef507ceeb 100644
--- a/code/modules/ai/interfaces.dm
+++ b/code/modules/ai/interfaces.dm
@@ -6,20 +6,20 @@
/mob/living/proc/IAttack(atom/A)
return FALSE
-/mob/living/simple_animal/IAttack(atom/A)
+/mob/living/simple_mob/IAttack(atom/A)
return attack_target(A)
/mob/living/proc/IRangedAttack(atom/A)
return FALSE
-/mob/living/simple_animal/IRangedAttack(atom/A)
+/mob/living/simple_mob/IRangedAttack(atom/A)
return shoot_target(A)
/mob/living/proc/ISpecialAttack(atom/A)
return FALSE
-/mob/living/simple_animal/ISpecialAttack(atom/A)
- return special_attack_target()
+/mob/living/simple_mob/ISpecialAttack(atom/A)
+ return special_attack_target(A)
/mob/living/proc/ISay(message)
@@ -31,8 +31,32 @@
if(!.) // Outside the faction, try to see if they're friends.
return L in friends
+/mob/living/simple_mob/IIsAlly(mob/living/L)
+ . = ..()
+ if(!.) // Outside the faction, try to see if they're friends.
+ return L in friends
+
/mob/living/proc/IGetID()
/mob/living/simple_animal/IGetID()
if(myid)
- return myid.GetID()
\ No newline at end of file
+ return myid.GetID()
+
+// Respects move cooldowns as if it had a client.
+/mob/living/proc/IMove(newloc)
+ if(check_move_cooldown())
+// if(!newdir)
+// newdir = get_dir(get_turf(src), newloc)
+
+ // Move()ing to another tile successfully returns 32 because BYOND. Would rather deal with TRUE/FALSE-esque terms.
+ // Note that moving to the same tile will be 'successful'.
+ var/turf/old_T = get_turf(src)
+ . = SelfMove(newloc) ? MOVEMENT_SUCCESSFUL : MOVEMENT_FAILED
+ if(. == MOVEMENT_SUCCESSFUL)
+ set_dir(get_dir(old_T, newloc))
+ // Apply movement delay.
+ // Player movement has more factors but its all in the client and fixing that would be its own project.
+ setMoveCooldown(movement_delay())
+ return
+
+ . = MOVEMENT_ON_COOLDOWN // To avoid superfast mobs that aren't meant to be superfast. Is actually -1.
diff --git a/code/modules/ai/say_list.dm b/code/modules/ai/say_list.dm
index 29cd476eee..37f14823e5 100644
--- a/code/modules/ai/say_list.dm
+++ b/code/modules/ai/say_list.dm
@@ -1,13 +1,29 @@
-// A simple datum that just holds many lists of lines for AI mobs to pick from.
+// A simple datum that just holds many lists of lines for mobs to pick from.
// This is its own datum in order to be able to have different types of mobs be able to use the same lines if desired,
// even when inheritence wouldn't be able to do so.
// Also note this also contains emotes, despite its name.
+// and now sounds because its probably better that way.
+
+/mob/living
+ var/datum/say_list/say_list = null
+ var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent.
+
+/mob/living/initialize()
+ if(say_list_type)
+ say_list = new say_list_type(src)
+ return ..()
+
+/mob/living/Destroy()
+ qdel_null(say_list)
+ return ..()
+
/datum/say_list
var/list/speak = list() // Things the mob might say if it talks while idle.
var/list/emote_hear = list() // Hearable emotes it might perform
var/list/emote_see = list() // Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps
+
var/list/say_understood = list() // When accepting an order.
var/list/say_cannot = list() // When they cannot comply.
var/list/say_maybe_target = list() // When they briefly see something.
@@ -16,6 +32,11 @@
var/list/say_stand_down = list() // When the threatened thing goes away.
var/list/say_escalate = list() // When the threatened thing doesn't go away.
+ var/threaten_sound = null // Sound file played when the mob's AI calls threaten_target() for the first time.
+ var/stand_down_sound = null // Sound file played when the mob's AI loses sight of the threatened target.
+
+
+
@@ -25,10 +46,38 @@
// This one's pretty dumb, but pirates are dumb anyways and it makes for a good test.
/datum/say_list/pirate
speak = list("Yarr!")
+
say_understood = list("Alright, matey.")
say_cannot = list("No, matey.")
say_maybe_target = list("Eh?")
say_got_target = list("Yarrrr!")
say_threaten = list("You best leave, this booty is mine.", "No plank to walk on, just walk away.")
say_stand_down = list("Good.")
- say_escalate = list("Yarr! The booty is mine!")
\ No newline at end of file
+ say_escalate = list("Yarr! The booty is mine!")
+
+/datum/say_list/malf_drone
+ speak = list("ALERT.","Hostile-ile-ile entities dee-twhoooo-wected.","Threat parameterszzzz- szzet.","Bring sub-sub-sub-systems uuuup to combat alert alpha-a-a.")
+ emote_see = list("beeps menacingly","whirrs threateningly","scans its immediate vicinity")
+
+ say_understood = list("Affirmative.", "Positive.")
+ say_cannot = list("Denied.", "Negative.")
+ say_maybe_target = list("Possible threat detected. Investigating.", "Motion detected.", "Investigating.")
+ say_got_target = list("Threat detected.", "New task: Remove threat.", "Threat removal engaged.", "Engaging target.")
+ say_threaten = list("Motion detected, judging target...")
+ say_stand_down = list("Visual lost.", "Error: Target not found.")
+ say_escalate = list("Viable target found. Removing.", "Engaging target.", "Target judgement complete. Removal required.")
+
+ threaten_sound = 'sound/effects/turret/move1.wav'
+ stand_down_sound = 'sound/effects/turret/move2.wav'
+
+/datum/say_list/mercenary
+ threaten_sound = 'sound/weapons/TargetOn.ogg'
+ stand_down_sound = 'sound/weapons/TargetOff.ogg'
+
+
+/datum/say_list/crab
+ emote_hear = list("clicks")
+ emote_see = list("clacks")
+
+/datum/say_list/spider
+ emote_hear = list("chitters")
\ No newline at end of file
diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm
index d6c2bc0405..6812323a4e 100644
--- a/code/modules/mob/living/death.dm
+++ b/code/modules/mob/living/death.dm
@@ -1,3 +1,7 @@
/mob/living/death()
clear_fullscreens()
+
+ if(ai_holder)
+ ai_holder.go_sleep()
+
. = ..()
\ No newline at end of file
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 3d9140dd46..e0e85edbcd 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -92,6 +92,13 @@
/mob/living/proc/getsoak(var/def_zone, var/type)
return 0
+// Clicking with an empty hand
+/mob/living/attack_hand(mob/living/L)
+ ..()
+ if(istype(L) && L.a_intent != I_HELP)
+ if(ai_holder) // Using disarm, grab, or harm intent is considered a hostile action to the mob's AI.
+ ai_holder.react_to_attack(L)
+
/mob/living/bullet_act(var/obj/item/projectile/P, var/def_zone)
//Being hit while using a deadman switch
@@ -102,6 +109,9 @@
src.visible_message("[src] triggers their deadman's switch!")
signaler.signal()
+ if(ai_holder && P.firer)
+ ai_holder.react_to_attack(P.firer)
+
//Armor
var/soaked = get_armor_soak(def_zone, P.check_armour, P.armor_penetration)
var/absorb = run_armor_check(def_zone, P.check_armour, P.armor_penetration)
@@ -272,6 +282,8 @@
var/client/assailant = M.client
if(assailant)
add_attack_logs(M,src,"Hit by thrown [O.name]")
+ if(ai_holder)
+ ai_holder.react_to_attack(O.thrower)
// Begin BS12 momentum-transfer code.
var/mass = 1.5
@@ -335,6 +347,8 @@
adjustBruteLoss(damage)
add_attack_logs(user,src,"Generic attack (probably animal)", admin_notify = FALSE) //Usually due to simple_animal attacks
+ if(ai_holder)
+ ai_holder.react_to_attack(user)
src.visible_message("[user] has [attack_message] [src]!")
user.do_attack_animation(src)
spawn(1) updatehealth()
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index e9faaa0769..30c5580d14 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -5,6 +5,8 @@
var/maxHealth = 100 //Maximum health that should be possible. Avoid adjusting this if you can, and instead use modifiers datums.
var/health = 100 //A mob's health
+ var/mob_class = MOB_CLASS_ANIMAL // A mob's "class", e.g. human, mechanical, animal, etc. Used for certain projectile effects. See __defines/mob.dm for available classes.
+
var/hud_updateflag = 0
//Damage related vars, NOTE: THESE SHOULD ONLY BE MODIFIED BY PROCS
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index 275e88be08..96d59dc330 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -8,4 +8,9 @@
update_antag_icons(mind)
client.screen |= global_hud.darksight
client.images |= dsoverlay
+
+ if(ai_holder && !ai_holder.autopilot)
+ ai_holder.go_sleep()
+ to_chat(src,"Mob AI disabled while you are controlling the mob.")
+
return .
diff --git a/code/modules/mob/living/logout.dm b/code/modules/mob/living/logout.dm
index 577221a03e..fa04b0b317 100644
--- a/code/modules/mob/living/logout.dm
+++ b/code/modules/mob/living/logout.dm
@@ -1,6 +1,11 @@
/mob/living/Logout()
..()
- if (mind)
+ if (mind)
//Per BYOND docs key remains set if the player DCs, becomes null if switching bodies.
- if(!key) //key and mind have become seperated.
+ if(!key) //key and mind have become seperated.
mind.active = 0 //This is to stop say, a mind.transfer_to call on a corpse causing a ghost to re-enter its body.
+
+ spawn(15 SECONDS) //15 seconds to get back into the mob before it goes wild
+ if(src && !src.client)
+ if(ai_holder)
+ ai_holder.go_wake()
diff --git a/code/modules/mob/living/simple_animal/animals/mouse.dm b/code/modules/mob/living/simple_animal/animals/mouse.dm
index 123e82a611..42d6ef6423 100644
--- a/code/modules/mob/living/simple_animal/animals/mouse.dm
+++ b/code/modules/mob/living/simple_animal/animals/mouse.dm
@@ -16,10 +16,10 @@
see_in_dark = 6
universal_understand = 1
- mob_size = MOB_MINISCULE
+ mob_size = MOB_SMALL
pass_flags = PASSTABLE
- can_pull_size = ITEMSIZE_TINY
- can_pull_mobs = MOB_PULL_NONE
+// can_pull_size = ITEMSIZE_TINY
+// can_pull_mobs = MOB_PULL_NONE
layer = MOB_LAYER
density = 0
diff --git a/code/modules/mob/living/simple_mob/appearance.dm b/code/modules/mob/living/simple_mob/appearance.dm
new file mode 100644
index 0000000000..2c8bc5efd8
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/appearance.dm
@@ -0,0 +1,69 @@
+/mob/living/simple_mob/update_icon()
+ . = ..()
+ var/mutable_appearance/ma = new(src)
+ ma.layer = layer
+ ma.plane = plane
+
+ ma.overlays = list(modifier_overlay)
+
+ //Awake and normal
+ if((stat == CONSCIOUS) && (!icon_rest || !resting || !incapacitated(INCAPACITATION_DISABLED) ))
+ ma.icon_state = icon_living
+
+ //Dead
+ else if(stat >= DEAD)
+ ma.icon_state = icon_dead
+
+ //Resting or KO'd
+ else if(((stat == UNCONSCIOUS) || resting || incapacitated(INCAPACITATION_DISABLED) ) && icon_rest)
+ ma.icon_state = icon_rest
+
+ //Backup
+ else
+ ma.icon_state = initial(icon_state)
+
+ if(has_hands)
+ if(r_hand_sprite)
+ ma.overlays += r_hand_sprite
+ if(l_hand_sprite)
+ ma.overlays += l_hand_sprite
+
+ if(has_eye_glow)
+ add_eyes()
+
+ appearance = ma
+
+
+// If your simple mob's update_icon() call calls overlays.Cut(), this needs to be called after this, or manually apply modifier_overly to overlays.
+/mob/living/simple_mob/update_modifier_visuals()
+ var/image/effects = null
+ if(modifier_overlay)
+ overlays -= modifier_overlay
+ modifier_overlay.overlays.Cut()
+ effects = modifier_overlay
+ else
+ effects = new()
+
+ for(var/datum/modifier/M in modifiers)
+ if(M.mob_overlay_state)
+ var/image/I = image("icon" = 'icons/mob/modifier_effects.dmi', "icon_state" = M.mob_overlay_state)
+ I.appearance_flags = RESET_COLOR // So colored mobs don't affect the overlay.
+ effects.overlays += I
+
+ modifier_overlay = effects
+ overlays += modifier_overlay
+
+
+/mob/living/simple_mob/proc/add_eyes()
+ if(!eye_layer)
+ eye_layer = image(icon, "[icon_state]-eyes")
+ eye_layer.plane = PLANE_LIGHTING_ABOVE
+
+ overlays += eye_layer
+
+/mob/living/simple_mob/proc/remove_eyes()
+ overlays -= eye_layer
+
+
+/mob/living/simple_mob/gib()
+ ..(icon_gib,1,icon) // we need to specify where the gib animation is stored
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_animal/simple_animal2.dm b/code/modules/mob/living/simple_mob/combat.dm
similarity index 68%
rename from code/modules/mob/living/simple_animal/simple_animal2.dm
rename to code/modules/mob/living/simple_mob/combat.dm
index 04522eb101..d7f784c2be 100644
--- a/code/modules/mob/living/simple_animal/simple_animal2.dm
+++ b/code/modules/mob/living/simple_mob/combat.dm
@@ -1,8 +1,5 @@
-// Reorganized and somewhat cleaned up.
-// AI code has been made into a datum, inside the AI module folder.
-
// Does a melee attack.
-/mob/living/simple_animal/proc/attack_target(atom/A)
+/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.
if(!A.Adjacent(src))
@@ -13,8 +10,10 @@
return do_attack(A)
+
// This does the actual attack.
-/mob/living/simple_animal/proc/do_attack(atom/A)
+// 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.
return FALSE
@@ -27,10 +26,8 @@
if(isliving(A)) // Check defenses.
var/mob/living/L = A
- if(prob(melee_miss_chance)) // This is stupid (The logging part).
- src.attack_log += text("\[[time_stamp()]\] attacked [L.name] ([L.ckey])")
- L.attack_log += text("\[[time_stamp()]\] was attacked by [src.name] ([src.ckey])")
- visible_message("[src] misses [L]!")
+ if(prob(melee_miss_chance))
+ add_attack_logs(src, L, "Animal-attacked (miss)", admin_notify = FALSE)
do_attack_animation(src)
return FALSE // We missed.
@@ -40,12 +37,16 @@
return FALSE // We were blocked.
if(A.attack_generic(src, damage_to_do, pick(attacktext)) && attack_sound)
+ apply_melee_effects(A)
playsound(src, attack_sound, 75, 1)
return TRUE
+// Override for special effects after a successful attack.
+/mob/living/simple_mob/proc/apply_melee_effects(atom/A)
+
//The actual top-level ranged attack proc
-/mob/living/simple_animal/proc/shoot_target(atom/A)
+/mob/living/simple_mob/proc/shoot_target(atom/A)
if(!canClick())
return FALSE
@@ -58,8 +59,9 @@
return TRUE
+
//Shoot a bullet at someone (idk why user is an argument when src would fit???)
-/mob/living/simple_animal/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0)
+/mob/living/simple_mob/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0)
if(A == start)
return
@@ -69,6 +71,7 @@
return
P.launch(A)
+
//Special attacks, like grenades or blinding spit or whatever
-/mob/living/simple_animal/proc/special_attack_target(atom/A)
+/mob/living/simple_mob/proc/special_attack_target(atom/A)
return FALSE
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/defense.dm b/code/modules/mob/living/simple_mob/defense.dm
new file mode 100644
index 0000000000..dac6bfeafc
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/defense.dm
@@ -0,0 +1,192 @@
+// Hit by a projectile.
+/mob/living/simple_mob/bullet_act(var/obj/item/projectile/P)
+ //Projectiles with bonus SA damage
+ if(!P.nodamage)
+ // if(!P.SA_vulnerability || P.SA_vulnerability == intelligence_level)
+ if(P.SA_vulnerability & mob_class)
+ P.damage += P.SA_bonus_damage
+
+ . = ..()
+
+
+// When someone clicks us with an empty hand
+/mob/living/simple_mob/attack_hand(mob/living/L)
+ ..()
+
+ switch(L.a_intent)
+ if(I_HELP)
+ if(health > 0)
+ L.visible_message("\The [L] [response_help] \the [src].")
+
+ if(I_DISARM)
+ L.visible_message("\The [L] [response_disarm] \the [src].")
+ L.do_attack_animation(src)
+ //TODO: Push the mob away or something
+
+ if(I_GRAB)
+ if (L == src)
+ return
+ if (!(status_flags & CANPUSH))
+ return
+ if(!incapacitated(INCAPACITATION_ALL) && prob(grab_resist))
+ L.visible_message("\The [L] tries to grab \the [src] but fails!")
+ return
+
+ var/obj/item/weapon/grab/G = new /obj/item/weapon/grab(L, src)
+
+ L.put_in_active_hand(G)
+
+ G.synch()
+ G.affecting = src
+ LAssailant = L
+
+ L.visible_message("\The [L] has grabbed [src] passively!")
+ L.do_attack_animation(src)
+
+ if(I_HURT)
+ var/armor = run_armor_check(def_zone = null, attack_flag = "melee")
+ apply_damage(damage = harm_intent_damage, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE)
+ L.visible_message("\The [L] [response_harm] \the [src]!")
+ L.do_attack_animation(src)
+
+ return
+
+
+// When somoene clicks us with an item in hand
+/mob/living/simple_mob/attackby(var/obj/item/O, var/mob/user)
+ if(istype(O, /obj/item/stack/medical))
+ if(stat != DEAD)
+ // This could be done better.
+ var/obj/item/stack/medical/MED = O
+ if(health < getMaxHealth())
+ if(MED.amount >= 1)
+ adjustBruteLoss(-MED.heal_brute)
+ MED.amount -= 1
+ if(MED.amount <= 0)
+ qdel(MED)
+ visible_message("\The [user] applies the [MED] on [src].")
+ else
+ var/datum/gender/T = gender_datums[src.get_visible_gender()]
+ to_chat(user, "\The [src] is dead, medical items won't bring [T.him] back to life.") // the gender lookup is somewhat overkill, but it functions identically to the obsolete gender macros and future-proofs this code
+ if(meat_type && (stat == DEAD)) //if the animal has a meat, and if it is dead.
+ if(istype(O, /obj/item/weapon/material/knife) || istype(O, /obj/item/weapon/material/knife/butch))
+ harvest(user)
+
+ return ..()
+
+
+// Handles the actual harming by a melee weapon.
+/mob/living/simple_mob/hit_with_weapon(obj/item/O, mob/living/user, var/effective_force, var/hit_zone)
+ effective_force = O.force
+
+ //Animals can't be stunned(?)
+ if(O.damtype == HALLOSS)
+ effective_force = 0
+ if(supernatural && istype(O,/obj/item/weapon/nullrod))
+ effective_force *= 2
+ purge = 3
+ if(O.force <= resistance)
+ to_chat(user,"This weapon is ineffective, it does no damage.")
+ return 2 //???
+
+// react_to_attack(user)
+
+ . = ..()
+
+
+// Exploding.
+/mob/living/simple_mob/ex_act(severity)
+ if(!blinded)
+ flash_eyes()
+ var/armor = run_armor_check(def_zone = null, attack_flag = "bomb")
+ var/bombdam = 500
+ switch (severity)
+ if (1.0)
+ bombdam = 500
+ if (2.0)
+ bombdam = 60
+ if (3.0)
+ bombdam = 30
+
+ apply_damage(damage = bombdam, damagetype = BRUTE, def_zone = null, blocked = armor, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE)
+
+ if(bombdam > maxHealth)
+ gib()
+
+
+// Fire stuff. Not really exciting at the moment.
+/mob/living/simple_mob/handle_fire()
+ return
+/mob/living/simple_mob/update_fire()
+ return
+/mob/living/simple_mob/IgniteMob()
+ return
+/mob/living/simple_mob/ExtinguishMob()
+ return
+
+
+// Electricity
+/mob/living/simple_mob/electrocute_act(var/shock_damage, var/obj/source, var/siemens_coeff = 1.0, var/def_zone = null)
+ shock_damage *= siemens_coeff
+ if(shock_damage < 1)
+ return 0
+
+ apply_damage(damage = shock_damage, damagetype = BURN, def_zone = null, blocked = null, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE)
+ playsound(loc, "sparks", 50, 1, -1)
+
+ var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread
+ s.set_up(5, 1, loc)
+ s.start()
+
+
+// Shot with taser/stunvolver
+/mob/living/simple_mob/stun_effect_act(var/stun_amount, var/agony_amount, var/def_zone, var/used_weapon=null)
+ if(taser_kill)
+ var/stunDam = 0
+ var/agonyDam = 0
+ var/armor = run_armor_check(def_zone = null, attack_flag = "energy")
+
+ if(stun_amount)
+ stunDam += stun_amount * 0.5
+ apply_damage(damage = stunDam, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = used_weapon, sharp = FALSE, edge = FALSE)
+
+ if(agony_amount)
+ agonyDam += agony_amount * 0.5
+ apply_damage(damage = agonyDam, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = used_weapon, sharp = FALSE, edge = FALSE)
+
+
+// Electromagnetism
+/mob/living/simple_mob/emp_act(severity)
+ ..() // To emp_act() its contents.
+ if(!isSynthetic())
+ return
+ switch(severity)
+ if(1)
+ // adjustFireLoss(rand(15, 25))
+ adjustFireLoss(min(60, getMaxHealth()*0.5)) // Weak mobs will always take two direct EMP hits to kill. Stronger ones might take more.
+ if(2)
+ adjustFireLoss(min(30, getMaxHealth()*0.25))
+ // adjustFireLoss(rand(10, 18))
+ if(3)
+ adjustFireLoss(min(15, getMaxHealth()*0.125))
+ // adjustFireLoss(rand(5, 12))
+ if(4)
+ adjustFireLoss(min(7, getMaxHealth()*0.0625))
+ // adjustFireLoss(rand(1, 6))
+
+
+// Armor
+/mob/living/simple_mob/getarmor(def_zone, attack_flag)
+ var/armorval = armor[attack_flag]
+ if(!armorval)
+ return 0
+ else
+ return armorval
+
+/mob/living/simple_mob/getsoak(def_zone, attack_flag)
+ var/armorval = armor_soak[attack_flag]
+ if(!armorval)
+ return 0
+ else
+ return armorval
+
diff --git a/code/modules/mob/living/simple_mob/hands.dm b/code/modules/mob/living/simple_mob/hands.dm
new file mode 100644
index 0000000000..52da3b75d7
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/hands.dm
@@ -0,0 +1,123 @@
+// Hand procs for player-controlled SA's
+/mob/living/simple_mob/swap_hand()
+ src.hand = !( src.hand )
+ if(hud_used.l_hand_hud_object && hud_used.r_hand_hud_object)
+ if(hand) //This being 1 means the left hand is in use
+ hud_used.l_hand_hud_object.icon_state = "l_hand_active"
+ hud_used.r_hand_hud_object.icon_state = "r_hand_inactive"
+ else
+ hud_used.l_hand_hud_object.icon_state = "l_hand_inactive"
+ hud_used.r_hand_hud_object.icon_state = "r_hand_active"
+ return
+
+/mob/living/simple_mob/put_in_active_hand(var/obj/item/I)
+ if(!has_hands || !istype(I))
+ return
+
+//Puts the item into our active hand if possible. returns 1 on success.
+/mob/living/simple_mob/put_in_active_hand(var/obj/item/W)
+ if(!has_hands)
+ return FALSE
+ return (hand ? put_in_l_hand(W) : put_in_r_hand(W))
+
+/mob/living/simple_mob/put_in_l_hand(var/obj/item/W)
+ if(!..() || l_hand)
+ return 0
+ W.forceMove(src)
+ l_hand = W
+ W.equipped(src,slot_l_hand)
+ W.add_fingerprint(src)
+ update_inv_l_hand()
+ return TRUE
+
+/mob/living/simple_mob/put_in_r_hand(var/obj/item/W)
+ if(!..() || r_hand)
+ return 0
+ W.forceMove(src)
+ r_hand = W
+ W.equipped(src,slot_r_hand)
+ W.add_fingerprint(src)
+ update_inv_r_hand()
+ return TRUE
+
+/mob/living/simple_mob/update_inv_r_hand()
+ if(QDESTROYING(src))
+ return
+
+ if(r_hand)
+ r_hand.screen_loc = ui_rhand //TODO
+
+ //determine icon state to use
+ var/t_state
+ if(r_hand.item_state_slots && r_hand.item_state_slots[slot_r_hand_str])
+ t_state = r_hand.item_state_slots[slot_r_hand_str]
+ else if(r_hand.item_state)
+ t_state = r_hand.item_state
+ else
+ t_state = r_hand.icon_state
+
+ //determine icon to use
+ var/icon/t_icon
+ if(r_hand.item_icons && (slot_r_hand_str in r_hand.item_icons))
+ t_icon = r_hand.item_icons[slot_r_hand_str]
+ else if(r_hand.icon_override)
+ t_state += "_r"
+ t_icon = r_hand.icon_override
+ else
+ t_icon = INV_R_HAND_DEF_ICON
+
+ //apply color
+ var/image/standing = image(icon = t_icon, icon_state = t_state)
+ standing.color = r_hand.color
+
+ r_hand_sprite = standing
+
+ else
+ r_hand_sprite = null
+
+ update_icon()
+
+/mob/living/simple_mob/update_inv_l_hand()
+ if(QDESTROYING(src))
+ return
+
+ if(l_hand)
+ l_hand.screen_loc = ui_lhand //TODO
+
+ //determine icon state to use
+ var/t_state
+ if(l_hand.item_state_slots && l_hand.item_state_slots[slot_l_hand_str])
+ t_state = l_hand.item_state_slots[slot_l_hand_str]
+ else if(l_hand.item_state)
+ t_state = l_hand.item_state
+ else
+ t_state = l_hand.icon_state
+
+ //determine icon to use
+ var/icon/t_icon
+ if(l_hand.item_icons && (slot_l_hand_str in l_hand.item_icons))
+ t_icon = l_hand.item_icons[slot_l_hand_str]
+ else if(l_hand.icon_override)
+ t_state += "_l"
+ t_icon = l_hand.icon_override
+ else
+ t_icon = INV_L_HAND_DEF_ICON
+
+ //apply color
+ var/image/standing = image(icon = t_icon, icon_state = t_state)
+ standing.color = l_hand.color
+
+ l_hand_sprite = standing
+
+ else
+ l_hand_sprite = null
+
+ update_icon()
+
+//Can insert extra huds into the hud holder here.
+/mob/living/simple_mob/proc/extra_huds(var/datum/hud/hud,var/icon/ui_style,var/list/hud_elements)
+ return
+
+//If they can or cannot use tools/machines/etc
+/mob/living/simple_mob/IsAdvancedToolUser()
+ return has_hands
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/life.dm b/code/modules/mob/living/simple_mob/life.dm
new file mode 100644
index 0000000000..2b3fbf50ed
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/life.dm
@@ -0,0 +1,159 @@
+/mob/living/simple_mob/Life()
+ ..()
+
+ //Health
+ updatehealth()
+ if(stat >= DEAD)
+ return FALSE
+
+ handle_stunned()
+ handle_weakened()
+ handle_paralysed()
+ handle_supernatural()
+ handle_atmos()
+
+ handle_special()
+
+ return TRUE
+
+
+//Should we be dead?
+/mob/living/simple_mob/updatehealth()
+ health = getMaxHealth() - getToxLoss() - getFireLoss() - getBruteLoss()
+
+ //Alive, becoming dead
+ if((stat < DEAD) && (health <= 0))
+ death()
+
+ //Overhealth
+ if(health > getMaxHealth())
+ health = getMaxHealth()
+
+ //Update our hud if we have one
+ if(healths)
+ if(stat != DEAD)
+ var/heal_per = (health / getMaxHealth()) * 100
+ switch(heal_per)
+ if(100 to INFINITY)
+ healths.icon_state = "health0"
+ if(80 to 100)
+ healths.icon_state = "health1"
+ if(60 to 80)
+ healths.icon_state = "health2"
+ if(40 to 60)
+ healths.icon_state = "health3"
+ if(20 to 40)
+ healths.icon_state = "health4"
+ if(0 to 20)
+ healths.icon_state = "health5"
+ else
+ healths.icon_state = "health6"
+ else
+ healths.icon_state = "health7"
+
+ //Updates the nutrition while we're here
+ if(nutrition_icon)
+ var/food_per = (nutrition / initial(nutrition)) * 100
+ switch(food_per)
+ if(90 to INFINITY)
+ nutrition_icon.icon_state = "nutrition0"
+ if(75 to 90)
+ nutrition_icon.icon_state = "nutrition1"
+ if(50 to 75)
+ nutrition_icon.icon_state = "nutrition2"
+ if(25 to 50)
+ nutrition_icon.icon_state = "nutrition3"
+ if(0 to 25)
+ nutrition_icon.icon_state = "nutrition4"
+
+// Override for special bullshit.
+/mob/living/simple_mob/proc/handle_special()
+ return
+
+
+// Handle interacting with and taking damage from atmos
+// TODO - Refactor this to use handle_environment() like a good /mob/living
+/mob/living/simple_mob/proc/handle_atmos()
+ var/atmos_unsuitable = 0
+
+ var/atom/A = src.loc
+
+ if(istype(A,/turf))
+ var/turf/T = A
+
+ var/datum/gas_mixture/Environment = T.return_air()
+
+ if(Environment)
+
+ if( abs(Environment.temperature - bodytemperature) > 40 )
+ bodytemperature += ((Environment.temperature - bodytemperature) / 5)
+
+ if(min_oxy)
+ if(Environment.gas["oxygen"] < min_oxy)
+ atmos_unsuitable = 1
+ if(max_oxy)
+ if(Environment.gas["oxygen"] > max_oxy)
+ atmos_unsuitable = 1
+ if(min_tox)
+ if(Environment.gas["phoron"] < min_tox)
+ atmos_unsuitable = 2
+ if(max_tox)
+ if(Environment.gas["phoron"] > max_tox)
+ atmos_unsuitable = 2
+ if(min_n2)
+ if(Environment.gas["nitrogen"] < min_n2)
+ atmos_unsuitable = 1
+ if(max_n2)
+ if(Environment.gas["nitrogen"] > max_n2)
+ atmos_unsuitable = 1
+ if(min_co2)
+ if(Environment.gas["carbon_dioxide"] < min_co2)
+ atmos_unsuitable = 1
+ if(max_co2)
+ if(Environment.gas["carbon_dioxide"] > max_co2)
+ atmos_unsuitable = 1
+
+ //Atmos effect
+ if(bodytemperature < minbodytemp)
+ fire_alert = 2
+ adjustBruteLoss(cold_damage_per_tick)
+ if(fire)
+ fire.icon_state = "fire1"
+ else if(bodytemperature > maxbodytemp)
+ fire_alert = 1
+ adjustBruteLoss(heat_damage_per_tick)
+ if(fire)
+ fire.icon_state = "fire2"
+ else
+ fire_alert = 0
+ if(fire)
+ fire.icon_state = "fire0"
+
+ if(atmos_unsuitable)
+ adjustBruteLoss(unsuitable_atoms_damage)
+ if(oxygen)
+ oxygen.icon_state = "oxy1"
+ else if(oxygen)
+ if(oxygen)
+ oxygen.icon_state = "oxy0"
+
+
+/mob/living/simple_mob/proc/handle_supernatural()
+ if(purge)
+ purge -= 1
+
+/mob/living/simple_mob/death(gibbed, deathmessage = "dies!")
+ density = 0 //We don't block even if we did before
+
+ if(has_eye_glow)
+ remove_eyes()
+
+ if(loot_list.len) //Drop any loot
+ for(var/path in loot_list)
+ if(prob(loot_list[path]))
+ new path(get_turf(src))
+
+ spawn(3) //We'll update our icon in a sec
+ update_icon()
+
+ return ..(gibbed,deathmessage)
\ 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
new file mode 100644
index 0000000000..be162bc230
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/on_click.dm
@@ -0,0 +1,47 @@
+/*
+ Animals
+*/
+/mob/living/simple_mob/UnarmedAttack(var/atom/A, var/proximity)
+ if(!(. = ..()))
+ return
+
+// setClickCooldown(get_attack_speed())
+
+ if(has_hands && istype(A,/obj) && a_intent != I_HURT)
+ var/obj/O = A
+ return O.attack_hand(src)
+
+ switch(a_intent)
+ if(I_HELP)
+ if(isliving(A))
+ custom_emote(1,"[pick(friendly)] [A]!")
+
+ if(I_HURT)
+ if(prob(special_attack_prob))
+ if(special_attack_min_range <= 1)
+ special_attack_target(A)
+
+ else if(melee_damage_upper == 0 && istype(A,/mob/living))
+ custom_emote(1,"[pick(friendly)] [A]!")
+
+ else
+ attack_target(A)
+
+ if(I_GRAB)
+ if(has_hands)
+ A.attack_hand(src)
+
+ if(I_DISARM)
+ if(has_hands)
+ A.attack_hand(src)
+
+/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()
+ return
+
+ if(projectiletype)
+ shoot_target(A)
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/simple_hud.dm b/code/modules/mob/living/simple_mob/simple_hud.dm
new file mode 100644
index 0000000000..fe851648b4
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/simple_hud.dm
@@ -0,0 +1,311 @@
+/mob/living/simple_mob/instantiate_hud(var/datum/hud/hud)
+ if(!client)
+ return //Why bother.
+
+ var/ui_style = 'icons/mob/screen1_animal.dmi'
+ if(ui_icons)
+ ui_style = ui_icons
+
+ var/ui_color = "#ffffff"
+ var/ui_alpha = 255
+
+ var/list/adding = list()
+ var/list/other = list()
+ var/list/hotkeybuttons = list()
+ var/list/slot_info = list()
+
+ hud.adding = adding
+ hud.other = other
+ hud.hotkeybuttons = hotkeybuttons
+
+ var/list/hud_elements = list()
+ var/obj/screen/using
+ var/obj/screen/inventory/inv_box
+
+ var/has_hidden_gear
+ if(LAZYLEN(hud_gears))
+ for(var/gear_slot in hud_gears)
+ inv_box = new /obj/screen/inventory()
+ inv_box.icon = ui_style
+ inv_box.color = ui_color
+ inv_box.alpha = ui_alpha
+
+ var/list/slot_data = hud_gears[gear_slot]
+ inv_box.name = gear_slot
+ inv_box.screen_loc = slot_data["loc"]
+ inv_box.slot_id = slot_data["slot"]
+ inv_box.icon_state = slot_data["state"]
+ slot_info["[inv_box.slot_id]"] = inv_box.screen_loc
+
+ if(slot_data["dir"])
+ inv_box.set_dir(slot_data["dir"])
+
+ if(slot_data["toggle"])
+ other += inv_box
+ has_hidden_gear = 1
+ else
+ adding += inv_box
+
+ if(has_hidden_gear)
+ using = new /obj/screen()
+ using.name = "toggle"
+ using.icon = ui_style
+ using.icon_state = "other"
+ using.screen_loc = ui_inventory
+ using.hud_layerise()
+ using.color = ui_color
+ using.alpha = ui_alpha
+ adding += using
+
+ //Intent Backdrop
+ using = new /obj/screen()
+ using.name = "act_intent"
+ using.icon = ui_style
+ using.icon_state = "intent_"+a_intent
+ using.screen_loc = ui_acti
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.adding += using
+ hud.action_intent = using
+
+ hud_elements |= using
+
+ //Small intent quarters
+ var/icon/ico
+
+ ico = new(ui_style, "black")
+ ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1)
+ ico.DrawBox(rgb(255,255,255,1),1,ico.Height()/2,ico.Width()/2,ico.Height())
+ using = new /obj/screen()
+ using.name = I_HELP
+ using.icon = ico
+ using.screen_loc = ui_acti
+ using.alpha = ui_alpha
+ using.layer = LAYER_HUD_ITEM //These sit on the intent box
+ hud.adding += using
+ hud.help_intent = using
+
+ ico = new(ui_style, "black")
+ ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1)
+ ico.DrawBox(rgb(255,255,255,1),ico.Width()/2,ico.Height()/2,ico.Width(),ico.Height())
+ using = new /obj/screen()
+ using.name = I_DISARM
+ using.icon = ico
+ using.screen_loc = ui_acti
+ using.alpha = ui_alpha
+ using.layer = LAYER_HUD_ITEM
+ hud.adding += using
+ hud.disarm_intent = using
+
+ ico = new(ui_style, "black")
+ ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1)
+ ico.DrawBox(rgb(255,255,255,1),ico.Width()/2,1,ico.Width(),ico.Height()/2)
+ using = new /obj/screen()
+ using.name = I_GRAB
+ using.icon = ico
+ using.screen_loc = ui_acti
+ using.alpha = ui_alpha
+ using.layer = LAYER_HUD_ITEM
+ hud.adding += using
+ hud.grab_intent = using
+
+ ico = new(ui_style, "black")
+ ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1)
+ ico.DrawBox(rgb(255,255,255,1),1,1,ico.Width()/2,ico.Height()/2)
+ using = new /obj/screen()
+ using.name = I_HURT
+ using.icon = ico
+ using.screen_loc = ui_acti
+ using.alpha = ui_alpha
+ using.layer = LAYER_HUD_ITEM
+ hud.adding += using
+ hud.hurt_intent = using
+
+ //Move intent (walk/run)
+ using = new /obj/screen()
+ using.name = "mov_intent"
+ using.icon = ui_style
+ using.icon_state = (m_intent == "run" ? "running" : "walking")
+ using.screen_loc = ui_movi
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.adding += using
+ hud.move_intent = using
+
+ //Resist button
+ using = new /obj/screen()
+ using.name = "resist"
+ using.icon = ui_style
+ using.icon_state = "act_resist"
+ using.screen_loc = ui_pull_resist
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.hotkeybuttons += using
+
+ //Pull button
+ pullin = new /obj/screen()
+ pullin.icon = ui_style
+ pullin.icon_state = "pull0"
+ pullin.name = "pull"
+ pullin.screen_loc = ui_pull_resist
+ hud.hotkeybuttons += pullin
+ hud_elements |= pullin
+
+ //Health status
+ healths = new /obj/screen()
+ healths.icon = ui_style
+ healths.icon_state = "health0"
+ healths.name = "health"
+ healths.screen_loc = ui_health
+ hud_elements |= healths
+
+ //Oxygen dep icon
+ oxygen = new /obj/screen()
+ oxygen.icon = ui_style
+ oxygen.icon_state = "oxy0"
+ oxygen.name = "oxygen"
+ oxygen.screen_loc = ui_oxygen
+ hud_elements |= oxygen
+
+ //Toxins present icon
+ toxin = new /obj/screen()
+ toxin.icon = ui_style
+ toxin.icon_state = "tox0"
+ toxin.name = "toxin"
+ toxin.screen_loc = ui_toxin
+ hud_elements |= toxin
+
+ //Fire warning
+ fire = new /obj/screen()
+ fire.icon = ui_style
+ fire.icon_state = "fire0"
+ fire.name = "fire"
+ fire.screen_loc = ui_fire
+ hud_elements |= fire
+
+ //Pressure warning
+ pressure = new /obj/screen()
+ pressure.icon = ui_style
+ pressure.icon_state = "pressure0"
+ pressure.name = "pressure"
+ pressure.screen_loc = ui_pressure
+ hud_elements |= pressure
+
+ //Body temp warning
+ bodytemp = new /obj/screen()
+ bodytemp.icon = ui_style
+ bodytemp.icon_state = "temp0"
+ bodytemp.name = "body temperature"
+ bodytemp.screen_loc = ui_temp
+ hud_elements |= bodytemp
+
+ //Nutrition status
+ nutrition_icon = new /obj/screen()
+ nutrition_icon.icon = ui_style
+ nutrition_icon.icon_state = "nutrition0"
+ nutrition_icon.name = "nutrition"
+ nutrition_icon.screen_loc = ui_nutrition
+ hud_elements |= nutrition_icon
+
+ pain = new /obj/screen( null )
+
+ zone_sel = new /obj/screen/zone_sel( null )
+ zone_sel.icon = ui_style
+ zone_sel.color = ui_color
+ zone_sel.alpha = ui_alpha
+ zone_sel.overlays.Cut()
+ zone_sel.overlays += image('icons/mob/zone_sel.dmi', "[zone_sel.selecting]")
+ hud_elements |= zone_sel
+
+ //Hand things
+ if(has_hands)
+ //Drop button
+ using = new /obj/screen()
+ using.name = "drop"
+ using.icon = ui_style
+ using.icon_state = "act_drop"
+ using.screen_loc = ui_drop_throw
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.hotkeybuttons += using
+
+ //Equip detail
+ using = new /obj/screen()
+ using.name = "equip"
+ using.icon = ui_style
+ using.icon_state = "act_equip"
+ using.screen_loc = ui_equip
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.adding += using
+
+ //Hand slots themselves
+ inv_box = new /obj/screen/inventory/hand()
+ inv_box.hud = src
+ inv_box.name = "r_hand"
+ inv_box.icon = ui_style
+ inv_box.icon_state = "r_hand_inactive"
+ if(!hand) //This being 0 or null means the right hand is in use
+ inv_box.icon_state = "r_hand_active"
+ inv_box.screen_loc = ui_rhand
+ inv_box.slot_id = slot_r_hand
+ inv_box.color = ui_color
+ inv_box.alpha = ui_alpha
+ hud.r_hand_hud_object = inv_box
+ hud.adding += inv_box
+ slot_info["[slot_r_hand]"] = inv_box.screen_loc
+
+ inv_box = new /obj/screen/inventory/hand()
+ inv_box.hud = src
+ inv_box.name = "l_hand"
+ inv_box.icon = ui_style
+ inv_box.icon_state = "l_hand_inactive"
+ if(hand) //This being 1 means the left hand is in use
+ inv_box.icon_state = "l_hand_active"
+ inv_box.screen_loc = ui_lhand
+ inv_box.slot_id = slot_l_hand
+ inv_box.color = ui_color
+ inv_box.alpha = ui_alpha
+ hud.l_hand_hud_object = inv_box
+ hud.adding += inv_box
+ slot_info["[slot_l_hand]"] = inv_box.screen_loc
+
+ //Swaphand titlebar
+ using = new /obj/screen/inventory()
+ using.name = "hand"
+ using.icon = ui_style
+ using.icon_state = "hand1"
+ using.screen_loc = ui_swaphand1
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.adding += using
+
+ using = new /obj/screen/inventory()
+ using.name = "hand"
+ using.icon = ui_style
+ using.icon_state = "hand2"
+ using.screen_loc = ui_swaphand2
+ using.color = ui_color
+ using.alpha = ui_alpha
+ hud.adding += using
+
+ //Throw button
+ throw_icon = new /obj/screen()
+ throw_icon.icon = ui_style
+ throw_icon.icon_state = "act_throw_off"
+ throw_icon.name = "throw"
+ throw_icon.screen_loc = ui_drop_throw
+ throw_icon.color = ui_color
+ throw_icon.alpha = ui_alpha
+ hud.hotkeybuttons += throw_icon
+ hud_elements |= throw_icon
+
+ extra_huds(hud,ui_style,hud_elements)
+
+ client.screen = list()
+
+ client.screen += hud_elements
+ client.screen += adding + hotkeybuttons
+ client.screen += client.void
+
+ return
diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm
new file mode 100644
index 0000000000..cc38d0f6b4
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/simple_mob.dm
@@ -0,0 +1,252 @@
+// Reorganized and somewhat cleaned up.
+// AI code has been made into a datum, inside the AI module folder.
+
+/mob/living/simple_mob
+ name = "animal"
+ desc = ""
+ icon = 'icons/mob/animal.dmi'
+ health = 20
+ maxHealth = 20
+
+ mob_bump_flag = SIMPLE_ANIMAL
+ mob_swap_flags = MONKEY|SLIME|HUMAN
+ mob_push_flags = MONKEY|SLIME|HUMAN
+
+ var/tt_desc = "Uncataloged Life Form" //Tooltip description
+
+ //Settings for played mobs
+ var/show_stat_health = 1 // Does the percentage health show in the stat panel for the mob
+ var/has_hands = 0 // Set to 1 to enable the use of hands and the hands hud
+ var/list/hud_gears // Slots to show on the hud (typically none)
+ var/ui_icons // Icon file path to use for the HUD, otherwise generic icons are used
+ var/r_hand_sprite // If they have hands,
+ var/l_hand_sprite // they could use some icons.
+ var/player_msg // Message to print to players about 'how' to play this mob on login.
+
+ //Mob icon/appearance settings
+ var/icon_living = "" // The iconstate if we're alive, required
+ var/icon_dead = "" // The iconstate if we're dead, required
+ var/icon_gib = "generic_gib" // The iconstate for being gibbed, optional. Defaults to a generic gib animation.
+ var/icon_rest = null // The iconstate for resting, optional
+ var/image/modifier_overlay = null // Holds overlays from modifiers.
+ var/image/eye_layer = null // Holds the eye overlay.
+ var/has_eye_glow = FALSE // If true, adds an overlay over the lighting plane for [icon_state]-eyes.
+ attack_icon = 'icons/effects/effects.dmi' //Just the default, played like the weapon attack anim
+ attack_icon_state = "slash" //Just the default
+
+ //Mob talking settings
+ universal_speak = 0 // Can all mobs in the entire universe understand this one?
+ var/has_langs = list(LANGUAGE_GALCOM)// Text name of their language if they speak something other than galcom. They speak the first one.
+
+ //Movement things.
+ var/movement_cooldown = 5 // Lower is faster.
+ var/movement_sound = null // If set, will play this sound when it moves on its own will.
+
+ //Mob interaction
+ var/response_help = "tries to help" // If clicked on help intent
+ var/response_disarm = "tries to disarm" // If clicked on disarm intent
+ var/response_harm = "tries to hurt" // If clicked on harm intent
+ var/list/friends = list() // Mobs on this list wont get attacked regardless of faction status.
+ var/harm_intent_damage = 3 // How much an unarmed harm click does to this mob.
+ var/meat_amount = 0 // How much meat to drop from this mob when butchered
+ var/obj/meat_type // The meat object to drop
+ var/list/loot_list = list() // The list of lootable objects to drop, with "/path = prob%" structure
+ var/obj/item/weapon/card/id/myid// An ID card if they have one to give them access to stuff.
+
+ //Mob environment settings
+ var/minbodytemp = 250 // Minimum "okay" temperature in kelvin
+ var/maxbodytemp = 350 // Maximum of above
+ var/heat_damage_per_tick = 3 // Amount of damage applied if animal's body temperature is higher than maxbodytemp
+ var/cold_damage_per_tick = 2 // Same as heat_damage_per_tick, only if the bodytemperature it's lower than minbodytemp
+ var/fire_alert = 0 // 0 = fine, 1 = hot, 2 = cold
+
+ var/min_oxy = 5 // Oxygen in moles, minimum, 0 is 'no minimum'
+ var/max_oxy = 0 // Oxygen in moles, maximum, 0 is 'no maximum'
+ var/min_tox = 0 // Phoron min
+ var/max_tox = 1 // Phoron max
+ var/min_co2 = 0 // CO2 min
+ var/max_co2 = 5 // CO2 max
+ var/min_n2 = 0 // N2 min
+ var/max_n2 = 0 // N2 max
+ var/unsuitable_atoms_damage = 2 // This damage is taken when atmos doesn't fit all the requirements above
+
+ //Hostility settings
+ var/taser_kill = 1 // Is the mob weak to tasers
+
+ //Attack ranged settings
+ var/projectiletype // The projectiles I shoot
+ var/projectilesound // The sound I make when I do it
+ var/casingtype // What to make the hugely laggy casings pile out of
+
+ //Mob melee settings
+ var/melee_damage_lower = 2 // Lower bound of randomized melee damage
+ 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_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?
+
+ //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
+
+ //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.
+ var/resistance = 0 // Damage reduction for all types
+ var/list/armor = list( // Values for normal getarmor() checks
+ "melee" = 0,
+ "bullet" = 0,
+ "laser" = 0,
+ "energy" = 0,
+ "bomb" = 0,
+ "bio" = 100,
+ "rad" = 100
+ )
+ var/list/armor_soak = list( // Values for getsoak() checks.
+ "melee" = 0,
+ "bullet" = 0,
+ "laser" = 0,
+ "energy" = 0,
+ "bomb" = 0,
+ "bio" = 0,
+ "rad" = 0
+ )
+ var/purge = 0 // Cult stuff.
+ var/supernatural = FALSE // Ditto.
+
+
+/mob/living/simple_mob/initialize()
+ verbs -= /mob/verb/observe
+ maxHealth = health
+
+ for(var/L in has_langs)
+ languages |= all_languages[L]
+ if(languages.len)
+ default_language = languages[1]
+
+ if(has_eye_glow)
+ add_eyes()
+ return ..()
+
+
+/mob/living/simple_mob/Destroy()
+ default_language = null
+ if(myid)
+ qdel(myid)
+ myid = null
+
+ friends.Cut()
+ languages.Cut()
+
+ if(has_eye_glow)
+ remove_eyes()
+ return ..()
+
+
+//Client attached
+/mob/living/simple_mob/Login()
+ . = ..()
+ to_chat(src,"You are \the [src]. [player_msg]")
+
+
+/mob/living/simple_mob/emote(var/act, var/type, var/desc)
+ if(act)
+ ..(act, type, desc)
+
+
+/mob/living/simple_mob/SelfMove()
+ . = ..()
+ if(movement_sound)
+ playsound(src, movement_sound, 50, 1)
+
+/mob/living/simple_mob/movement_delay()
+ var/tally = 0 //Incase I need to add stuff other than "speed" later
+
+ tally = movement_cooldown
+
+ if(force_max_speed)
+ return -3
+
+ for(var/datum/modifier/M in modifiers)
+ if(!isnull(M.haste) && M.haste == TRUE)
+ return -3
+ if(!isnull(M.slowdown))
+ tally += M.slowdown
+
+ if(purge)//Purged creatures will move more slowly. The more time before their purge stops, the slower they'll move.
+ if(tally <= 0)
+ tally = 1
+ tally *= purge
+
+ if(m_intent == "walk")
+ tally *= 1.5
+
+ return tally+config.animal_delay
+
+
+/mob/living/simple_mob/Stat()
+ ..()
+ if(statpanel("Status") && show_stat_health)
+ stat(null, "Health: [round((health / getMaxHealth()) * 100)]%")
+
+/mob/living/simple_mob/lay_down()
+ ..()
+ if(resting && icon_rest)
+ icon_state = icon_rest
+ else
+ icon_state = icon_living
+ update_icon()
+
+
+/mob/living/simple_mob/say(var/message,var/datum/language/language)
+ var/verb = "says"
+ if(speak_emote.len)
+ verb = pick(speak_emote)
+
+ message = sanitize(message)
+
+ ..(message, null, verb)
+
+/mob/living/simple_mob/get_speech_ending(verb, var/ending)
+ return verb
+
+
+/mob/living/simple_mob/put_in_hands(var/obj/item/W) // No hands.
+ W.forceMove(get_turf(src))
+ return 1
+
+// Harvest an animal's delicious byproducts
+/mob/living/simple_mob/proc/harvest(var/mob/user)
+ var/actual_meat_amount = max(1,(meat_amount/2))
+ if(meat_type && actual_meat_amount>0 && (stat == DEAD))
+ for(var/i=0;i[user] chops up \the [src]!")
+ new/obj/effect/decal/cleanable/blood/splatter(get_turf(src))
+ qdel(src)
+ else
+ user.visible_message("[user] butchers \the [src] messily!")
+ gib()
+
+
+/mob/living/simple_mob/is_sentient()
+ return mob_class & MOB_CLASS_HUMANOID|MOB_CLASS_ANIMAL // Update this if needed.
+// return intelligence_level != SA_PLANT && intelligence_level != SA_ROBOTIC
+
+//Just some subpaths for easy searching
+/mob/living/simple_mob/hostile
+ faction = "not yours"
+// ai_holder_type = /datum/ai_holder/regular/hostile
+
+/mob/living/simple_mob/retaliate
+// ai_holder_type = /datum/ai_holder/regular/retaliate
+
+/mob/living/simple_mob/get_nametag_desc(mob/user)
+ return "[tt_desc]"
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm
new file mode 100644
index 0000000000..864ee02ace
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm
@@ -0,0 +1,2 @@
+/mob/living/simple_mob/animal
+ mob_class = MOB_CLASS_ANIMAL
\ No newline at end of file
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.dm
new file mode 100644
index 0000000000..efa0dc77bc
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm
@@ -0,0 +1,64 @@
+/*
+ Spiders come in various types, and are a fairly common enemy both inside and outside the station.
+ Their attacks can inject reagents, which can cause harm long after the spider is killed.
+ Thick material will prevent injections, similar to other means of injections.
+*/
+
+// The base spider, in the 'walking tank' family.
+/mob/living/simple_mob/animal/giant_spider
+ name = "giant spider"
+ desc = "Furry and brown, it makes you shudder to look at it. This one has deep red eyes."
+ tt_desc = "Atrax robustus gigantus"
+ icon_state = "guard"
+ icon_living = "guard"
+ icon_dead = "guard_dead"
+ has_eye_glow = TRUE
+
+ faction = "spiders"
+ maxHealth = 200
+ health = 200
+ pass_flags = PASSTABLE
+ movement_cooldown = 10
+
+ see_in_dark = 10
+
+ response_help = "pets"
+ response_disarm = "gently pushes aside"
+ response_harm = "punches"
+
+ melee_damage_lower = 18
+ melee_damage_upper = 30
+ attack_sharp = 1
+ attack_edge = 1
+
+ heat_damage_per_tick = 20
+ cold_damage_per_tick = 20
+
+ speak_emote = list("chitters")
+
+ meat_type = /obj/item/weapon/reagent_containers/food/snacks/xenomeat/spidermeat
+
+ say_list_type = /datum/say_list/spider
+ ai_holder_type = /datum/ai_holder/simple_mob/melee
+
+ var/poison_type = "spidertoxin" // The reagent that gets injected when it attacks.
+ var/poison_chance = 10 // Chance for injection to occur.
+ var/poison_per_bite = 5 // Amount added per injection.
+
+/mob/living/simple_mob/animal/giant_spider/apply_melee_effects(var/atom/A)
+ if(isliving(A))
+ var/mob/living/L = A
+ if(L.reagents)
+ var/target_zone = pick(BP_TORSO,BP_TORSO,BP_TORSO,BP_L_LEG,BP_R_LEG,BP_L_ARM,BP_R_ARM,BP_HEAD)
+ if(L.can_inject(src, null, target_zone))
+ inject_poison(L, target_zone)
+
+// Does actual poison injection, after all checks passed.
+/mob/living/simple_mob/animal/giant_spider/proc/inject_poison(mob/living/L, target_zone)
+ if(prob(poison_chance))
+ 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/nurse.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/nurse.dm
new file mode 100644
index 0000000000..8d7532fbf5
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/nurse.dm
@@ -0,0 +1,188 @@
+// Nurses, they create webs and eggs.
+// They're fragile but their attacks can cause horrifying consequences.
+/mob/living/simple_mob/animal/giant_spider/nurse
+ desc = "Furry and beige, it makes you shudder to look at it. This one has brilliant green eyes."
+ icon_state = "nurse"
+ icon_living = "nurse"
+ icon_dead = "nurse_dead"
+
+ maxHealth = 40
+ health = 40
+
+ movement_cooldown = 5 // A bit faster so that they can inject the eggs easier.
+
+ melee_damage_lower = 5 // Doesn't do a lot of damage, since the goal is to make more spiders with egg attacks.
+ melee_damage_upper = 10
+ poison_per_bite = 5
+ poison_type = "stoxin"
+
+ player_msg = "You can spin webs on an adjacent tile, or cocoon an object by clicking on it.
\
+ You can also cocoon a dying or dead entity by clicking on them, and you will gain charges for egg-laying.
\
+ To lay eggs, click a nearby tile. Laying eggs will deplete a charge."
+ ai_holder_type = /datum/ai_holder/simple_mob/melee/nurse_spider
+
+ var/fed = 0 // Counter for how many egg laying 'charges' the spider has.
+ var/egg_inject_chance = 25 // One in four chance to get eggs.
+ var/egg_type = /obj/effect/spider/eggcluster/small
+ var/web_type = /obj/effect/spider/stickyweb/dark
+
+
+/mob/living/simple_mob/animal/giant_spider/nurse/inject_poison(mob/living/L, target_zone)
+ ..() // Inject the stoxin here.
+ if(ishuman(L) && prob(egg_inject_chance))
+ var/mob/living/carbon/human/H = L
+ var/obj/item/organ/external/O = H.get_organ(target_zone)
+ if(O)
+ var/eggcount = 0
+ for(var/obj/effect/spider/eggcluster/E in O.implants)
+ eggcount++
+ if(!eggcount)
+ var/eggs = new egg_type(O, src)
+ O.implants += eggs
+ to_chat(H, span("critical", "\The [src] injects something into your [O.name]!") ) // Oh god its laying eggs in me!
+
+// Webs target in a web if able to.
+/mob/living/simple_mob/animal/giant_spider/nurse/attack_target(atom/A)
+ if(isturf(A))
+ if(fed)
+ return lay_eggs(A)
+ return web_tile(A)
+
+ if(isliving(A))
+ var/mob/living/L = A
+ if(!L.stat)
+ return ..()
+
+ if(!istype(A, /atom/movable))
+ return
+ var/atom/movable/AM = A
+
+ if(AM.anchored)
+ return ..()
+
+ return spin_cocoon(AM)
+
+/mob/living/simple_mob/animal/giant_spider/nurse/proc/spin_cocoon(atom/movable/AM)
+ if(!istype(AM))
+ return FALSE // We can't cocoon walls sadly.
+ visible_message(span("notice", "\The [src] begins to secrete a sticky substance around \the [AM].") )
+
+ // Get our AI to stay still.
+ set_AI_busy(TRUE)
+
+ if(!do_mob(src, AM, 5 SECONDS))
+ set_AI_busy(FALSE)
+ to_chat(src, span("warning", "You need to stay still to spin a web around \the [AM]."))
+ return FALSE
+
+ set_AI_busy(FALSE)
+
+ if(!AM) // Make sure it didn't get deleted for whatever reason.
+ to_chat(src, span("warning", "Whatever you were spinning a web for, its no longer there..."))
+ return FALSE
+
+ if(!isturf(AM.loc))
+ to_chat(src, span("warning", "You can't spin \the [AM] in a web while it is inside \the [AM.loc]."))
+ return FALSE
+
+ if(!Adjacent(AM))
+ to_chat(src, span("warning", "You need to be next to \the [AM] to spin it into a web."))
+ return FALSE
+
+ // Finally done with the checks.
+ var/obj/effect/spider/cocoon/C = new(AM.loc)
+ var/large_cocoon = FALSE
+ for(var/mob/living/L in C.loc)
+ if(istype(L, /mob/living/simple_mob/animal/giant_spider)) // Cannibalism is bad.
+ continue
+ fed++
+ visible_message(span("warning","\The [src] sticks a proboscis into \the [L], and sucks a viscous substance out."))
+ to_chat(src, span("notice", "You've fed upon \the [L], and can now lay [fed] cluster\s of eggs."))
+ L.forceMove(C)
+ large_cocoon = TRUE
+ break
+
+ // This part's pretty stupid.
+ for(var/obj/O in C.loc)
+ if(!O.anchored)
+ O.forceMove(C)
+
+ // Todo: Put this code on the cocoon object itself?
+ if(large_cocoon)
+ C.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3")
+
+ return TRUE
+
+/mob/living/simple_mob/animal/giant_spider/nurse/handle_special()
+ set waitfor = FALSE
+ if(get_AI_stance() == STANCE_IDLE && !is_AI_busy() && isturf(loc))
+ if(fed)
+ lay_eggs(loc)
+ else
+ web_tile(loc)
+
+/mob/living/simple_mob/animal/giant_spider/nurse/proc/web_tile(turf/T)
+ if(!istype(T))
+ return FALSE
+
+ var/obj/effect/spider/stickyweb/W = locate() in T
+ if(W)
+ return FALSE // Already got webs here.
+
+ visible_message(span("notice", "\The [src] begins to secrete a sticky substance.") )
+ // Get our AI to stay still.
+ set_AI_busy(TRUE)
+
+ if(!do_mob(src, T, 5 SECONDS))
+ set_AI_busy(FALSE)
+ to_chat(src, span("warning", "You need to stay still to spin a web on \the [T]."))
+ return FALSE
+
+ W = locate() in T
+ if(W)
+ return FALSE // Spamclick protection.
+
+ set_AI_busy(FALSE)
+ new web_type(T)
+ return TRUE
+
+
+/mob/living/simple_mob/animal/giant_spider/nurse/proc/lay_eggs(turf/T)
+ if(!istype(T))
+ return FALSE
+
+ if(!fed)
+ return FALSE
+
+ var/obj/effect/spider/eggcluster/E = locate() in T
+ if(E)
+ return FALSE // Already got eggs here.
+
+ visible_message(span("notice", "\The [src] begins to lay a cluster of eggs.") )
+ // Get our AI to stay still.
+ set_AI_busy(TRUE)
+
+ if(!do_mob(src, T, 5 SECONDS))
+ set_AI_busy(FALSE)
+ to_chat(src, span("warning", "You need to stay still to lay eggs on \the [T]."))
+ return FALSE
+
+ E = locate() in T
+ if(E)
+ return FALSE // Spamclick protection.
+
+ set_AI_busy(FALSE)
+ new egg_type(T)
+ fed--
+ return TRUE
+
+
+// Variant that 'blocks' light (by being a negative light source).
+// This is done to make webbed rooms scary and allow for spiders on the other side of webs to see prey.
+/obj/effect/spider/stickyweb/dark
+ name = "dense web"
+ desc = "It's sticky, and blocks a lot of light."
+ light_color = "#FFFFFF"
+ light_range = 2
+ light_power = -3
+
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
new file mode 100644
index 0000000000..fd0f5a8fbc
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm
@@ -0,0 +1,59 @@
+/*
+ Hooligan Crabs are called so because they are rather curious and tend to follow people,
+ 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.
+*/
+
+/mob/living/simple_mob/animal/sif/hooligan_crab
+ name = "hooligan crab"
+ desc = "A large, hard-shelled crustacean. This one is mostly grey."
+ icon_state = "sif_crab"
+ icon_living = "sif_crab"
+ icon_dead = "sif_crab_dead"
+ icon_scale = 1.5
+
+ faction = "crabs"
+
+ maxHealth = 200
+ health = 200
+ movement_cooldown = 10
+ movement_sound = 'sound/weapons/heavysmash.ogg'
+ armor = list(
+ "melee" = 40,
+ "bullet" = 20,
+ "laser" = 10,
+ "energy" = 0,
+ "bomb" = 0,
+ "bio" = 0,
+ "rad" = 0
+ )
+ armor_soak = list(
+ "melee" = 10,
+ "bullet" = 5,
+ "laser" = 0,
+ "energy" = 0,
+ "bomb" = 0,
+ "bio" = 0,
+ "rad" = 0
+ )
+
+ mob_size = MOB_LARGE
+
+ melee_damage_lower = 22
+ melee_damage_upper = 35
+ attack_armor_pen = 35
+ attack_sharp = 1
+ attack_edge = 1
+
+ meat_type = /obj/item/weapon/reagent_containers/food/snacks/meat
+ response_help = "pets"
+ response_disarm = "gently pushes aside"
+ response_harm = "kicks"
+ friendly = "pinches"
+ attacktext = list("clawed", "pinched", "crushed")
+ speak_emote = list("clicks")
+
+ ai_holder_type = /datum/ai_holder/simple_mob/melee/hooligan
+ say_list_type = /datum/say_list/crab
+
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm
new file mode 100644
index 0000000000..b38949c237
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm
@@ -0,0 +1,3 @@
+// Mobs intended to be on Sif. As such, they won't die to the cold.
+/mob/living/simple_mob/animal/sif
+ minbodytemp = 175
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm
new file mode 100644
index 0000000000..bf425954a0
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm
@@ -0,0 +1,71 @@
+/*
+ Combat drones have a rapid ranged attack, and have a projectile shield.
+ They are rather slow, but attempt to 'kite' its target.
+ A solid hit with an EMP grenade will kill the shield instantly.
+*/
+
+/mob/living/simple_mob/mechanical/combat_drone
+ name = "combat drone"
+ desc = "An automated combat drone armed with state of the art weaponry and shielding."
+ icon_state = "drone"
+ icon_living = "drone"
+ icon_dead = "drone_dead"
+ has_eye_glow = TRUE
+
+ faction = "malf_drone"
+
+ maxHealth = 50 // Shield has 150 for total of 200.
+ health = 50
+ movement_cooldown = 5
+ hovering = TRUE
+
+ base_attack_cooldown = 5
+ projectiletype = /obj/item/projectile/beam/drone
+ projectilesound = 'sound/weapons/laser3.ogg'
+
+ response_help = "pokes"
+ response_disarm = "gently pushes aside"
+ response_harm = "hits"
+
+ ai_holder_type = /datum/ai_holder/simple_mob/ranged/kiting/threatening
+ say_list_type = /datum/say_list/malf_drone
+
+ var/datum/effect/effect/system/ion_trail_follow/ion_trail = null
+ var/obj/item/shield_projector/shields = null
+
+/mob/living/simple_mob/mechanical/combat_drone/initialize()
+ ion_trail = new
+ ion_trail.set_up(src)
+ ion_trail.start()
+
+ shields = new /obj/item/shield_projector/rectangle/automatic/drone(src)
+ return ..()
+
+/mob/living/simple_mob/mechanical/combat_drone/Destroy()
+ qdel_null(ion_trail)
+ qdel_null(shields)
+ return ..()
+
+/mob/living/simple_mob/mechanical/combat_drone/death()
+ ..(null,"suddenly breaks apart.")
+ qdel(src)
+
+/mob/living/simple_mob/mechanical/combat_drone/Process_Spacemove(var/check_drift = 0)
+ return TRUE
+
+/obj/item/projectile/beam/drone
+ damage = 10
+
+/obj/item/shield_projector/rectangle/automatic/drone
+ shield_health = 150
+ max_shield_health = 150
+ shield_regen_delay = 10 SECONDS
+ shield_regen_amount = 10
+ size_x = 1
+ size_y = 1
+
+// A slightly easier drone, for POIs.
+// Difference is that it should not be faster than you.
+/mob/living/simple_mob/mechanical/combat_drone/lesser
+ desc = "An automated combat drone with an aged apperance."
+ movement_cooldown = 10
\ 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
new file mode 100644
index 0000000000..7fd21b43c7
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm
@@ -0,0 +1,20 @@
+/mob/living/simple_mob/mechanical
+ mob_class = MOB_CLASS_CONSTRUCT
+
+ 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
+
+ taser_kill = FALSE
+
+/mob/living/simple_mob/mechanical/isSynthetic()
+ return TRUE
+
+/mob/living/simple_mob/mechanical/speech_bubble_appearance()
+ return faction != "neutral" ? "synthetic_evil" : "machine"
\ No newline at end of file
diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm
new file mode 100644
index 0000000000..e718cd880f
--- /dev/null
+++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm
@@ -0,0 +1,36 @@
+/*
+ Viscerators are fragile and don't hit very hard, but fast, evasive, and rarely come alone.
+ They also tend to dodge while in melee range.
+ A weapon that can cleave is very effective against them.
+*/
+
+/mob/living/simple_mob/mechanical/viscerator
+ name = "viscerator"
+ desc = "A small, twin-bladed machine capable of inflicting very deadly lacerations."
+ icon = 'icons/mob/critter.dmi'
+ icon_state = "viscerator_attack"
+ icon_living = "viscerator_attack"
+ hovering = TRUE // Won't trigger landmines.
+
+ faction = "syndicate"
+ maxHealth = 15
+ health = 15
+ movement_cooldown = 0
+
+ pass_flags = PASSTABLE
+ mob_swap_flags = 0
+ mob_push_flags = 0
+
+ melee_damage_lower = 4 // Approx 8 DPS.
+ melee_damage_upper = 4
+ base_attack_cooldown = 5 // Two attacks a second or so.
+ attack_sharp = 1
+ attack_edge = 1
+ attack_sound = 'sound/weapons/bladeslice.ogg'
+ attacktext = list("cut", "sliced")
+
+ ai_holder_type = /datum/ai_holder/simple_mob/melee/evasive
+
+/mob/living/simple_mob/mechanical/viscerator/death()
+ ..(null,"is smashed into pieces!")
+ qdel(src)
\ No newline at end of file
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 2841003152..80fc03bae1 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -13,6 +13,11 @@
/mob/proc/setMoveCooldown(var/timeout)
move_delay = max(world.time + timeout, move_delay)
+/mob/proc/check_move_cooldown()
+ if(world.time < src.move_delay)
+ return FALSE // Need to wait more.
+ return TRUE
+
/client/North()
..()
@@ -198,7 +203,8 @@
if(moving) return 0
- if(world.time < mob.move_delay) return
+ if(!mob.check_move_cooldown())
+ return
if(locate(/obj/effect/stop/, mob.loc))
for(var/obj/effect/stop/S in mob.loc)
diff --git a/code/modules/shieldgen/directional_shield.dm b/code/modules/shieldgen/directional_shield.dm
index 1892a3f3ad..91d3a008b5 100644
--- a/code/modules/shieldgen/directional_shield.dm
+++ b/code/modules/shieldgen/directional_shield.dm
@@ -88,6 +88,8 @@
but allow those projectiles to leave the shield from the inside. Blocking too many damaging projectiles will cause the shield to fail."
icon = 'icons/obj/device.dmi'
icon_state = "signmaker_sec"
+ light_range = 4
+ light_power = 4
var/active = FALSE // If it's on.
var/shield_health = 400 // How much damage the shield blocks before breaking. This is a shared health pool for all shields attached to this projector.
var/max_shield_health = 400 // Ditto. This is fairly high, but shields are really big, you can't miss them, and laser carbines pump out so much hurt.
@@ -170,6 +172,8 @@
var/new_color = rgb(new_r, new_g, new_b)
+ set_light(light_range, light_power, new_color)
+
// Now deploy the new color to all the shields.
for(var/obj/effect/directional_shield/S in active_shields)
S.update_color(new_color)
diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi
index 90cc743bcf..a88deb1b5d 100644
Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ
diff --git a/polaris.dme b/polaris.dme
index efd6fe7878..2064763b89 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1266,6 +1266,7 @@
#include "code\modules\ai\ai_holder_targeting.dm"
#include "code\modules\ai\interfaces.dm"
#include "code\modules\ai\say_list.dm"
+#include "code\modules\ai\aI_holder_subtypes\simple_mob_ai.dm"
#include "code\modules\alarm\alarm.dm"
#include "code\modules\alarm\alarm_handler.dm"
#include "code\modules\alarm\atmosphere_alarm.dm"
@@ -1887,7 +1888,6 @@
#include "code\modules\mob\living\silicon\robot\subtypes\syndicate.dm"
#include "code\modules\mob\living\simple_animal\corpse.dm"
#include "code\modules\mob\living\simple_animal\simple_animal.dm"
-#include "code\modules\mob\living\simple_animal\simple_animal2.dm"
#include "code\modules\mob\living\simple_animal\simple_hud.dm"
#include "code\modules\mob\living\simple_animal\aliens\alien.dm"
#include "code\modules\mob\living\simple_animal\aliens\creature.dm"
@@ -1937,6 +1937,22 @@
#include "code\modules\mob\living\simple_animal\slime\life.dm"
#include "code\modules\mob\living\simple_animal\slime\slime.dm"
#include "code\modules\mob\living\simple_animal\slime\subtypes.dm"
+#include "code\modules\mob\living\simple_mob\appearance.dm"
+#include "code\modules\mob\living\simple_mob\combat.dm"
+#include "code\modules\mob\living\simple_mob\defense.dm"
+#include "code\modules\mob\living\simple_mob\hands.dm"
+#include "code\modules\mob\living\simple_mob\life.dm"
+#include "code\modules\mob\living\simple_mob\on_click.dm"
+#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\nurse.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"
+#include "code\modules\mob\living\simple_mob\subtypes\mechanical\mechanical.dm"
+#include "code\modules\mob\living\simple_mob\subtypes\mechanical\viscerator.dm"
#include "code\modules\mob\living\voice\voice.dm"
#include "code\modules\mob\new_player\login.dm"
#include "code\modules\mob\new_player\logout.dm"
@@ -2448,7 +2464,7 @@
#include "code\ZAS\Zone.dm"
#include "interface\interface.dm"
#include "interface\skin.dmf"
-#include "maps\plane\plane.dm"
+#include "maps\example\example.dm"
#include "maps\submaps\_readme.dm"
#include "maps\submaps\space_submaps\space.dm"
#include "maps\submaps\surface_submaps\mountains\mountains.dm"