Demolishing Mobs (#9264)

* Work on the Bluespace Worm, Again.

Space worms propagate their sprites/tooltips/meat types/segment types from the head back.
Space worms have modified organ lists, only having a brain in their head.

Bluespace worms have a unique sprite done by Sypsoti/Schnayy

Bluespace worms have more xeno-organs in addition.

Bluespace worms convert eaten things to Magmellite, instead of Phoron.

* Target through walls.

Distance traveled when not hitting a solid structure during charge is based on the length of the worm.

Bluespace Worms drop xeno hide.

* can_demolish includes trees and barricades

simplemobs can now actually attack trees

* Truncated version of my Bluespace Worm pr.
Simplemob AI can be permitted to demolish walls, mineral turfs, and other common obstructions.

Simplemob AI can be given vision through walls, to make greater use of wall demolishing.

Spaceworm now has an AI controller with x-ray and demolishing capability to tunnel through rock / into buildings, and a wind-up charge that devours impacted mobs or structures.

* Thermic spiders are demolishers, and able to detect prey through walls.

* Inert.

* Decreased chance for thermic spiders to spawn, matching carriers and webslingers.

* Adjust for continued response & thoughts.

Thermic dropped to only Demolishing by default. Still a barricade buster, not a wallhacker.
Phorogenic bumped to be Demolishing. It's big, purple, and unlikely to care there's a twig in the way of its target.

Thermic now has an xray variant for PoI or adminspawn, similar to carriers having the recursion variant.

* Spiders:
Demolishing Phorogen and Thermic moved to subtypes

AI targeting only loops over range once, typecache manually checked for machines instead of making a new list from range again.

* AI targeting dist check instead of range

== FALSE check is a ! prefix, me.

Worm no longer early returns off its overshoot charge call as it does nothing but end the proc regardless of a successful movement

* Pain in two letters.
This commit is contained in:
Mechoid
2024-01-14 14:10:24 -08:00
committed by GitHub
parent eefde7ed17
commit 7dc13d532f
9 changed files with 319 additions and 22 deletions
+23 -1
View File
@@ -7,9 +7,10 @@
var/can_breakthrough = TRUE // If false, the AI will not try to open a path to its goal, like opening doors.
var/violent_breakthrough = TRUE // If false, the AI is not allowed to destroy things like windows or other structures in the way. Requires above var to be true.
var/can_demolish = FALSE // If false, the AI is not allowed to destroy walls in the way. Requires the above var to be true.
var/stand_ground = FALSE // If true, the AI won't try to get closer to an enemy if out of range.
// This does the actual attacking.
/datum/ai_holder/proc/engage_target()
ai_log("engage_target() : Entering.", AI_LOG_DEBUG)
@@ -296,6 +297,27 @@
ai_log("destroy_surroundings() : Attacking closed door.", AI_LOG_INFO)
return melee_attack(D)
if(can_demolish) // Demolish walls, girders, barricades, and trees.
if(istype(problem_turf, /turf/simulated/wall))
ai_log("destroy_surroundings() : Attacking wall.", AI_LOG_INFO)
return melee_attack(problem_turf)
if(istype(problem_turf, /turf/simulated/mineral))
ai_log("destroy_surroundings() : Digging rock.", AI_LOG_INFO)
return melee_attack(problem_turf)
if(istype(obstacle, /obj/structure/girder))
ai_log("destroy_surroundings() : Attacking girder.", AI_LOG_INFO)
return melee_attack(obstacle)
if(istype(obstacle, /obj/structure/barricade))
ai_log("destroy_surroundings() : Attacking barricade.", AI_LOG_INFO)
return melee_attack(obstacle)
if(istype(obstacle, /obj/structure/flora/tree))
ai_log("destroy_surroundings() : Attacking tree.", AI_LOG_INFO)
return melee_attack(obstacle)
ai_log("destroy_surroundings() : Exiting due to nothing to attack.", AI_LOG_INFO)
return ATTACK_FAILED // Nothing to attack.
@@ -30,6 +30,14 @@
/datum/ai_holder/simple_mob/guard/give_chase
home_low_priority = TRUE
// Able to destroy common, but usually-ignored barriers. Walls, Trees, Barricades
/datum/ai_holder/simple_mob/demolishing
can_demolish = TRUE
// Able to select targets with solid objects between them and it to make greater use of Demolishing
/datum/ai_holder/simple_mob/demolishing/xray
ignore_opacity = TRUE
// Doesn't really act until told to by something on the outside.
/datum/ai_holder/simple_mob/inert
hostile = FALSE
+12 -4
View File
@@ -15,6 +15,8 @@
var/respect_alpha = TRUE // If true, mobs with a sufficiently low alpha will be treated as invisible.
var/alpha_vision_threshold = FAKE_INVIS_ALPHA_THRESHOLD // Targets with an alpha less or equal to this will be considered invisible. Requires above var to be true.
var/ignore_opacity = FALSE // If true, can target through opaque objects/turfs. Essentially x-ray vision.
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.
@@ -27,13 +29,19 @@
// Step 1, find out what we can see.
/datum/ai_holder/proc/list_targets()
. = ohearers(vision_range, holder)
. -= dview_mob // Not the dview mob!
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/blob))
for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
if(can_see(holder, HM, vision_range))
. += HM
for(var/atom/movable/AM in range(vision_range, holder))
// Ignoring opacity makes this a slightly hungrier proc as we're now not just checking range for specific machine types, but also living things outside hearing but inside range.
if(ignore_opacity && (isliving(AM) || ((ispath(preferred_target) && istype(AM, preferred_target)) || (istype(AM) && AM == preferred_target))))
. |= AM
if(hostile_machines[AM.type]) // Is the atom in the cache of machines we care about
if(ignore_opacity || can_see(holder, AM, vision_range))
. += AM
// 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)
@@ -213,7 +221,7 @@
ai_log("can_see_target() : Target ([the_target]) was too far from holder. Exiting.", AI_LOG_TRACE)
return FALSE
if(!can_see(holder, the_target, view_range))
if(!(ignore_opacity && (get_dist(holder, the_target) <= view_range)) && !can_see(holder, the_target, view_range))
ai_log("can_see_target() : Target ([the_target]) failed can_see(). Exiting.", AI_LOG_TRACE)
return FALSE
+13
View File
@@ -356,6 +356,19 @@ var/global/list/mining_overlay_cache = list()
excavation_level += Proj.excavation_amount
update_archeo_overlays(Proj.excavation_amount)
/turf/simulated/mineral/attack_generic(var/mob/living/L, var/damage = 0, var/attack_text = "clawed")
. = ..()
if(!damage)
visible_message(SPAN_NOTICE("\The [L] [attack_text] \the [src] ineffectively."))
return FALSE
else
excavation_level = excavation_level + damage
visible_message(SPAN_WARNING("\The [L] [attack_text] \the [src], scattering [pick("gravel", "sand", "dust")]!"))
if(excavation_level >= 200)
GetDrilled(0)
return TRUE
/turf/simulated/mineral/Bumped(AM)
. = ..()
+10
View File
@@ -398,6 +398,16 @@
L.Add(T)
return L
// Same as the above, but ignores density of turfs.
/turf/proc/CardinalTurfsWithAccessNoDensity(var/obj/item/card/id/ID)
var/L[] = new()
for(var/d in cardinal)
var/turf/T = get_step(src, d)
if(istype(T))
if(!LinkBlockedWithAccess(src, T, ID))
L.Add(T)
return L
// Similar to above but not restricted to just cardinal directions.
/turf/proc/TurfsWithAccess(var/obj/item/card/id/ID, flags)
@@ -83,3 +83,10 @@
explosion(src.loc, explosion_dev_range, explosion_heavy_range, explosion_light_range, explosion_flash_range)
return ..()
/mob/living/simple_mob/animal/giant_spider/phorogenic/demolishing
desc = "Crystalline and purple, it makes you shudder to look at it. This one has carapace-covered forelimbs and haunting purple eyes."
ai_holder_type = /datum/ai_holder/simple_mob/demolishing/xray
/mob/living/simple_mob/animal/giant_spider/phorogenic/demolishing/xray
desc = "Crystalline and purple, it makes you shudder to look at it. This one has carapace-covered forelimbs and haunting purple eyes with a malevolent sheen."
ai_holder_type = /datum/ai_holder/simple_mob/demolishing/xray
@@ -36,3 +36,11 @@
poison_chance = 30
poison_per_bite = 1
poison_type = "thermite_v"
/mob/living/simple_mob/animal/giant_spider/thermic/demolishing
desc = "Mirage-cloaked and orange, it makes you shudder to look at it. This one has carapace-coated forelimbs and simmering orange eyes."
ai_holder_type = /datum/ai_holder/simple_mob/demolishing
/mob/living/simple_mob/animal/giant_spider/thermic/demolishing/xray
desc = "Mirage-cloaked and orange, it makes you shudder to look at it. This one has carapace-coated forelegs and simmering orange eyes with a malevolent sheen."
ai_holder_type = /datum/ai_holder/simple_mob/demolishing/xray
@@ -29,7 +29,7 @@
attacktext = list("slammed")
organ_names = /decl/mob_organ_names
organ_names = /decl/mob_organ_names/wormlike
ai_holder_type = /datum/ai_holder/simple_mob/inert
@@ -38,6 +38,18 @@
meat_amount = 2
meat_type = /obj/item/reagent_containers/food/snacks/meat/worm
internal_organs = list(\
/obj/item/organ/internal/heart/grey,\
/obj/item/organ/internal/intestine/xeno,\
/obj/item/organ/internal/lungs/grey,\
/obj/item/organ/internal/xenos/plasmavessel,\
/obj/item/organ/internal/xenos/acidgland\
)
butchery_loot = list(\
/obj/item/stack/animalhide = 3\
)
var/mob/living/simple_mob/animal/space/space_worm/previous //next/previous segments, correspondingly
var/mob/living/simple_mob/animal/space/space_worm/next //head is the nextest segment
@@ -51,6 +63,7 @@
var/flatPlasmaValue = 5 //flat Phoron amount given for non-items
var/atom/currentlyEating // Worm's current Maw target.
var/obj/item/stack/material/digested_sheet = /obj/item/stack/material/phoron // Material sheet type produced from digestion. Default, phoron.
var/z_transitioning = FALSE // Are we currently moving between Z-levels, or doing something that might mean we can't rely on distance checking for segments?
var/sever_chunks = FALSE // Do we fall apart when dying?
@@ -58,6 +71,16 @@
var/time_maw_opened = 0
var/maw_cooldown = 30 SECONDS
var/open_maw = FALSE // Are we trying to eat things?
var/charging = FALSE // Are we charging?
/*
* The head. Holds multiple abilities.
*/
/decl/mob_organ_names/wormlike
hit_zones = list("annuli") //Worms.
/decl/mob_organ_names/wormhead
hit_zones = list("annuli", "cephalon", "mouth") //Worms, again. This time the head or "cephalon" though.
/mob/living/simple_mob/animal/space/space_worm/head
name = "space worm"
@@ -71,6 +94,8 @@
hovering = TRUE
ai_holder_type = /datum/ai_holder/simple_mob/destructive/worm
melee_damage_lower = 10
melee_damage_upper = 25
attack_sharp = TRUE
@@ -78,10 +103,36 @@
attack_armor_pen = 30
attacktext = list("bitten", "gored", "gouged", "chomped", "slammed")
// Charging is a special attack, based on the tunneler spiders' namesake ability.
special_attack_min_range = 2
special_attack_max_range = 6
special_attack_cooldown = 30 SECONDS
var/charge_warning = 0.5 SECONDS // How long the dig telegraphing is.
var/charge_tile_speed = 2 // How long to wait between each tile. Higher numbers result in an easier to charge attack.
organ_names = /decl/mob_organ_names/wormhead
internal_organs = list(\
/obj/item/organ/internal/heart/grey,\
/obj/item/organ/internal/intestine,\
/obj/item/organ/internal/lungs/grey,\
/obj/item/organ/internal/xenos/plasmavessel,\
/obj/item/organ/internal/immunehub,\
/obj/item/organ/internal/brain/grey\
)
animate_movement = SLIDE_STEPS
var/segment_count = 6
/mob/living/simple_mob/animal/space/space_worm/head/apply_bonus_melee_damage(atom/A, damage_amount)
if(istype(A, /turf) || istype(A, /obj/machinery/door))
damage_amount *= 5
else if(istype(A, /obj/mecha))
damage_amount *= 1.5
return damage_amount
/mob/living/simple_mob/animal/space/space_worm/head/severed
segment_count = 0
severed = TRUE
@@ -94,7 +145,7 @@
/mob/living/simple_mob/animal/space/space_worm/head/handle_special()
..()
update_body_faction()
update_body_states()
/mob/living/simple_mob/animal/space/space_worm/head/update_icon()
..()
@@ -121,6 +172,8 @@
current = newSegment
current.faction = faction
update_body_states()
/mob/living/simple_mob/animal/space/space_worm/head/verb/toggle_devour()
set name = "Toggle Feeding"
set desc = "Extends your teeth for 30 seconds so that you can chew through mobs and structures alike."
@@ -136,6 +189,96 @@
else
set_maw(!open_maw)
/mob/living/simple_mob/animal/space/space_worm/head/should_special_attack(atom/A)
if(world.time < time_maw_opened + maw_cooldown)
return FALSE
// Make sure its possible for the worm to reach the target.
var/turf/destination = get_turf(A)
var/turf/starting_turf = get_turf(src)
var/turf/T = starting_turf
for(var/i = 1 to get_dist(starting_turf, destination))
if(T == destination)
break
T = get_step(T, get_dir(T, destination))
return T == destination
/mob/living/simple_mob/animal/space/space_worm/head/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.
set_maw(TRUE)
do_windup_animation(A, charge_warning)
sleep(charge_warning) // For the telegraphing.
// Do the charge!
visible_message(span("danger","\The [src] charges towards \the [A]!"))
if(!handle_charge(destination))
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] slams into \the [L]!"))
playsound(src, 'sound/weapons/heavysmash.ogg', 75, 1)
AttemptToEat(L)
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.
var/dir_to_go = get_dir(starting_turf, destination)
for(var/i = 1 to rand(2, max(4, tally_segments(0))))
destination = get_step(destination, dir_to_go)
handle_charge(destination) // This is overshooting, we don't really care if we hit this destination so don't check the return, just move.
set_AI_busy(FALSE)
return FALSE
// Does the charge movement, stuns enemies, etc.
/mob/living/simple_mob/animal/space/space_worm/head/proc/handle_charge(turf/destination)
var/turf/T = get_turf(src) // Hold our current tile.
// Regular charge 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))
Move(T)
sleep(charge_tile_speed)
set_maw(FALSE)
/*
* Head end.
*/
/mob/living/simple_mob/animal/space/space_worm/proc/set_maw(var/state = FALSE)
open_maw = state
if(open_maw)
@@ -185,7 +328,7 @@
if(istype(mover, /mob/living/simple_mob/animal/space/space_worm)) // Worms don't run over worms. That's weird. And also really annoying.
return TRUE
else if(src.stat == DEAD && !istype(mover, /obj/item/projectile)) // Projectiles need to do their normal checks.
if(src.stat == DEAD && !istype(mover, /obj/item/projectile)) // Projectiles need to do their normal checks.
return TRUE
return ..()
@@ -206,6 +349,12 @@
else
previous.z_transitioning = FALSE
previous.forceMove(old_loc) // None of this 'ripped in half by an airlock' business.
if(open_maw)
for(var/atom/movable/AM in get_turf(src))
if(!AM.anchored && prob(30))
AttemptToEat(AM)
update_icon()
/mob/living/simple_mob/animal/space/space_worm/head/Bump(atom/obstacle)
@@ -214,10 +363,12 @@
if(currentlyEating != obstacle)
currentlyEating = obstacle
set_AI_busy(TRUE)
if(!charging)
set_AI_busy(TRUE)
if(AttemptToEat(obstacle))
currentlyEating = null
set_AI_busy(FALSE)
if(!charging)
set_AI_busy(FALSE)
else
currentlyEating = null
. = ..(obstacle)
@@ -245,7 +396,7 @@
if((!W.reinf_material && do_after(src, 5 SECONDS)) || do_after(src, 10 SECONDS)) // 10 seconds for an R-wall, 5 seconds for a normal one.
if(target)
W.dismantle_wall()
return 1
return TRUE
else if(istype(target,/atom/movable))
if(istype(target,/mob) || do_after(src, 5)) // 5 ticks to eat stuff like tables.
var/atom/movable/objectOrMob = target
@@ -268,7 +419,8 @@
if(D && (D.stat & BROKEN|NOPOWER))
D.open(TRUE)
break
D.update_nearby_tiles(need_rebuild=1)
return TRUE
if(istype(objectOrMob, /obj/effect/energy_field))
var/obj/effect/energy_field/EF = objectOrMob
@@ -283,12 +435,15 @@
else
EF.visible_message("<span class='danger'>\The [EF] reverberates as it returns to normal.</span>")
if(istype(objectOrMob, /obj/machinery/shieldwall)) // This shield type is explicitly a containment field, and if you somehow get a worm inside this, congrats.
return FALSE
if(objectOrMob)
objectOrMob.update_nearby_tiles(need_rebuild=1)
objectOrMob.forceMove(src)
return 1
return TRUE
return 0
return FALSE
/mob/living/simple_mob/animal/space/space_worm/proc/Attach(var/mob/living/simple_mob/animal/space/space_worm/attachement)
if(!attachement)
@@ -320,18 +475,20 @@
if(stomach_special_digest(stomachContent))
continue
if(istype(stomachContent,/obj/item/stack)) //converts to plasma, keeping the stack value
if(!istype(stomachContent,/obj/item/stack/material/phoron))
if(!istype(stomachContent,digested_sheet))
var/obj/item/stack/oldStack = stomachContent
new /obj/item/stack/material/phoron(src, oldStack.get_amount())
new digested_sheet(src, oldStack.get_amount())
qdel(oldStack)
continue
else if(istype(stomachContent,/obj/item)) //converts to plasma, keeping the w_class
var/obj/item/oldItem = stomachContent
new /obj/item/stack/material/phoron(src, oldItem.w_class)
if(oldItem.unacidable)
continue
new digested_sheet(src, oldItem.w_class)
qdel(oldItem)
continue
else
new /obj/item/stack/material/phoron(src, flatPlasmaValue) //just flat amount
new digested_sheet(src, flatPlasmaValue) //just flat amount
if(!isliving(stomachContent))
qdel(stomachContent)
else
@@ -359,19 +516,67 @@
return
/mob/living/simple_mob/animal/space/space_worm/proc/stomach_special(var/atom/A) // Futureproof. Anything that interacts with contents without relying on digestion probability. Return TRUE if it should skip digest.
if(istype(A, /mob/living/simple_mob))
var/mob/living/simple_mob/SM = A
if(!SM.ckey) // Players can always fight back.
SM.Stun(3)
return FALSE
/mob/living/simple_mob/animal/space/space_worm/proc/stomach_special_digest(var/atom/A) // Futureproof. Any special checks that interact with digested atoms. I.E., ore processing. Return TRUE if it should skip future digest checks.
if(istype(A, /obj/mecha))
var/obj/mecha/ME = A
ME.take_damage(rand(10,60), "bio")
return TRUE
return FALSE
/mob/living/simple_mob/animal/space/space_worm/proc/update_body_faction()
/mob/living/simple_mob/animal/space/space_worm/proc/update_body_states()
if(next) // Keep us on the same page, here.
faction = next.faction
if(mob_class != next.mob_class)
mob_class = next.mob_class
if(plane != next.plane)
plane = next.plane
if(invisibility != next.invisibility)
invisibility = next.invisibility
if(meat_type != next.meat_type)
meat_type = next.meat_type
if(severed_head_type != next.severed_head_type)
severed_head_type = next.severed_head_type
if(segment_type != next.segment_type)
segment_type = next.segment_type
if(digested_sheet != next.digested_sheet)
digested_sheet = next.digested_sheet
if(tt_desc != next.tt_desc)
tt_desc = next.tt_desc
if(faction != next.faction)
faction = next.faction
if(icon != next.icon)
icon = next.icon
// Handle damage distribution, if we're alive. Dead segments are dead weight.
// If the next headward segment has more damage, take some of the damage to ourself.
if(stat != DEAD)
if(next.bruteloss > bruteloss)
var/change = round((next.bruteloss - bruteloss) / 2)
next.adjustBruteLoss(-change)
adjustBruteLoss(change)
if(next.fireloss > fireloss)
var/change = round((next.fireloss - fireloss) / 2)
next.adjustFireLoss(-change)
adjustFireLoss(change)
if(previous)
previous.update_body_faction()
previous.update_body_states()
return 1
return 0
// Returns the number of segments.
/mob/living/simple_mob/animal/space/space_worm/proc/tally_segments(var/count = 0)
count++
if(previous)
return previous.tally_segments(count)
return count
// Worm meat.
/obj/item/reagent_containers/food/snacks/meat/worm
@@ -390,7 +595,7 @@
src.bitesize = 3
/obj/item/reagent_containers/food/snacks/meat/worm/attackby(obj/item/W as obj, mob/user as mob)
if(istype(W,/obj/item/material/knife))
if(is_sharp(W))
var/to_spawn = pickweight(/obj/random/junk = 30,
/obj/random/trash = 30,
/obj/random/maintenance/clean = 15,
@@ -414,3 +619,15 @@
to_chat(user, "<span class='alien'>You cut the tissue holding the chunks together.</span>")
..()
/*
* AI
*/
/datum/ai_holder/simple_mob/destructive/worm
can_demolish = TRUE
ignore_opacity = TRUE
/datum/ai_holder/simple_mob/destructive/worm/intelligent
astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccessNoDensity
use_astar = TRUE