Basic Heretic Mobs: The Rest of Them (#78757)

## About The Pull Request

Raw Prophet and Armsy had fun stuff going on and merited their own PRs,
but the rest of these guys are basically just statblocks with abilities
so I converted them all at once.

Rust Walkers are present in a ruin and so have new AI which will
actually use their abilities. They rust the area around where they spawn
and throw their rust blast at people.

I also gave Flesh Stalkers AI even though nobody has put them in a map
because I thought it would be cool. This adds an AI behaviour where if
they're not doing anything else they will turn into an animal and chill
until someone's been around them for a bit, before attacking. They will
also use EMP almost immediately upon performing their ambush, which
kills the lights in that room. Spooky!
To support this I needed to make some changes to let AI continue
processing and targetting correctly while shapeshifted.

I didn't give Maids or Ash Spirits AI because they'd be really boring.

Other changes:
I made the maid in the mirror flicker when it takes examine damage
because the `visible_message` says it does but despite having the power,
nobody made it actually flicker...

## Why It's Good For The Game

No more simple mob heretic summons.

## Changelog

🆑
refactor: Rust Walkers, Ash Spirits, Flesh Stalkers, and The Maid in the
Mirror now use the basic mob framework. Please report any unusual
behaviour.
/🆑

---------

Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
This commit is contained in:
Jacquerel
2023-10-06 16:06:22 -06:00
committed by GitHub
co-authored by MrMelbert
parent c3446fa542
commit 5e2c8459dd
26 changed files with 266 additions and 181 deletions
@@ -0,0 +1,25 @@
/**
* Player-only mob which is fast, can jaunt a short distance, and is dangerous at close range
*/
/mob/living/basic/heretic_summon/ash_spirit
name = "Ash Spirit"
real_name = "Ashy"
desc = "A manifestation of ash, trailing a perpetual cloud of short-lived cinders."
icon_state = "ash_walker"
icon_living = "ash_walker"
maxHealth = 75
health = 75
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_TURFS
/mob/living/basic/heretic_summon/ash_spirit/Initialize(mapload)
. = ..()
var/static/list/actions_to_add = list(
/datum/action/cooldown/spell/fire_sworn,
/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
/datum/action/cooldown/spell/pointed/cleave,
)
for (var/action in actions_to_add)
var/datum/action/cooldown/new_action = new action(src)
new_action.Grant(src)
@@ -0,0 +1,46 @@
/// Durable ambush mob with an EMP ability
/mob/living/basic/heretic_summon/stalker
name = "Flesh Stalker"
real_name = "Flesh Stalker"
desc = "An abomination cobbled together from varied remains. Its appearance changes slightly every time you blink."
icon_state = "stalker"
icon_living = "stalker"
maxHealth = 150
health = 150
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_MOBS
ai_controller = /datum/ai_controller/basic_controller/stalker
/// Associative list of action types we would like to have, and what blackboard key (if any) to put it in
var/static/list/actions_to_add = list(
/datum/action/cooldown/spell/emp/eldritch = BB_GENERIC_ACTION,
/datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash = null,
/datum/action/cooldown/spell/shapeshift/eldritch = BB_SHAPESHIFT_ACTION,
)
/mob/living/basic/heretic_summon/stalker/Initialize(mapload)
. = ..()
AddComponent(/datum/component/ai_target_timer)
for (var/action_type in actions_to_add)
var/datum/action/new_action = new action_type(src)
new_action.Grant(src)
var/blackboard_key = actions_to_add[action_type]
if (!isnull(blackboard_key))
ai_controller?.set_blackboard_key(blackboard_key, new_action)
/// Changes shape and lies in wait when it has no target, uses EMP and attacks once it does
/datum/ai_controller/basic_controller/stalker
ai_traits = CAN_ACT_IN_STASIS
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/shapechange_ambush,
/datum/ai_planning_subtree/use_mob_ability,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
@@ -0,0 +1,81 @@
/// Scout and assassin who can appear and disappear from glass surfaces. Damaged by being examined.
/mob/living/basic/heretic_summon/maid_in_the_mirror
name = "Maid in the Mirror"
real_name = "Maid in the Mirror"
desc = "A floating and flowing wisp of chilled air. Glancing at it causes it to shimmer slightly."
icon = 'icons/mob/simple/mob.dmi'
icon_state = "stand"
icon_living = "stand" // Placeholder sprite... still
speak_emote = list("whispers")
movement_type = FLOATING
status_flags = CANSTUN | CANPUSH
attack_sound = SFX_SHATTER
maxHealth = 80
health = 80
melee_damage_lower = 12
melee_damage_upper = 16
sight = SEE_MOBS | SEE_OBJS | SEE_TURFS
death_message = "shatters and vanishes, releasing a gust of cold air."
/// Whether we take damage when someone looks at us
var/harmed_by_examine = TRUE
/// How often being examined by a specific mob can hurt us
var/recent_examine_damage_cooldown = 10 SECONDS
/// A list of REFs to people who recently examined us
var/list/recent_examiner_refs = list()
/mob/living/basic/heretic_summon/Initialize(mapload)
. = ..()
var/static/list/loot = list(
/obj/effect/decal/cleanable/ash,
/obj/item/clothing/suit/armor/vest,
/obj/item/organ/internal/lungs,
/obj/item/shard,
)
AddElement(/datum/element/death_drops, loot)
var/datum/action/cooldown/spell/jaunt/mirror_walk/jaunt = new (src)
jaunt.Grant(src)
/mob/living/basic/heretic_summon/maid_in_the_mirror/death(gibbed)
var/turf/death_turf = get_turf(src)
death_turf.TakeTemperature(-40) // Spooky
return ..()
// Examining them will harm them, on a cooldown.
/mob/living/basic/heretic_summon/maid_in_the_mirror/examine(mob/user)
. = ..()
if(!harmed_by_examine || user == src || user.stat == DEAD || !isliving(user) || IS_HERETIC_OR_MONSTER(user))
return
var/user_ref = REF(user)
if(user_ref in recent_examiner_refs)
return
// If we have health, we take some damage
if(health > (maxHealth * 0.125))
visible_message(
span_warning("[src] seems to fade in and out slightly."),
span_userdanger("[user]'s gaze pierces your every being!"),
)
recent_examiner_refs += user_ref
apply_damage(maxHealth * 0.1) // We take 10% of our health as damage upon being examined
playsound(src, 'sound/effects/ghost2.ogg', 40, TRUE)
addtimer(CALLBACK(src, PROC_REF(clear_recent_examiner), user_ref), recent_examine_damage_cooldown, TIMER_DELETE_ME)
animate(src, alpha = 120, time = 0.5 SECONDS, easing = ELASTIC_EASING, loop = 2, flags = ANIMATION_PARALLEL)
animate(alpha = 255, time = 0.5 SECONDS, easing = ELASTIC_EASING)
// If we're examined on low enough health we die straight up
else
visible_message(
span_danger("[src] vanishes from existence!"),
span_userdanger("[user]'s gaze shatters your form, destroying you!"),
)
death()
/mob/living/basic/heretic_summon/maid_in_the_mirror/proc/clear_recent_examiner(mob_ref)
if(!(mob_ref in recent_examiner_refs))
return
recent_examiner_refs -= mob_ref
heal_overall_damage(5)
@@ -0,0 +1,87 @@
/// Pretty simple mob which creates areas of rust and has a rust-creating projectile spell
/mob/living/basic/heretic_summon/rust_walker
name = "Rust Walker"
real_name = "Rusty"
desc = "A grinding, clanking construct which leaches life from its surroundings with every armoured step."
icon_state = "rust_walker_s"
base_icon_state = "rust_walker"
icon_living = "rust_walker_s"
maxHealth = 75
health = 75
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_TURFS
speed = 1
ai_controller = /datum/ai_controller/basic_controller/rust_walker
/mob/living/basic/heretic_summon/rust_walker/Initialize(mapload)
. = ..()
AddElement(/datum/element/footstep, FOOTSTEP_MOB_RUST)
var/datum/action/cooldown/spell/aoe/rust_conversion/small/conversion = new(src)
conversion.Grant(src)
ai_controller?.set_blackboard_key(BB_GENERIC_ACTION, conversion)
var/datum/action/cooldown/spell/basic_projectile/rust_wave/short/wave = new(src)
wave.Grant(src)
ai_controller?.set_blackboard_key(BB_TARGETTED_ACTION, wave)
/mob/living/basic/heretic_summon/rust_walker/setDir(newdir)
. = ..()
update_appearance(UPDATE_ICON_STATE)
/mob/living/basic/heretic_summon/rust_walker/update_icon_state()
. = ..()
if(stat == DEAD) // We usually delete on death but just in case
return
if(dir & NORTH)
icon_state = "[base_icon_state]_n"
else if(dir & SOUTH)
icon_state = "[base_icon_state]_s"
icon_living = icon_state
/mob/living/basic/heretic_summon/rust_walker/Life(seconds_per_tick = SSMOBS_DT, times_fired)
if(stat == DEAD)
return ..()
var/turf/our_turf = get_turf(src)
if(HAS_TRAIT(our_turf, TRAIT_RUSTY))
adjustBruteLoss(-3 * seconds_per_tick)
return ..()
/// Converts unconverted terrain, sprays pocket sand around
/datum/ai_controller/basic_controller/rust_walker
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk/rust
planning_subtrees = list(
/datum/ai_planning_subtree/use_mob_ability/rust_walker,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/// Moves a lot if healthy and on rust (to find more tiles to rust) or unhealthy and not on rust (to find healing rust)
/// Still moving in random directions though we're not really seeking it out
/datum/idle_behavior/idle_random_walk/rust
/datum/idle_behavior/idle_random_walk/rust/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/our_mob = controller.pawn
var/turf/our_turf = get_turf(our_mob)
if (HAS_TRAIT(our_turf, TRAIT_RUSTY))
walk_chance = (our_mob.health < our_mob.maxHealth) ? 10 : 50
else
walk_chance = (our_mob.health < our_mob.maxHealth) ? 50 : 10
return ..()
/// Use if we're not stood on rust right now
/datum/ai_planning_subtree/use_mob_ability/rust_walker
/datum/ai_planning_subtree/use_mob_ability/rust_walker/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/turf/our_turf = get_turf(controller.pawn)
if (HAS_TRAIT(our_turf, TRAIT_RUSTY))
return
return ..()