Skip to content

Commit

Permalink
[MIRROR] Bioscrambler Anomaly chases you (#2889)
Browse files Browse the repository at this point in the history
* 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

:cl:
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.
/:cl:

* Bioscrambler Anomaly chases you

* Adds some limitations to the bioscrambler event (code courtesy of LT3)

---------

Co-authored-by: NovaBot <[email protected]>
Co-authored-by: Jacquerel <[email protected]>
Co-authored-by: Mal <[email protected]>
  • Loading branch information
4 people authored Apr 15, 2024
1 parent a9a21fc commit 7eccdef
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 37 deletions.
3 changes: 3 additions & 0 deletions code/__DEFINES/research/anomalies.dm
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,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,
/obj/item/organ/internal/empowered_borer_egg, // NOVA EDIT ADDITION
Expand Down
3 changes: 3 additions & 0 deletions code/__DEFINES/traits/declarations.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions code/_globalvars/traits/_traits.dm
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,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,
Expand Down
21 changes: 3 additions & 18 deletions code/datums/components/singularity.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
24 changes: 17 additions & 7 deletions code/game/machinery/shieldgen.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -463,7 +460,6 @@
balloon_alert(user, "malfunctioning!")
else
balloon_alert(user, "no access!")

return

add_fingerprint(user)
Expand Down Expand Up @@ -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)

Expand All @@ -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"
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions code/game/objects/effects/anomalies/_anomalies.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
. = ..()
Expand Down Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion code/game/objects/effects/anomalies/anomalies_bioscrambler.dm
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,63 @@
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
var/pulse_delay = 15 SECONDS
/// 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 changes: 10 additions & 0 deletions code/modules/mob/living/carbon/carbon.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1501,3 +1501,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 changes: 4 additions & 0 deletions code/modules/mob/living/living.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2508,6 +2508,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.
Expand Down
1 change: 1 addition & 0 deletions code/modules/power/singularity/containment_field.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions code/modules/power/singularity/field_generator.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
10 changes: 1 addition & 9 deletions code/modules/power/singularity/singularity.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
60 changes: 60 additions & 0 deletions modular_nova/modules/ices_events/code/events/ev_bioscrambler.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* A modification to the standard bioscrambler anomaly event. The anomaly cannot pass through walls,
* and the spawn is restricted to common areas that engineering has rapid access.
*/

/obj/effect/anomaly/bioscrambler
pass_flags = PASSTABLE | PASSGLASS | PASSMACHINE | PASSDOORS
range = 4
pulse_delay = 20 SECONDS

/datum/round_event/anomaly/anomaly_bioscrambler/setup()
if(spawn_location)
impact_area = get_area(spawn_location)
else
impact_area = placer.find_bioscrambler_area()

/datum/round_event/anomaly/anomaly_bioscrambler/announce(fake)
if(isnull(impact_area))
impact_area = placer.find_bioscrambler_area()
priority_announce("Biologic limb swapping agent detected on [ANOMALY_ANNOUNCE_MEDIUM_TEXT] [impact_area.name]. Engineers are advised to set up containment fields to prevent movement. Wear biosuits or other protective gear to counter the effects. Calculated half-life of %9£$T$%F3 years.", "Anomaly Alert", ANNOUNCER_ANOMALIES)

/**
* Returns an area which is safe to place a bioscrambler anomaly.
*/
/datum/anomaly_placer/proc/find_bioscrambler_area()
var/static/list/bioscrambler_inclusions = typecacheof(list(
/area/station/commons,
/area/station/hallway/primary,
/area/station/hallway/secondary,
))

//Subtypes from the above that shouldn't be included.
var/static/list/bioscrambler_exclusions = typecacheof(list(
/area/station/commons/dorms,
/area/station/hallway/secondary/command,
/area/station/hallway/secondary/construction,
/area/station/hallway/secondary/dock,
/area/station/hallway/secondary/exit/escape_pod,
/area/station/hallway/secondary/recreation,
/area/station/hallway/secondary/service,
/area/station/hallway/secondary/spacebridge,
/area/station/commons/storage,
/area/station/commons/toilet,
/area/station/commons/vacant_room,
))

var/static/list/bioscrambler_areas = bioscrambler_inclusions - bioscrambler_exclusions

log_game("ICES: Anomaly: Bioscrambler: [length(bioscrambler_inclusions)] areas cached for selection")
var/list/possible_areas = typecache_filter_list(GLOB.areas, bioscrambler_areas)
if(!length(possible_areas))
CRASH("No valid areas for anomaly found.")

var/area/landing_area = pick(possible_areas)
log_game("ICES: Anomaly: Bioscrambler: [landing_area.name] selected for spawn")
var/list/turf_test = get_area_turfs(landing_area)
if(!turf_test.len)
CRASH("Anomaly : No valid turfs found for [landing_area] - [landing_area.type]")

return landing_area
1 change: 1 addition & 0 deletions tgstation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -7453,6 +7453,7 @@
#include "modular_nova\modules\ices_events\code\ICES_seclevel.dm"
#include "modular_nova\modules\ices_events\code\ICES_tgui.dm"
#include "modular_nova\modules\ices_events\code\effects\ef_foam.dm"
#include "modular_nova\modules\ices_events\code\events\ev_bioscrambler.dm"
#include "modular_nova\modules\ices_events\code\events\ev_meteors.dm"
#include "modular_nova\modules\ices_events\code\events\ev_roleplay_check.dm"
#include "modular_nova\modules\ices_events\code\events\ev_scrubbers.dm"
Expand Down

0 comments on commit 7eccdef

Please sign in to comment.