mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-29 08:08:49 +01:00
[MIRROR] Basic Watchers & Basilisks [MDB IGNORE] (#23137)
* Basic Watchers & Basilisks (#77630) ## About The Pull Request This one is a double feature because Watchers and Basilisks share the same typepath. You might see a couple more of those. As is tradition I decided to fuck with them rather than just port them. Here's what's up. **Basilisks**   - Have a new soulless sprite which looks less like a living blue hedge. - Walk at you and shoot you while you are not in range (just like before). - Become supercharged if they become "heated" by lava, lasers, or temperature weapons. This was a feature they also previously had but they would never encounter lava, so now it also works if you use the wrong gun on them. - Lose their supercharge if you cool them down. - Otherwise pretty normal mobs. **Watchers** https://www.youtube.com/watch?v=kOq_Bf78k5A Here's a traditional video of me intentionally getting hit by mechanics (trust me its definitely on purpose) - They glow emmissively a little bit so you can see them from further away. - Their eyes light up about 0.5 seconds before they are able to shoot at you. - No longer melee attack, instead try to stay out of melee. - Will occasionally put you into "Overwatch", meaning they will shoot you rapidly if you move or act while they're staring at you for a brief time period (after which you become immune for 12 seconds, and during which other watchers will play fair and stop shooting at you). - If they start taking damage they will also start using their "Gaze" attack, look away or suffer some kind of negative effect! - - Normal watcher gaze flashes and confuses you. - - Magmawing watcher gaze obviously burns (and briefly stuns) you. - - Icewing watcher gaze freezes you and throws you backwards. - Magnetically attract and eat diamonds. They also used to do this, but just if they happened to coincidentally walk past some. **Other accompanying changes** All basic mobs will now adopt the "stop gliding" trait if they get slowed down too much. I moved behaviour for "fire a projectile from this atom" into a helper proc because I was using it in three places and I will probably use it in more places. There are probably other places in the existing code which could be using this. I think I made the basic mob melee attack forecast default a little more forgiving, they were fucking me up too much and I am the playtester. ## Why It's Good For The Game Another one off the list. New tricks for old dogs. Framework for making mobs with ranged attacks "fairer" (you can see when they are ready to shoot you). More (hopefully) versatile AI behaviours which we will reuse later (I hope I'm not duplicating one someone already made). If our players "enjoy" them enough we can give more mobs "don't look at me" mechanics. Removes some soul sprites. ## Changelog 🆑 refactor: Basilisks and Watchers now use the basic mob framework. Please bug report any unusual behaviour. sprite: Basilisks have new sprites. add: Basilisks will go into a frenzy if heated by energy weapons or temperature beams as well as by lava. add: Watcher eyes will be illuminated briefly when they are ready to fire at you. add: Watchers can now briefly put you into "Overwatch" and penalise you for moving while they can see you. add: Wounded watchers will occasionally punish players who look at them. balance: Unusual watcher variants are more likely to appear. /🆑 * Basic Watchers & Basilisks * Modular paths --------- Co-authored-by: Jacquerel <hnevard@gmail.com> Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com>
This commit is contained in:
co-authored by
Jacquerel
Giz
parent
22b0e5c17b
commit
57272c8d22
@@ -205,6 +205,13 @@
|
||||
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/simplemob_varspeed, multiplicative_slowdown = speed)
|
||||
SEND_SIGNAL(src, POST_BASIC_MOB_UPDATE_VARSPEED)
|
||||
|
||||
/mob/living/basic/update_movespeed()
|
||||
. = ..()
|
||||
if (cached_multiplicative_slowdown > END_GLIDE_SPEED)
|
||||
ADD_TRAIT(src, TRAIT_NO_GLIDE, SPEED_TRAIT)
|
||||
else
|
||||
REMOVE_TRAIT(src, TRAIT_NO_GLIDE, SPEED_TRAIT)
|
||||
|
||||
/mob/living/basic/relaymove(mob/living/user, direction)
|
||||
if(user.incapacitated())
|
||||
return
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/// Watchers' ground-dwelling cousins, they shoot at you until they get into melee and absorb laser fire to power up.
|
||||
/mob/living/basic/mining/basilisk
|
||||
name = "basilisk"
|
||||
desc = "A territorial beast, covered in a diamond shell which absorbs heat. Its stare causes victims to freeze from the inside."
|
||||
icon_state = "basilisk"
|
||||
icon_living = "basilisk"
|
||||
icon_dead = "basilisk_dead"
|
||||
speak_emote = list("chimes")
|
||||
damage_coeff = list(BRUTE = 1, BURN = 0.1, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
|
||||
speed = 20
|
||||
maxHealth = 200
|
||||
health = 200
|
||||
obj_damage = 60
|
||||
melee_damage_lower = 12
|
||||
melee_damage_upper = 12
|
||||
attack_verb_continuous = "bites into"
|
||||
attack_verb_simple = "bite into"
|
||||
throw_blocked_message = "bounces off the shell of"
|
||||
attack_sound = 'sound/weapons/bladeslice.ogg'
|
||||
attack_vis_effect = ATTACK_EFFECT_BITE
|
||||
ai_controller = /datum/ai_controller/basic_controller/basilisk
|
||||
butcher_results = list(
|
||||
/obj/item/stack/sheet/bone = 1,
|
||||
/obj/item/stack/ore/diamond = 2,
|
||||
/obj/item/stack/sheet/sinew = 2,
|
||||
)
|
||||
/// The component we use for making ranged attacks
|
||||
var/datum/component/ranged_attacks/ranged_attacks
|
||||
|
||||
/mob/living/basic/mining/basilisk/Initialize(mapload)
|
||||
. = ..()
|
||||
AddComponent(/datum/component/basic_mob_attack_telegraph)
|
||||
ranged_attacks = AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/temp/watcher, projectile_sound = 'sound/weapons/pierce.ogg')
|
||||
RegisterSignal(src, COMSIG_MOVABLE_MOVED, PROC_REF(check_lava))
|
||||
|
||||
/mob/living/basic/mining/basilisk/Destroy()
|
||||
QDEL_NULL(ranged_attacks)
|
||||
return ..()
|
||||
|
||||
/mob/living/basic/mining/basilisk/welder_act(mob/living/user, obj/item/tool)
|
||||
. = ..()
|
||||
heat_up() // Who would do this?
|
||||
|
||||
/mob/living/basic/mining/basilisk/bullet_act(obj/projectile/bullet, def_zone, piercing_hit)
|
||||
. = ..()
|
||||
if (istype(bullet, /obj/projectile/temp))
|
||||
var/obj/projectile/temp/heat_bullet = bullet
|
||||
if (heat_bullet.temperature < 0)
|
||||
return
|
||||
heat_up()
|
||||
return
|
||||
|
||||
if (bullet.damage == 0 || bullet.damage_type != BURN)
|
||||
return
|
||||
heat_up()
|
||||
|
||||
/// Are we standing in lava?
|
||||
/mob/living/basic/mining/basilisk/proc/check_lava()
|
||||
SIGNAL_HANDLER
|
||||
var/turf/open/lava/entered_lava = loc
|
||||
if (!islava(entered_lava) || entered_lava.immunity_trait != TRAIT_LAVA_IMMUNE)
|
||||
return
|
||||
heat_up()
|
||||
|
||||
/// We got hit by something hot, go into heat mode
|
||||
/mob/living/basic/mining/basilisk/proc/heat_up()
|
||||
if (stat != CONSCIOUS || has_status_effect(/datum/status_effect/basilisk_overheat))
|
||||
return
|
||||
apply_status_effect(/datum/status_effect/basilisk_overheat)
|
||||
|
||||
/// Change what kind of beam we fire
|
||||
/mob/living/basic/mining/basilisk/proc/set_projectile_type(projectile_type)
|
||||
ranged_attacks.projectile_type = projectile_type
|
||||
|
||||
/datum/ai_controller/basic_controller/basilisk
|
||||
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/ranged_skirmish,
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
/// Status effect gained by basilisks when they touch something hot
|
||||
/datum/status_effect/basilisk_overheat
|
||||
id = "basilisk_overheat"
|
||||
duration = 3 MINUTES
|
||||
/// Things which will chill us out if we get hit by them
|
||||
var/static/list/chilling_reagents = list(
|
||||
/datum/reagent/medicine/cryoxadone,
|
||||
/datum/reagent/firefighting_foam,
|
||||
/datum/reagent/consumable/frostoil,
|
||||
/datum/reagent/consumable/ice,
|
||||
/datum/reagent/water,
|
||||
)
|
||||
|
||||
/datum/status_effect/basilisk_overheat/on_apply()
|
||||
. = ..()
|
||||
if (!. || !istype(owner, /mob/living/basic/mining/basilisk) || owner.stat != CONSCIOUS)
|
||||
return FALSE
|
||||
var/mob/living/basic/mining/basilisk/hot_stuff = owner
|
||||
hot_stuff.visible_message(span_warning("[hot_stuff] is getting fired up!"))
|
||||
hot_stuff.fully_heal()
|
||||
hot_stuff.icon_living = "basilisk_alert"
|
||||
hot_stuff.icon_state = "basilisk_alert"
|
||||
hot_stuff.update_appearance(UPDATE_ICON_STATE)
|
||||
hot_stuff.add_movespeed_modifier(/datum/movespeed_modifier/basilisk_overheat)
|
||||
hot_stuff.set_projectile_type(/obj/projectile/basilisk_hot)
|
||||
|
||||
RegisterSignal(hot_stuff, COMSIG_LIVING_DEATH, PROC_REF(on_death))
|
||||
RegisterSignal(hot_stuff, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(on_splashed))
|
||||
RegisterSignal(hot_stuff, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_shot))
|
||||
|
||||
/datum/status_effect/basilisk_overheat/on_remove()
|
||||
. = ..()
|
||||
var/mob/living/basic/mining/basilisk/hot_stuff = owner
|
||||
hot_stuff.icon_living = "basilisk"
|
||||
hot_stuff.icon_state = "basilisk"
|
||||
hot_stuff.set_projectile_type(/obj/projectile/temp/watcher)
|
||||
|
||||
hot_stuff.update_appearance(UPDATE_ICON_STATE)
|
||||
hot_stuff.remove_movespeed_modifier(/datum/movespeed_modifier/basilisk_overheat)
|
||||
UnregisterSignal(hot_stuff, list(COMSIG_LIVING_DEATH, COMSIG_ATOM_EXPOSE_REAGENTS, COMSIG_ATOM_BULLET_ACT))
|
||||
|
||||
if (hot_stuff.stat != CONSCIOUS)
|
||||
return
|
||||
hot_stuff.visible_message(span_notice("[hot_stuff] seems to have cooled down."))
|
||||
var/obj/effect/particle_effect/fluid/smoke/poof = new(get_turf(hot_stuff))
|
||||
poof.lifetime = 2 SECONDS
|
||||
|
||||
/// Cool down if we die
|
||||
/datum/status_effect/basilisk_overheat/proc/on_death()
|
||||
SIGNAL_HANDLER
|
||||
qdel(src)
|
||||
|
||||
/// Cool down if splashed with water
|
||||
/datum/status_effect/basilisk_overheat/proc/on_splashed(atom/source, list/reagents, datum/reagents/source_reagents, methods, volume_modifier, show_message)
|
||||
SIGNAL_HANDLER
|
||||
if(!(methods & (TOUCH|VAPOR)))
|
||||
return
|
||||
for (var/datum/reagent in reagents)
|
||||
if (!is_type_in_list(reagent, chilling_reagents))
|
||||
continue
|
||||
qdel(src)
|
||||
return
|
||||
|
||||
/// Cool down if shot with a cryo beam
|
||||
/datum/status_effect/basilisk_overheat/proc/on_shot(datum/source, obj/projectile/temp/cryo_shot)
|
||||
SIGNAL_HANDLER
|
||||
if (!istype(cryo_shot) || cryo_shot.temperature > 0)
|
||||
return
|
||||
qdel(src)
|
||||
|
||||
/// Projectile basilisks use when hot
|
||||
/obj/projectile/basilisk_hot
|
||||
name = "energy blast"
|
||||
icon_state = "chronobolt"
|
||||
damage = 40
|
||||
damage_type = BRUTE
|
||||
@@ -11,7 +11,6 @@
|
||||
gender = MALE // Female ones are the bipedal elites
|
||||
speed = 30
|
||||
basic_mob_flags = IMMUNE_TO_FISTS
|
||||
mob_biotypes = MOB_ORGANIC|MOB_BEAST
|
||||
maxHealth = 300
|
||||
health = 300
|
||||
friendly_verb_continuous = "wails at"
|
||||
@@ -50,7 +49,6 @@
|
||||
|
||||
/mob/living/basic/mining/goliath/Initialize(mapload)
|
||||
. = ..()
|
||||
ADD_TRAIT(src, TRAIT_NO_GLIDE, INNATE_TRAIT)
|
||||
ADD_TRAIT(src, TRAIT_TENTACLE_IMMUNE, INNATE_TRAIT)
|
||||
AddElement(/datum/element/ai_retaliate)
|
||||
AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY)
|
||||
@@ -168,16 +166,6 @@
|
||||
. = ..()
|
||||
faction = new_friend.faction.Copy()
|
||||
|
||||
/// Goliath which sometimes replaces itself with a rare variant
|
||||
/mob/living/basic/mining/goliath/random
|
||||
|
||||
/mob/living/basic/mining/goliath/random/Initialize(mapload)
|
||||
. = ..()
|
||||
if(!prob(1))
|
||||
return
|
||||
new /mob/living/basic/mining/goliath/ancient/immortal(loc)
|
||||
return INITIALIZE_HINT_QDEL
|
||||
|
||||
/// Legacy Goliath mob with different sprites, largely the same behaviour
|
||||
/mob/living/basic/mining/goliath/ancient
|
||||
name = "ancient goliath"
|
||||
@@ -234,3 +222,15 @@
|
||||
desc = "This rough saddle will give you a serviceable seat upon a goliath! Provided you can get one to stand still."
|
||||
icon = 'icons/obj/mining.dmi'
|
||||
icon_state = "goliath_saddle"
|
||||
|
||||
/// For map generation, has a chance to instantiate as a special subtype
|
||||
/obj/effect/spawner/random/goliath
|
||||
name = "random goliath"
|
||||
desc = "Chance to spawn a rare shiny version."
|
||||
icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi'
|
||||
icon_state = "goliath"
|
||||
pixel_x = -12
|
||||
loot = list(
|
||||
/mob/living/basic/mining/goliath = 99,
|
||||
/mob/living/basic/mining/goliath/ancient/immortal = 1,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
icon_state = "arctic_lobstrosity"
|
||||
icon_living = "arctic_lobstrosity"
|
||||
icon_dead = "arctic_lobstrosity_dead"
|
||||
mob_biotypes = MOB_ORGANIC|MOB_BEAST
|
||||
friendly_verb_continuous = "chitters at"
|
||||
friendly_verb_simple = "chitters at"
|
||||
speak_emote = list("chitters")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
///prototype for mining mobs
|
||||
/mob/living/basic/mining
|
||||
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
|
||||
combat_mode = TRUE
|
||||
mob_size = MOB_SIZE_LARGE
|
||||
mob_biotypes = MOB_ORGANIC|MOB_BEAST
|
||||
faction = list(FACTION_MINING)
|
||||
unsuitable_atmos_damage = 0
|
||||
minimum_survivable_temperature = 0
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/// A floating eyeball which keeps its distance and plays red light/green light with you.
|
||||
/mob/living/basic/mining/watcher
|
||||
name = "watcher"
|
||||
desc = "A levitating, monocular creature held aloft by wing-like veins. A sharp spine of crystal protrudes from its body."
|
||||
icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi'
|
||||
icon_state = "watcher"
|
||||
icon_living = "watcher"
|
||||
icon_dead = "watcher_dead"
|
||||
health_doll_icon = "watcher"
|
||||
pixel_x = -12
|
||||
base_pixel_x = -12
|
||||
speak_emote = list("chimes")
|
||||
speed = 3
|
||||
maxHealth = 200
|
||||
health = 200
|
||||
attack_verb_continuous = "buffets"
|
||||
attack_verb_simple = "buffet"
|
||||
crusher_loot = /obj/item/crusher_trophy/watcher_wing
|
||||
ai_controller = /datum/ai_controller/basic_controller/watcher
|
||||
butcher_results = list(
|
||||
/obj/item/stack/sheet/bone = 1,
|
||||
/obj/item/stack/ore/diamond = 2,
|
||||
/obj/item/stack/sheet/sinew = 2,
|
||||
)
|
||||
/// How often can we shoot?
|
||||
var/ranged_cooldown = 3 SECONDS
|
||||
/// What kind of beams we got?
|
||||
var/projectile_type = /obj/projectile/temp/watcher
|
||||
/// Icon state for our eye overlay
|
||||
var/eye_glow = "ice_glow"
|
||||
/// Sound to play when we shoot
|
||||
var/shoot_sound = 'sound/weapons/pierce.ogg'
|
||||
/// Typepath of our gaze ability
|
||||
var/gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze
|
||||
// We attract and eat these things for some reason
|
||||
var/list/wanted_objects = list(
|
||||
/obj/item/stack/sheet/mineral/diamond,
|
||||
/obj/item/stack/ore/diamond,
|
||||
/obj/item/pen/survival,
|
||||
)
|
||||
|
||||
/mob/living/basic/mining/watcher/Initialize(mapload)
|
||||
. = ..()
|
||||
AddElement(/datum/element/ai_retaliate)
|
||||
AddElement(/datum/element/simple_flying)
|
||||
AddElement(/datum/element/content_barfer)
|
||||
AddComponent(/datum/component/basic_ranged_ready_overlay, overlay_state = eye_glow)
|
||||
AddComponent(\
|
||||
/datum/component/ranged_attacks,\
|
||||
cooldown_time = ranged_cooldown,\
|
||||
projectile_type = projectile_type,\
|
||||
projectile_sound = shoot_sound,\
|
||||
)
|
||||
AddComponent(\
|
||||
/datum/component/magnet,\
|
||||
attracted_typecache = wanted_objects,\
|
||||
on_contact = CALLBACK(src, PROC_REF(consume)),\
|
||||
)
|
||||
update_appearance(UPDATE_OVERLAYS)
|
||||
|
||||
var/datum/action/cooldown/mob_cooldown/watcher_overwatch/overwatch = new(src)
|
||||
overwatch.Grant(src)
|
||||
overwatch.projectile_type = projectile_type
|
||||
ai_controller.set_blackboard_key(BB_WATCHER_OVERWATCH, overwatch)
|
||||
|
||||
var/datum/action/cooldown/mob_cooldown/watcher_gaze/gaze = new gaze_attack(src)
|
||||
gaze.Grant(src)
|
||||
ai_controller.set_blackboard_key(BB_WATCHER_GAZE, gaze)
|
||||
|
||||
/mob/living/basic/mining/watcher/update_overlays()
|
||||
. = ..()
|
||||
. += emissive_appearance(icon, "watcher_emissive", src)
|
||||
|
||||
/// I love eating diamonds yum
|
||||
/mob/living/basic/mining/watcher/proc/consume(atom/movable/thing)
|
||||
visible_message(span_warning("[thing] seems to vanish into [src]'s body!"))
|
||||
thing.forceMove(src)
|
||||
|
||||
/// More durable, burning projectiles
|
||||
/mob/living/basic/mining/watcher/magmawing
|
||||
name = "magmawing watcher"
|
||||
desc = "Presented with extreme temperatures, adaptive watchers absorb heat through their circulatory wings and repurpose it as a weapon."
|
||||
icon_state = "watcher_magmawing"
|
||||
icon_living = "watcher_magmawing"
|
||||
icon_dead = "watcher_magmawing_dead"
|
||||
eye_glow = "fire_glow"
|
||||
maxHealth = 215 //Compensate for the lack of slowdown on projectiles with a bit of extra health
|
||||
health = 215
|
||||
projectile_type = /obj/projectile/temp/watcher/magma_wing
|
||||
gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/fire
|
||||
crusher_loot = /obj/item/crusher_trophy/blaster_tubes/magma_wing
|
||||
crusher_drop_chance = 100 // There's only going to be one of these per round throw them a bone
|
||||
|
||||
/// Less durable, freezing projectiles
|
||||
/mob/living/basic/mining/watcher/icewing
|
||||
name = "icewing watcher"
|
||||
desc = "Watchers which fail to absorb enough heat during their development become fragile, but share their internal chill with their enemies."
|
||||
icon_state = "watcher_icewing"
|
||||
icon_living = "watcher_icewing"
|
||||
icon_dead = "watcher_icewing_dead"
|
||||
maxHealth = 170
|
||||
health = 170
|
||||
projectile_type = /obj/projectile/temp/watcher/ice_wing
|
||||
gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/ice
|
||||
butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/bone = 1)
|
||||
crusher_loot = /obj/item/crusher_trophy/watcher_wing/ice_wing
|
||||
crusher_drop_chance = 100
|
||||
|
||||
/// For map generation, has a chance to instantiate as a special subtype
|
||||
/obj/effect/spawner/random/watcher
|
||||
name = "random watcher"
|
||||
desc = "Chance to spawn a rare shiny version."
|
||||
icon = 'icons/mob/simple/lavaland/lavaland_monsters_wide.dmi'
|
||||
icon_state = "watcher"
|
||||
pixel_x = -12
|
||||
loot = list(
|
||||
/mob/living/basic/mining/watcher = 80,
|
||||
/mob/living/basic/mining/watcher/magmawing = 15,
|
||||
/mob/living/basic/mining/watcher/icewing = 5,
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
/datum/ai_controller/basic_controller/watcher
|
||||
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/target_retaliate,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/use_mob_ability/gaze,
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/overwatch,
|
||||
/datum/ai_planning_subtree/ranged_skirmish/watcher,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
)
|
||||
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/overwatch
|
||||
ability_key = BB_WATCHER_OVERWATCH
|
||||
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/overwatch/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCH_IMMUNE))
|
||||
return // We should probably let miners move sometimes
|
||||
return ..()
|
||||
|
||||
/datum/ai_planning_subtree/use_mob_ability/gaze
|
||||
ability_key = BB_WATCHER_GAZE
|
||||
finish_planning = TRUE
|
||||
|
||||
/datum/ai_planning_subtree/use_mob_ability/gaze/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/watcher = controller.pawn
|
||||
if (watcher.health > watcher.maxHealth * 0.66) // When we're a little hurt
|
||||
return
|
||||
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
if (!isliving(target))
|
||||
return // Don't do this if there's nothing hostile around or if our target is a mech
|
||||
return ..()
|
||||
|
||||
/datum/ai_planning_subtree/ranged_skirmish/watcher
|
||||
attack_behavior = /datum/ai_behavior/ranged_skirmish/watcher
|
||||
|
||||
/datum/ai_planning_subtree/ranged_skirmish/watcher/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCHED))
|
||||
return // Don't bully people who are playing red light green light
|
||||
return ..()
|
||||
|
||||
/datum/ai_behavior/ranged_skirmish/watcher
|
||||
min_range = 0
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Do something nasty to everyone nearby if they're looking at us.
|
||||
*/
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze
|
||||
name = "Disorienting Gaze"
|
||||
desc = "After a delay, flash everyone looking at you."
|
||||
button_icon = 'icons/mob/actions/actions_animal.dmi'
|
||||
button_icon_state = "gaze"
|
||||
background_icon_state = "bg_demon"
|
||||
overlay_icon_state = "bg_demon_border"
|
||||
cooldown_time = 30 SECONDS
|
||||
check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
|
||||
click_to_activate = FALSE
|
||||
shared_cooldown = null
|
||||
/// At what range do we check for vision?
|
||||
var/effect_radius = 7
|
||||
/// How long does it take to play our various animation stages
|
||||
var/animation_time = 0.8 SECONDS
|
||||
/// How long after pressing the button do we give people to turn around?
|
||||
var/wait_delay = 1.6 SECONDS
|
||||
/// What are we currently displaying?
|
||||
var/image/current_overlay
|
||||
/// Timer until we go to the next stage
|
||||
var/stage_timer
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/Activate(mob/living/target)
|
||||
show_indicator_overlay("eye_open")
|
||||
stage_timer = addtimer(CALLBACK(src, PROC_REF(show_indicator_overlay), "eye_pulse"), animation_time, TIMER_STOPPABLE)
|
||||
StartCooldown(360 SECONDS, 360 SECONDS)
|
||||
if (do_after(owner, delay = wait_delay, target = owner))
|
||||
trigger_effect()
|
||||
else
|
||||
deltimer(stage_timer)
|
||||
clear_current_overlay()
|
||||
StartCooldown()
|
||||
return TRUE
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/Destroy()
|
||||
deltimer(stage_timer)
|
||||
clear_current_overlay()
|
||||
return ..()
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/Remove(mob/removed_from)
|
||||
deltimer(stage_timer)
|
||||
clear_current_overlay()
|
||||
return ..()
|
||||
|
||||
/// Do some effects to whoever is looking at us
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/trigger_effect()
|
||||
deltimer(stage_timer)
|
||||
show_indicator_overlay("eye_flash")
|
||||
for (var/mob/living/viewer in viewers(effect_radius, owner))
|
||||
var/view_dir = get_dir(viewer, owner)
|
||||
if (!(viewer.dir & view_dir) || viewer.stat != CONSCIOUS)
|
||||
continue
|
||||
if (!apply_effect(viewer))
|
||||
continue
|
||||
var/image/flashed_overlay = image(
|
||||
icon = 'icons/effects/eldritch.dmi',
|
||||
loc = viewer,
|
||||
icon_state = "eye_flash",
|
||||
pixel_x = -viewer.pixel_x,
|
||||
pixel_y = -viewer.pixel_y,
|
||||
)
|
||||
flick_overlay_global(flashed_overlay, show_to = GLOB.clients, duration = animation_time)
|
||||
stage_timer = addtimer(CALLBACK(src, PROC_REF(hide_eye)), animation_time, TIMER_STOPPABLE)
|
||||
|
||||
/// Do something bad to someone who was looking at us
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/apply_effect(mob/living/viewer)
|
||||
if (!viewer.flash_act(intensity = 4, affect_silicon = TRUE, visual = TRUE, length = 3 SECONDS))
|
||||
return FALSE
|
||||
viewer.set_confusion_if_lower(12 SECONDS)
|
||||
return TRUE
|
||||
|
||||
/// Animate our effect out
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/hide_eye()
|
||||
show_indicator_overlay("eye_close")
|
||||
stage_timer = addtimer(CALLBACK(src, PROC_REF(clear_current_overlay)), animation_time, TIMER_STOPPABLE)
|
||||
|
||||
/// Display an animated overlay over our head to indicate what's going on
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/show_indicator_overlay(overlay_state)
|
||||
clear_current_overlay()
|
||||
current_overlay = image(icon = 'icons/effects/eldritch.dmi', loc = owner, icon_state = overlay_state, pixel_x = -owner.pixel_x, pixel_y = 28)
|
||||
for(var/client/add_to in GLOB.clients)
|
||||
add_to.images += current_overlay
|
||||
|
||||
/// Hide whatever overlay we are showing
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/proc/clear_current_overlay()
|
||||
if (!isnull(current_overlay))
|
||||
remove_image_from_clients(current_overlay, GLOB.clients)
|
||||
current_overlay = null
|
||||
|
||||
/// Magmawing glare burns you
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/fire
|
||||
name = "Searing Glare"
|
||||
desc = "After a delay, burn and stun everyone looking at you."
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/fire/apply_effect(mob/living/viewer)
|
||||
viewer.Paralyze(3 SECONDS)
|
||||
viewer.adjust_fire_stacks(10)
|
||||
viewer.ignite_mob()
|
||||
return TRUE
|
||||
|
||||
/// Icewing glare freezes you
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/ice
|
||||
name = "Cold Stare"
|
||||
desc = "After a delay, freeze and repulse everyone looking at you."
|
||||
/// Max distance to throw people looking at us
|
||||
var/max_throw = 3
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_gaze/ice/apply_effect(mob/living/viewer)
|
||||
viewer.apply_status_effect(/datum/status_effect/freon/watcher/extended)
|
||||
viewer.safe_throw_at(
|
||||
target = get_edge_target_turf(owner, get_dir(owner, get_step_away(viewer, owner))),
|
||||
range = max_throw,
|
||||
speed = 1,
|
||||
thrower = owner,
|
||||
force = MOVE_FORCE_EXTREMELY_STRONG,
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Automatically shoot at a target if they do anything while this is active on them.
|
||||
*/
|
||||
/datum/action/cooldown/mob_cooldown/watcher_overwatch
|
||||
name = "Overwatch"
|
||||
desc = "Keep a close eye on the target's actions, automatically firing upon them if they act."
|
||||
button_icon = 'icons/mob/actions/actions_ecult.dmi'
|
||||
button_icon_state = "eye"
|
||||
background_icon_state = "bg_demon"
|
||||
overlay_icon_state = "bg_demon_border"
|
||||
cooldown_time = 20 SECONDS
|
||||
check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
|
||||
click_to_activate = TRUE
|
||||
shared_cooldown = null
|
||||
/// Furthest range we can activate ability at
|
||||
var/max_range = 7
|
||||
/// Type of projectile to fire
|
||||
var/projectile_type = /obj/projectile/temp/watcher
|
||||
/// Sound the projectile we fire makes
|
||||
var/projectile_sound = 'sound/weapons/pierce.ogg'
|
||||
/// Time to watch for
|
||||
var/overwatch_duration = 3 SECONDS
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_overwatch/New(Target, original)
|
||||
. = ..()
|
||||
melee_cooldown_time = overwatch_duration
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_overwatch/PreActivate(atom/target)
|
||||
if (target == owner)
|
||||
return
|
||||
if (ismecha(target))
|
||||
var/obj/vehicle/sealed/mecha/mech = target
|
||||
var/list/drivers = mech.return_drivers()
|
||||
if (!length(drivers))
|
||||
return
|
||||
target = drivers[1]
|
||||
if (!isliving(target))
|
||||
return
|
||||
if (get_dist(owner, target) > max_range)
|
||||
return
|
||||
return ..()
|
||||
|
||||
/datum/action/cooldown/mob_cooldown/watcher_overwatch/Activate(mob/living/target)
|
||||
var/mob/living/living_owner = owner
|
||||
living_owner.face_atom(target)
|
||||
living_owner.Stun(overwatch_duration, ignore_canstun = TRUE)
|
||||
target.apply_status_effect(/datum/status_effect/overwatch, overwatch_duration, owner, projectile_type, projectile_sound)
|
||||
StartCooldown()
|
||||
return TRUE
|
||||
|
||||
/// Status effect which tracks whether our overwatched mob moves or acts
|
||||
/datum/status_effect/overwatch
|
||||
id = "watcher_overwatch"
|
||||
duration = 5 SECONDS
|
||||
status_type = STATUS_EFFECT_MULTIPLE
|
||||
alert_type = /atom/movable/screen/alert/status_effect/overwatch
|
||||
/// Distance at which we break off the ability
|
||||
var/watch_range = 9
|
||||
/// Visual effect to make the status obvious
|
||||
var/datum/beam/link
|
||||
/// Which watcher is watching?
|
||||
var/mob/living/watcher
|
||||
/// Type of projectile to fire
|
||||
var/projectile_type
|
||||
/// Noise to make when we shoot beam
|
||||
var/projectile_sound
|
||||
/// Did the overwatch ever trigger during our run?
|
||||
var/overwatch_triggered = FALSE
|
||||
/// Signals which trigger a hostile response
|
||||
var/static/list/forbidden_actions = list(
|
||||
COMSIG_MOB_ABILITY_FINISHED,
|
||||
COMSIG_MOB_ATTACK_HAND,
|
||||
COMSIG_MOB_DROVE_MECH,
|
||||
COMSIG_MOB_FIRED_GUN,
|
||||
COMSIG_MOB_ITEM_ATTACK,
|
||||
COMSIG_MOB_THROW,
|
||||
COMSIG_MOB_USED_MECH_EQUIPMENT,
|
||||
COMSIG_MOB_USED_MECH_MELEE,
|
||||
COMSIG_MOVABLE_MOVED,
|
||||
)
|
||||
|
||||
/datum/status_effect/overwatch/on_creation(mob/living/new_owner, set_duration, mob/living/watcher, projectile_type, projectile_sound)
|
||||
if (isnull(watcher) || isnull(projectile_type))
|
||||
return FALSE
|
||||
if (HAS_TRAIT(new_owner, TRAIT_OVERWATCH_IMMUNE))
|
||||
return FALSE
|
||||
src.watcher = watcher
|
||||
src.projectile_type = projectile_type
|
||||
src.projectile_sound = projectile_sound
|
||||
if (!isnull(set_duration))
|
||||
duration = set_duration
|
||||
return ..()
|
||||
|
||||
/datum/status_effect/overwatch/on_apply()
|
||||
. = ..()
|
||||
if (!.)
|
||||
return FALSE
|
||||
ADD_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id))
|
||||
owner.do_alert_animation()
|
||||
owner.Immobilize(0.25 SECONDS) // Just long enough that they don't trigger it by mistake
|
||||
owner.playsound_local(owner, 'sound/machines/chime.ogg', 50, TRUE)
|
||||
var/atom/beam_origin = ismecha(owner.loc) ? owner.loc : owner
|
||||
link = beam_origin.Beam(watcher, icon_state = "r_beam", override_target_pixel_x = 0)
|
||||
RegisterSignals(owner, forbidden_actions, PROC_REF(opportunity_attack))
|
||||
RegisterSignals(owner, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_participant_died))
|
||||
RegisterSignals(watcher, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_participant_died))
|
||||
|
||||
/datum/status_effect/overwatch/on_remove()
|
||||
UnregisterSignal(owner, forbidden_actions + list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
|
||||
QDEL_NULL(link)
|
||||
REMOVE_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id))
|
||||
if (!QDELETED(owner))
|
||||
owner.apply_status_effect(/datum/status_effect/overwatch_immune)
|
||||
return ..()
|
||||
|
||||
/datum/status_effect/overwatch/Destroy()
|
||||
QDEL_NULL(link)
|
||||
if (!isnull(watcher)) // Side effects in Destroy? Well it turns out `on_remove` is also just called on Destroy. But only if the owner isn't deleting.
|
||||
INVOKE_ASYNC(src, PROC_REF(unregister_watcher), watcher)
|
||||
watcher = null
|
||||
|
||||
return ..()
|
||||
|
||||
/// Clean up our association with the caster of this ability.
|
||||
/datum/status_effect/overwatch/proc/unregister_watcher(mob/living/former_overwatcher)
|
||||
if (!overwatch_triggered)
|
||||
former_overwatcher.Stun(2 SECONDS, ignore_canstun = TRUE)
|
||||
UnregisterSignal(former_overwatcher, list(COMSIG_QDELETING, COMSIG_LIVING_DEATH))
|
||||
|
||||
/// Uh oh, you did something within my threat radius, now we're going to shoot you
|
||||
/datum/status_effect/overwatch/proc/opportunity_attack()
|
||||
SIGNAL_HANDLER
|
||||
if (!can_see(watcher, owner, length = watch_range))
|
||||
qdel(src)
|
||||
return
|
||||
overwatch_triggered = TRUE
|
||||
INVOKE_ASYNC(watcher, TYPE_PROC_REF(/atom/, fire_projectile), projectile_type, owner, projectile_sound)
|
||||
|
||||
/// Can't overwatch you if I don't exist
|
||||
/datum/status_effect/overwatch/proc/on_participant_died()
|
||||
SIGNAL_HANDLER
|
||||
qdel(src)
|
||||
|
||||
/atom/movable/screen/alert/status_effect/overwatch
|
||||
name = "Overwatched"
|
||||
desc = "Freeze! You are being watched!"
|
||||
icon_state = "aimed"
|
||||
|
||||
/// Blocks further applications of the ability for a little while
|
||||
/datum/status_effect/overwatch_immune
|
||||
id = "watcher_overwatch_immunity"
|
||||
duration = 10 SECONDS // To stop watcher tendrils spamming the shit out of you
|
||||
alert_type = null
|
||||
|
||||
/datum/status_effect/overwatch_immune/on_apply()
|
||||
. = ..()
|
||||
ADD_TRAIT(owner, TRAIT_OVERWATCH_IMMUNE, TRAIT_STATUS_EFFECT(id))
|
||||
|
||||
/datum/status_effect/overwatch_immune/on_remove()
|
||||
REMOVE_TRAIT(owner, TRAIT_OVERWATCH_IMMUNE, TRAIT_STATUS_EFFECT(id))
|
||||
return ..()
|
||||
@@ -0,0 +1,37 @@
|
||||
/// Chilling projectile, hurts and slows you down
|
||||
/obj/projectile/temp/watcher
|
||||
name = "chilling blast"
|
||||
icon_state = "ice_2"
|
||||
damage = 10
|
||||
damage_type = BURN
|
||||
armor_flag = ENERGY
|
||||
temperature = -50
|
||||
|
||||
/obj/projectile/temp/watcher/on_hit(mob/living/target, blocked = 0)
|
||||
. = ..()
|
||||
if (!isliving(target))
|
||||
return
|
||||
apply_status(target)
|
||||
|
||||
/// Apply an additional on-hit effect
|
||||
/obj/projectile/temp/watcher/proc/apply_status(mob/living/target)
|
||||
target.apply_status_effect(/datum/status_effect/freezing_blast)
|
||||
|
||||
/// Lava projectile, ignites you
|
||||
/obj/projectile/temp/watcher/magma_wing
|
||||
name = "scorching blast"
|
||||
icon_state = "lava"
|
||||
damage = 5
|
||||
temperature = 200
|
||||
|
||||
/obj/projectile/temp/watcher/magma_wing/apply_status(mob/living/target)
|
||||
target.adjust_fire_stacks(0.1)
|
||||
target.ignite_mob()
|
||||
|
||||
/// Freezing projectile, freezes you
|
||||
/obj/projectile/temp/watcher/ice_wing
|
||||
name = "freezing blast"
|
||||
damage = 5
|
||||
|
||||
/obj/projectile/temp/watcher/ice_wing/apply_status(mob/living/target)
|
||||
target.apply_status_effect(/datum/status_effect/freon/watcher)
|
||||
Reference in New Issue
Block a user