Bioscrambler Anomaly chases you (#82555)

## About The Pull Request

I heard reports that people just ignore the bioscrambler anomaly because
basically you just don't go into that room any more and depending on
where it spawned, that's no big deal.
That won't do.

Now the Bioscrambler will be attracted to the nearest sign of advanced
thinking life (read: nearest humanoid mob controlled by a player) and
will very slowly pursue them, travelling through walls and obstacles in
order to do so.

Also if it decides to target you, you will get a foreboding psychic
warning like with the dark matteor, because I think it's funny for dire
warnings to have multiple obscuring sources.

The Bioscrambler can be blocked with containment fields if you want to
make an overly-elaborate pen for it.
To accomplish this I refactored containment fields a little bit to apply
turf traits instead of making four different `locate()` checks for
different objects. Those files smell bad.

Oh also I moved the dullahan organs to the Bioscrambler blacklist
because they runtimed while I was testing it (see also: my other
incoming PRs) and I can't see any other reasonable way to fix it (they
expect to be in an abstract body zone...)

## Why It's Good For The Game

Anomalies are generally meant to be problems that you deal with or face
some kind of consequence.
Because the Bioscrambler isn't a timed anomaly with a dramatic
detonation effect, being spawned in a poorly-trafficked area could
simply mean that it isn't a problem to anyone.
Now it will make sure that it is a problem for someone until someone
gets rid of it.

I thought this solution was funnier than making it do something zany if
you leave it alone for 3 minutes.

## Changelog

