From bff56bf388e6003a0abe09e19262caafbfa84400 Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Sun, 15 Feb 2026 20:43:45 -0500 Subject: [PATCH] Jury-Rigged Anomaly Tech: Adds effects to pulsed anomaly cores (#95049) ## About The Pull Request Each type of anomaly core has an effect when pulsed by an assembly, button, or wire. Most of these effects are a weaker version of their respective reactive armor's unique effect (except for pyro and vortex anomalies, which make stealth armor solely because there isn't specific reactive armor for them). In particular: - Pyro anomalies make a 3x3 area of hotspots around themselves. - Gravity anomalies gently pull most unanchored objects in a 2 tile radius. - Flux anomalies make a short range 10 kJ tesla zap that usually doesn't propagate more than a single time. - Bluespace anomalies teleport the object they are connected to up to 4 tiles away. They will teleport out of storage, and they will teleport out of your hands or equipment slots unless they have NODROP. This has a 15 second cooldown, which is longer than reactive teleport armor. - Vortex anomalies do a weaker version of the the vortex thing, but limited to a 1 tile radius around the core. This has a 5 second cooldown, and can potentially destroy the core or what it's attached to. - Bioscrambler anomalies perform a bioscramble pulse in a 1 tile range. This has a 10 second cooldown. - Hallucination anomalies perform a hallucination pulse in a 1 tile range, adding 20 seconds of hallucinations, up to a maximum of 1 minute. This has a 10 second cooldown, so hallucinations from a single pulsed core can stack if you stay in range. - Dimensional anomalies perform a dimensional shift in the same range as the science relic. This has a fixed 15 second cooldown. - Ectoplasmic anomalies haunt a few objects in a 5x5 area around the core for 30 seconds. This has a 60 second cooldown. - Weather anomalies cause a single lightning bolt to strike a random open turf in a 5x5 area around the core. This bolt deals less damage than the reactive weather armor's bolts, and does not have an AOE. ## Why It's Good For The Game Gives both the station and antagonists additional uses for anomaly cores, beyond the ones that are specific items printed by science. As some examples: - A bundle of wired flux anomalies surrounded by tesla coils can provide a decent drip-feed of free power to the station - only on the level of several pacmans, but better than nothing. - Bluespace anomalies give you a manual version of the reactive teleport armor's teleport, but you'll need to get creative in order to actually teleport yourself and not just what you attached the core to. - People who really want to play the bioscrambler lottery can keep doing it after the original anomaly is neutralized. ## Changelog :cl: add: Anomaly cores now have effects when pulsed by an assembly, button, or wire. These effects are generally weaker versions of the effects of the source anomalies, or of their respective reactive armors, and many have a 50% longer cooldown than said armors. /:cl: --- code/__DEFINES/movement.dm | 16 +++ code/datums/helper_datums/teleport.dm | 47 ++++++++ .../effects/anomalies/anomalies_vortex.dm | 7 +- .../temporary_visuals/miscellaneous.dm | 8 ++ code/game/objects/items/devices/swapper.dm | 22 +--- code/modules/assembly/assembly.dm | 8 +- code/modules/research/anomaly/anomaly_core.dm | 114 ++++++++++++++++++ 7 files changed, 193 insertions(+), 29 deletions(-) diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index 35084b51aca..e07fe65552d 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -128,6 +128,22 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) /// Anything else #define TELEPORT_CHANNEL_FREE "free" +// Container flags for get_teleportable_container +/// Count mob inventory as a valid container +#define TELEPORT_CONTAINER_INCLUDE_INVENTORY (1<<0) +/// Count modsuits with at least one sealed part as valid containers, even if mob inventory is not a valid container +#define TELEPORT_CONTAINER_INCLUDE_SEALED_MODSUIT (1<<1) +/// Count atom storage as a valid container +#define TELEPORT_CONTAINER_INCLUDE_STORAGE (1<<2) +/// Count closets as a valid container +#define TELEPORT_CONTAINER_INCLUDE_CLOSET (1<<3) +/// Count vehicles as a valid container +#define TELEPORT_CONTAINER_INCLUDE_VEHICLE (1<<4) +/// Count mech equipment (particularly cargo holds and sleepers) as a valid container +#define TELEPORT_CONTAINER_INCLUDE_MECH_EQUIPMENT (1<<5) +/// Count stomachs as a valid container (ew) +#define TELEPORT_CONTAINER_INCLUDE_STOMACH (1<<6) + ///Return values for moveloop Move() #define MOVELOOP_FAILURE 0 #define MOVELOOP_SUCCESS 1 diff --git a/code/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm index 4d0b715d762..02e133b5718 100644 --- a/code/datums/helper_datums/teleport.dm +++ b/code/datums/helper_datums/teleport.dm @@ -267,3 +267,50 @@ return FALSE return TRUE + +//Gets the topmost teleportable container +/proc/get_teleportable_container(atom/movable/teleportable, container_flags = ALL) + while(ismovable(teleportable.loc)) + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_STORAGE) && isitem(teleportable)) + var/obj/item/item = teleportable + if(item.item_flags & IN_STORAGE) + break + var/atom/movable/movable = teleportable.loc + if(movable.anchored) + break + if(isliving(movable)) + var/mob/living/living = movable + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_INVENTORY)) + var/list/equipped = living.get_equipped_items(INCLUDE_HELD|INCLUDE_POCKETS) + if((teleportable in equipped) && !HAS_TRAIT(teleportable, TRAIT_NODROP)) + if(istype(teleportable, /obj/item/mod/control) && (container_flags & TELEPORT_CONTAINER_INCLUDE_SEALED_MODSUIT)) + var/obj/item/mod/control/modsuit = teleportable + var/sealed = TRUE + for(var/datum/mod_part/part as anything in modsuit.get_part_datums(TRUE)) + if((part.part_item == modsuit || part.part_item.loc != modsuit) && !part.sealed) + sealed = FALSE + break + if(!sealed) + break + else + break + if(living.buckled) + if(living.buckled.anchored) + break + else + var/obj/buckled_obj = living.buckled + buckled_obj.unbuckle_mob(living) + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_CLOSET) && iscloset(movable)) + break + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_MECH_EQUIPMENT) && istype(movable, /obj/item/mecha_parts/mecha_equipment)) + break + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_VEHICLE) && isvehicle(movable)) + var/obj/vehicle/vehicle = movable + if(vehicle.is_occupant(teleportable)) + break + if(!(container_flags & TELEPORT_CONTAINER_INCLUDE_STOMACH) && istype(movable, /obj/item/organ/stomach)) + var/obj/item/organ/stomach/stomach = movable + if(teleportable in stomach.stomach_contents) + break + teleportable = movable + return teleportable diff --git a/code/game/objects/effects/anomalies/anomalies_vortex.dm b/code/game/objects/effects/anomalies/anomalies_vortex.dm index 4c3a63ae5a6..1bb8f19b0b8 100644 --- a/code/game/objects/effects/anomalies/anomalies_vortex.dm +++ b/code/game/objects/effects/anomalies/anomalies_vortex.dm @@ -64,11 +64,6 @@ SSexplosions.lowturf += T /obj/effect/anomaly/bhole/detonate() - new /obj/effect/temp_visual/circle_wave/vortex(get_turf(src)) + new /obj/effect/temp_visual/circle_wave/vortex/small(get_turf(src)) playsound(src, 'sound/effects/hallucinations/far_noise.ogg', vol = 50) -/obj/effect/temp_visual/circle_wave/vortex - color = COLOR_BLACK - duration = 3 SECONDS - amount_to_scale = 4 - diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm index 55239756339..ec8c398f335 100644 --- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm +++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm @@ -808,6 +808,14 @@ /obj/effect/temp_visual/circle_wave/star_blast color = COLOR_VOID_PURPLE +/obj/effect/temp_visual/circle_wave/vortex + color = COLOR_BLACK + duration = 3 SECONDS + amount_to_scale = 4 + +/obj/effect/temp_visual/circle_wave/vortex/small + amount_to_scale = 2 + /obj/effect/temp_visual/focus_ring randomdir = FALSE name = "ring" diff --git a/code/game/objects/items/devices/swapper.dm b/code/game/objects/items/devices/swapper.dm index 2b6d7b5b651..a6ddb03dc0f 100644 --- a/code/game/objects/items/devices/swapper.dm +++ b/code/game/objects/items/devices/swapper.dm @@ -78,30 +78,12 @@ update_appearance() return CLICK_ACTION_SUCCESS -//Gets the topmost teleportable container -/obj/item/swapper/proc/get_teleportable_container() - var/atom/movable/teleportable = src - while(ismovable(teleportable.loc)) - var/atom/movable/AM = teleportable.loc - if(AM.anchored) - break - if(isliving(AM)) - var/mob/living/L = AM - if(L.buckled) - if(L.buckled.anchored) - break - else - var/obj/buckled_obj = L.buckled - buckled_obj.unbuckle_mob(L) - teleportable = AM - return teleportable - /obj/item/swapper/proc/swap(mob/user) if(QDELETED(linked_swapper) || isnull(linked_swapper.loc) || world.time < linked_swapper.cooldown) return - var/atom/movable/A = get_teleportable_container() - var/atom/movable/B = linked_swapper.get_teleportable_container() + var/atom/movable/A = get_teleportable_container(src) + var/atom/movable/B = get_teleportable_container(linked_swapper) var/target_A = A.drop_location() var/target_B = B.drop_location() diff --git a/code/modules/assembly/assembly.dm b/code/modules/assembly/assembly.dm index 4a8e1b9dbc0..f639d29e79f 100644 --- a/code/modules/assembly/assembly.dm +++ b/code/modules/assembly/assembly.dm @@ -25,7 +25,9 @@ var/obj/item/assembly_holder/holder = null var/assembly_behavior = ASSEMBLY_FUNCTIONAL_OUTPUT // how does the assembly behave with respect to what it's connected to var/datum/wires/connected = null - var/next_activate = 0 //When we're next allowed to activate - for spam control + COOLDOWN_DECLARE(next_activate) + /// Length of the cooldown between activations + var/activation_cooldown = 3 SECONDS /obj/item/assembly/Destroy() holder = null @@ -99,9 +101,9 @@ /// What the device does when turned on /obj/item/assembly/proc/activate(mob/activator) - if(QDELETED(src) || !secured || (next_activate > world.time)) + if(QDELETED(src) || !secured || !COOLDOWN_FINISHED(src, next_activate)) return FALSE - next_activate = world.time + 30 + COOLDOWN_START(src, next_activate, activation_cooldown) return TRUE /obj/item/assembly/proc/toggle_secure() diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm index 26674e5d2ce..2e1d59cd4be 100644 --- a/code/modules/research/anomaly/anomaly_core.dm +++ b/code/modules/research/anomaly/anomaly_core.dm @@ -8,6 +8,7 @@ righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi' resistance_flags = FIRE_PROOF custom_materials = null + assembly_behavior = ASSEMBLY_FUNCTIONAL_OUTPUT var/anomaly_type = /obj/effect/anomaly /obj/item/assembly/signaler/anomaly/receive_signal(datum/signal/signal) @@ -42,47 +43,131 @@ icon_state = "pyro_core" anomaly_type = /obj/effect/anomaly/pyro +/obj/item/assembly/signaler/anomaly/pyro/signal() + var/turf/our_turf = get_turf(src) + if(!our_turf) + return + playsound(our_turf, 'sound/effects/magic/fireball.ogg', 100, TRUE) + for(var/turf/turf as anything in RANGE_TURFS(1, our_turf)) + new /obj/effect/hotspot(turf) + /obj/item/assembly/signaler/anomaly/grav name = "\improper gravitational anomaly core" desc = "The neutralized core of a gravitational anomaly. It feels much heavier than it looks. It'd probably be valuable for research." icon_state = "grav_core" anomaly_type = /obj/effect/anomaly/grav +/obj/item/assembly/signaler/anomaly/grav/signal() + for(var/obj/object in orange(2, src)) + if(!object.anchored) + step_towards(object,src) + for(var/mob/living/living in orange(2, src)) + if(!living.mob_negates_gravity()) + step_towards(living,src) + /obj/item/assembly/signaler/anomaly/flux name = "\improper flux anomaly core" desc = "The neutralized core of a flux anomaly. Touching it makes your skin tingle. It'd probably be valuable for research." icon_state = "flux_core" anomaly_type = /obj/effect/anomaly/flux +/obj/item/assembly/signaler/anomaly/flux/signal() + tesla_zap(src, 0, 10 KILO JOULES, 5 KILO JOULES, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_GENERATES_POWER) + /obj/item/assembly/signaler/anomaly/bluespace name = "\improper bluespace anomaly core" desc = "The neutralized core of a bluespace anomaly. It keeps phasing in and out of view. It'd probably be valuable for research." icon_state = "anomaly_core" anomaly_type = /obj/effect/anomaly/bluespace + activation_cooldown = 15 SECONDS // Slightly longer than reactive teleport armor cooldown + +/obj/item/assembly/signaler/anomaly/bluespace/signal() + var/atom/movable/to_teleport = get_teleportable_container(src, container_flags = TELEPORT_CONTAINER_INCLUDE_SEALED_MODSUIT) + if(!to_teleport) + return + var/turf/teleportable_turf = get_turf(to_teleport) + playsound(teleportable_turf, 'sound/effects/magic/blink.ogg', 100, TRUE) + do_teleport(to_teleport, teleportable_turf, 4, channel = TELEPORT_CHANNEL_BLUESPACE) /obj/item/assembly/signaler/anomaly/vortex name = "\improper vortex anomaly core" desc = "The neutralized core of a vortex anomaly. It won't sit still, as if some invisible force is acting on it. It'd probably be valuable for research." icon_state = "vortex_core" anomaly_type = /obj/effect/anomaly/bhole + activation_cooldown = 5 SECONDS + var/turf_contents_pull_chance = 33 + var/turf_destroy_chance = 25 + var/max_explosion_force = EXPLODE_HEAVY + +// Causes a small vortex pulse +/obj/item/assembly/signaler/anomaly/vortex/signal() + var/turf/our_turf = get_turf(src) + if(!our_turf) + return + playsound(our_turf, 'sound/effects/bamf.ogg', 100, TRUE) + for(var/turf/turf as anything in RANGE_TURFS(1, our_turf)) + var/explosion_power = rand(EXPLODE_NONE, max_explosion_force) + if(prob(turf_contents_pull_chance)) + for(var/obj/object in turf.contents) + if(object.anchored) + switch(explosion_power) + if(EXPLODE_DEVASTATE) + SSexplosions.high_mov_atom += object + if(EXPLODE_HEAVY) + SSexplosions.med_mov_atom += object + if(EXPLODE_LIGHT) + SSexplosions.low_mov_atom += object + else + step_towards(object, src) + for(var/mob/living/living in turf.contents) + step_towards(living, src) + if(prob(turf_destroy_chance)) + switch(explosion_power) + if(EXPLODE_DEVASTATE) + SSexplosions.highturf += turf + if(EXPLODE_HEAVY) + SSexplosions.medturf += turf + if(EXPLODE_LIGHT) + SSexplosions.lowturf += turf + new /obj/effect/temp_visual/circle_wave/vortex(our_turf) /obj/item/assembly/signaler/anomaly/bioscrambler name = "\improper bioscrambler anomaly core" desc = "The neutralized core of a bioscrambler anomaly. It's squirming, as if moving. It'd probably be valuable for research." icon_state = "bioscrambler_core" anomaly_type = /obj/effect/anomaly/bioscrambler + activation_cooldown = 10 SECONDS + +/obj/item/assembly/signaler/anomaly/bioscrambler/signal() + new /obj/effect/temp_visual/circle_wave/bioscrambler(get_turf(src)) + for(var/mob/living/carbon/nearby in hearers(1, src)) + nearby.bioscramble(name) /obj/item/assembly/signaler/anomaly/hallucination name = "\improper hallucination anomaly core" desc = "The neutralized core of a hallucination anomaly. It seems to be moving, but it's probably your imagination. It'd probably be valuable for research." icon_state = "hallucination_core" anomaly_type = /obj/effect/anomaly/hallucination + activation_cooldown = 10 SECONDS + +/obj/item/assembly/signaler/anomaly/hallucination/signal() + visible_hallucination_pulse(get_turf(src), 2, 20 SECONDS, 1 MINUTES) /obj/item/assembly/signaler/anomaly/dimensional name = "\improper dimensional anomaly core" desc = "The neutralized core of a dimensional anomaly. Objects reflected on its surface don't look quite right. It'd probably be valuable for research." icon_state = "dimensional_core" anomaly_type = /obj/effect/anomaly/dimensional + activation_cooldown = 15 SECONDS // A bit longer than reactive barricade armor cooldown + +/obj/item/assembly/signaler/anomaly/dimensional/signal() + var/turf/our_turf = get_turf(src) + if(!our_turf) + return + var/new_theme_path = pick(subtypesof(/datum/dimension_theme)) + var/datum/dimension_theme/theme = SSmaterials.dimensional_themes[new_theme_path] + for(var/turf/turf as anything in RANGE_TURFS(1, our_turf)) + theme.apply_theme(turf, show_effect = TRUE) /obj/item/assembly/signaler/anomaly/dimensional/Initialize(mapload) . = ..() @@ -94,6 +179,10 @@ desc = "The neutralized core of an ectoplasmic anomaly. When you hold it close, you can hear faint murmuring from inside. It'd probably be valuable for research." icon_state = "dimensional_core" anomaly_type = /obj/effect/anomaly/ectoplasm + activation_cooldown = 60 SECONDS // A bit longer than reactive posession armor cooldown + +/obj/item/assembly/signaler/anomaly/ectoplasm/signal() + haunt_outburst(get_turf(src), 2, 33, 30 SECONDS) /obj/item/assembly/signaler/anomaly/weather name = "\improper weather anomaly core" @@ -103,3 +192,28 @@ /// Used in weather towers to track uses before depleting var/charges = 8 + +/obj/item/assembly/signaler/anomaly/weather/signal() + var/turf/our_turf = get_turf(src) + if(!our_turf) + return + var/list/possible_targets = list() + for(var/turf/turf as anything in RANGE_TURFS(2, our_turf)) + if(isopenturf(turf)) + possible_targets += turf + if(!length(possible_targets)) + return + var/turf/target = pick(possible_targets) + new /obj/effect/temp_visual/telegraphing/thunderbolt(target) + addtimer(CALLBACK(src, PROC_REF(strike), target), 1 SECONDS) + +/obj/item/assembly/signaler/anomaly/weather/proc/strike(turf/target) + playsound(target, 'sound/effects/magic/lightningbolt.ogg', 66, TRUE) + new /obj/effect/temp_visual/thunderbolt(target) + + for(var/mob/living/hit_mob in target) + to_chat(hit_mob, span_userdanger("You've been struck by lightning!")) + hit_mob.electrocute_act(20, src, flags = SHOCK_TESLA|SHOCK_NOSTUN) + + for(var/obj/hit_thing in target) + hit_thing.take_damage(15, BURN, ENERGY, FALSE)