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