🆑
balance: The Bioscrambler will now actively attempt to get closer to
living targets rather than chilling in a closet nobody goes into (unless
you trap it in a containment field).
balance: Because it can now travel through walls, the Bioscrambler will
no longer transform you THROUGH walls.
/🆑
This commit is contained in:
Jacquerel
2024-04-12 13:49:34 -04:00
committed by GitHub
parent 336ffa9d83
commit 16438d0d4e
12 changed files with 99 additions and 37 deletions
+3
View File
@@ -34,10 +34,13 @@ GLOBAL_LIST_INIT(bioscrambler_organs_blacklist, typecacheof(list (
/obj/item/organ/internal/brain,
/obj/item/organ/internal/body_egg,
/obj/item/organ/internal/cyberimp,
/obj/item/organ/internal/ears/dullahan,
/obj/item/organ/internal/eyes/dullahan,
/obj/item/organ/internal/heart/cursed,
/obj/item/organ/internal/heart/demon,
/obj/item/organ/internal/lungs,
/obj/item/organ/internal/monster_core,
/obj/item/organ/internal/tongue/dullahan,
/obj/item/organ/internal/vocal_cords/colossus,
/obj/item/organ/internal/zombie_infection,
)))
+3
View File
@@ -813,6 +813,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
///Trait applied to turfs when an atmos holosign is placed on them. It will stop firedoors from closing.
#define TRAIT_FIREDOOR_STOP "firedoor_stop"
///Trait applied to turf blocked by a containment field
#define TRAIT_CONTAINMENT_FIELD "containment_field"
/// Trait applied when the MMI component is added to an [/obj/item/integrated_circuit]
#define TRAIT_COMPONENT_MMI "component_mmi"
+1
View File
@@ -599,6 +599,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
),
/turf = list(
"TRAIT_CHASM_STOPPED" = TRAIT_CHASM_STOPPED,
"TRAIT_CONTAINMENT_FIELD" = TRAIT_CONTAINMENT_FIELD,
"TRAIT_FIREDOOR_STOP" = TRAIT_FIREDOOR_STOP,
"TRAIT_HYPERSPACE_STOPPED" = TRAIT_HYPERSPACE_STOPPED,
"TRAIT_IMMERSE_STOPPED" = TRAIT_IMMERSE_STOPPED,
+3 -18
View File
@@ -360,8 +360,9 @@
target = potentially_closer
//if we lost that target get a new one
if(!target || QDELETED(target))
target = find_new_target()
foreboding_nosebleed(target)
var/mob/living/new_target = find_new_target()
new_target?.ominous_nosebleed()
target = new_target
return ..()
///Searches the living list for the closest target, and begins chasing them down.
@@ -380,22 +381,6 @@
closest_target = target
return closest_target
/// gives a little fluff warning that someone is being hunted.
/datum/component/singularity/bloodthirsty/proc/foreboding_nosebleed(mob/living/target)
if(!iscarbon(target))
to_chat(target, span_warning("You feel a bit nauseous for just a moment."))
return
var/mob/living/carbon/carbon_target = target
var/obj/item/bodypart/head = carbon_target.get_bodypart(BODY_ZONE_HEAD)
if(head)
if(HAS_TRAIT(carbon_target, TRAIT_NOBLOOD))
to_chat(carbon_target, span_notice("You get a headache."))
return
head.adjustBleedStacks(5)
carbon_target.visible_message(span_notice("[carbon_target] gets a nosebleed."), span_warning("You get a nosebleed."))
return
to_chat(target, span_warning("You feel a bit nauseous for just a moment."))
#undef CHANCE_TO_MOVE_TO_TARGET
#undef CHANCE_TO_MOVE_TO_TARGET_BLOODTHIRSTY
#undef CHANCE_TO_CHANGE_TARGET_BLOODTHIRSTY
+17 -7
View File
@@ -364,11 +364,8 @@
visible_message(span_danger("[src] shuts down due to lack of power!"), \
"If this message is ever seen, something is wrong.",
span_hear("You hear heavy droning fade out."))
active = FALSE
deactivate()
log_game("[src] deactivated due to lack of power at [AREACOORD(src)]")
for(var/d in GLOB.cardinals)
cleanup_field(d)
update_appearance()
else
update_appearance()
for(var/d in GLOB.cardinals)
@@ -463,7 +460,6 @@
balloon_alert(user, "malfunctioning!")
else
balloon_alert(user, "no access!")
return
add_fingerprint(user)
@@ -493,13 +489,13 @@
user.visible_message(span_notice("[user] turned \the [src] off."), \
span_notice("You turn off \the [src]."), \
span_hear("You hear heavy droning fade out."))
active = FALSE
deactivate()
user.log_message("deactivated [src].", LOG_GAME)
else
user.visible_message(span_notice("[user] turned \the [src] on."), \
span_notice("You turn on \the [src]."), \
span_hear("You hear heavy droning."))
active = ACTIVE_SETUPFIELDS
activate()
user.log_message("activated [src].", LOG_GAME)
add_fingerprint(user)
@@ -513,6 +509,19 @@
balloon_alert(user, "access controller shorted")
return TRUE
/// Turn the machine on with side effects
/obj/machinery/power/shieldwallgen/proc/activate()
active = ACTIVE_SETUPFIELDS
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
/// Turn the machine off with side effects
/obj/machinery/power/shieldwallgen/proc/deactivate()
active = FALSE
for(var/d in GLOB.cardinals)
cleanup_field(d)
update_appearance()
RemoveElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
//////////////Containment Field START
/obj/machinery/shieldwall
name = "shield wall"
@@ -538,6 +547,7 @@
L.investigate_log("has been gibbed by [src].", INVESTIGATE_DEATHS)
L.gib(DROP_ALL_REMAINS)
RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, PROC_REF(block_singularity))
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
/obj/machinery/shieldwall/Destroy()
gen_primary = null
@@ -23,6 +23,8 @@
var/immortal = FALSE
///Do we stay in one place?
var/immobile = FALSE
///Chance per second that we will move
var/move_chance = ANOMALY_MOVECHANCE
/obj/effect/anomaly/Initialize(mapload, new_lifespan, drops_core = TRUE)
. = ..()
@@ -76,8 +78,12 @@
return ..()
/obj/effect/anomaly/proc/anomalyEffect(seconds_per_tick)
if(!immobile && SPT_PROB(ANOMALY_MOVECHANCE, seconds_per_tick))
step(src,pick(GLOB.alldirs))
if(SPT_PROB(move_chance, seconds_per_tick))
move_anomaly()
/// Move in a direction
/obj/effect/anomaly/proc/move_anomaly()
step(src, pick(GLOB.alldirs))
/obj/effect/anomaly/proc/detonate()
return
@@ -4,6 +4,10 @@
icon_state = "bioscrambler"
aSignal = /obj/item/assembly/signaler/anomaly/bioscrambler
immortal = TRUE
pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE | PASSCLOSEDTURF | PASSMACHINE | PASSSTRUCTURE | PASSDOORS
layer = ABOVE_MOB_LAYER
/// Who are we moving towards?
var/datum/weakref/pursuit_target
/// Cooldown for every anomaly pulse
COOLDOWN_DECLARE(pulse_cooldown)
/// How many seconds between each anomaly pulses
@@ -11,11 +15,52 @@
/// Range of the anomaly pulse
var/range = 5
/obj/effect/anomaly/bioscrambler/Initialize(mapload, new_lifespan, drops_core)
. = ..()
pursuit_target = WEAKREF(find_nearest_target())
/obj/effect/anomaly/bioscrambler/anomalyEffect(seconds_per_tick)
. = ..()
if(!COOLDOWN_FINISHED(src, pulse_cooldown))
return
COOLDOWN_START(src, pulse_cooldown, pulse_delay)
for(var/mob/living/carbon/nearby in range(range, src))
for(var/mob/living/carbon/nearby in hearers(range, src))
nearby.bioscramble(name)
/obj/effect/anomaly/bioscrambler/move_anomaly()
var/mob/living/current_target = pursuit_target?.resolve()
if (QDELETED(current_target))
pursuit_target = null
if (isnull(pursuit_target) || prob(20))
var/mob/living/new_target = find_nearest_target()
if (isnull(new_target))
pursuit_target = null
else if (new_target != current_target)
current_target = new_target
pursuit_target = WEAKREF(new_target)
new_target.ominous_nosebleed()
if (isnull(pursuit_target))
return
var/turf/step_turf = get_step(src, get_dir(src, current_target))
if (!HAS_TRAIT(step_turf, TRAIT_CONTAINMENT_FIELD))
Move(step_turf)
/// Returns the closest conscious carbon on our z level or null if there somehow isn't one
/obj/effect/anomaly/bioscrambler/proc/find_nearest_target()
var/closest_distance = INFINITY
var/mob/living/carbon/closest_target = null
for(var/mob/living/carbon/target in GLOB.player_list)
if (target.z != z)
continue
if (target.status_effects & GODMODE)
continue
if (target.stat >= UNCONSCIOUS)
continue // Don't just haunt a corpse
var/distance_from_target = get_dist(src, target)
if(distance_from_target >= closest_distance)
continue
closest_distance = distance_from_target
closest_target = target
return closest_target
+10
View File
@@ -1477,3 +1477,13 @@
if (!IS_ORGANIC_LIMB(target_part) || (target_part.bodypart_flags & BODYPART_PSEUDOPART))
return FALSE
return ..()
/mob/living/carbon/ominous_nosebleed()
var/obj/item/bodypart/head = get_bodypart(BODY_ZONE_HEAD)
if(isnull(head))
return ..()
if(HAS_TRAIT(src, TRAIT_NOBLOOD))
to_chat(src, span_notice("You get a headache."))
return
head.adjustBleedStacks(5)
visible_message(span_notice("[src] gets a nosebleed."), span_warning("You get a nosebleed."))
+4
View File
@@ -2426,6 +2426,10 @@ GLOBAL_LIST_EMPTY(fire_appearances)
apply_damage(rand(5,10), BRUTE, BODY_ZONE_CHEST)
lattice.deconstruct(FALSE)
/// Prints an ominous message if something bad is going to happen to you
/mob/living/proc/ominous_nosebleed()
to_chat(src, span_warning("You feel a bit nauseous for just a moment."))
/**
* Proc used by different station pets such as Ian and Poly so that some of their data can persist between rounds.
* This base definition only contains a trait and comsig to stop memory from being (over)written.
@@ -29,6 +29,7 @@
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
/obj/machinery/field/containment/Destroy()
if(field_gen_1)
@@ -208,6 +208,7 @@ no power level overlay is currently in the overlays list.
air_update_turf(TRUE, FALSE)
INVOKE_ASYNC(src, PROC_REF(cleanup))
addtimer(CALLBACK(src, PROC_REF(cool_down)), 5 SECONDS)
RemoveElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
/obj/machinery/field/generator/proc/cool_down()
if(active || warming_up <= 0)
@@ -220,6 +221,7 @@ no power level overlay is currently in the overlays list.
/obj/machinery/field/generator/proc/turn_on()
active = FG_CHARGING
addtimer(CALLBACK(src, PROC_REF(warm_up)), 5 SECONDS)
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_CONTAINMENT_FIELD)))
/obj/machinery/field/generator/proc/warm_up()
if(!active)
@@ -386,16 +386,8 @@
/obj/singularity/proc/can_move(turf/considered_turf)
if(!considered_turf)
return FALSE
if((locate(/obj/machinery/field/containment) in considered_turf) || (locate(/obj/machinery/shieldwall) in considered_turf))
if (HAS_TRAIT(considered_turf, TRAIT_CONTAINMENT_FIELD))
return FALSE
else if(locate(/obj/machinery/field/generator) in considered_turf)
var/obj/machinery/field/generator/check_generator = locate(/obj/machinery/field/generator) in considered_turf
if(check_generator?.active)
return FALSE
else if(locate(/obj/machinery/power/shieldwallgen) in considered_turf)
var/obj/machinery/power/shieldwallgen/check_shield = locate(/obj/machinery/power/shieldwallgen) in considered_turf
if(check_shield?.active)
return FALSE
return TRUE
/obj/singularity/proc/event()