diff --git a/code/__defines/anomaly.dm b/code/__defines/anomaly.dm
index ef3db08b37..03ccb13e0f 100644
--- a/code/__defines/anomaly.dm
+++ b/code/__defines/anomaly.dm
@@ -47,3 +47,19 @@
#define DIMENSIONAL_ANOMALY "dimensional_anomaly"
#define WEATHER_ANOMALY "weather_anomaly"
#define DUST_ANOMALY "dust_anomaly"
+
+#define ANOMALY_PARTICLE_SIGMA "Sigma particles"
+#define ANOMALY_PARTICLE_DELTA "Delta particles"
+#define ANOMALY_PARTICLE_ZETA "Zeta particles"
+#define ANOMALY_PARTICLE_EPSILON "Epsilon particles"
+
+#define ANOMALY_PARTICLE_ALL list( \
+ ANOMALY_PARTICLE_SIGMA, \
+ ANOMALY_PARTICLE_DELTA, \
+ ANOMALY_PARTICLE_ZETA, \
+ ANOMALY_PARTICLE_EPSILON \
+)
+
+#define ANOMALY_DECAYING "Decaying"
+#define ANOMALY_STABLE "Stable"
+#define ANOMALY_GROWING "Growing"
diff --git a/code/__defines/research.dm b/code/__defines/research.dm
index 15aee3327d..778afa580f 100644
--- a/code/__defines/research.dm
+++ b/code/__defines/research.dm
@@ -35,7 +35,7 @@
#define TECHWEB_TIER_5_POINTS 200
//! Amount of points gained per second by a single R&D server, see: [research][code/controllers/subsystem/research.dm]
-#define TECHWEB_SINGLE_SERVER_INCOME 1
+#define TECHWEB_SINGLE_SERVER_INCOME 0.2
//! Swab cell line types
#define CELL_LINE_TABLE_SLUDGE "cell_line_sludge_table"
diff --git a/code/__defines/research/techweb_nodes.dm b/code/__defines/research/techweb_nodes.dm
index 186d476878..c596ec298f 100644
--- a/code/__defines/research/techweb_nodes.dm
+++ b/code/__defines/research/techweb_nodes.dm
@@ -4,6 +4,8 @@
#define TECHWEB_NODE_ALIEN_SURGERY "alien_surgery"
#define TECHWEB_NODE_ALIENTECH "alientech"
#define TECHWEB_NODE_ANOMALY_RESEARCH "anomaly_research"
+#define TECHWEB_NODE_ANOMALY_HARVESTING "anomaly_harvesting"
+#define TECHWEB_NODE_APPLIED_ANOMALY_HARVESTING "applied_anomaly_harvesting"
#define TECHWEB_NODE_ANOMALY_SHELLS "anomaly_shells"
#define TECHWEB_NODE_TELEKINETIC_RESEARCH "telekinetic_research"
#define TECHWEB_NODE_APPLIED_BLUESPACE "applied_bluespace"
@@ -144,6 +146,7 @@
#define TECHWEB_NODE_TACKLE_ADVANCED "tackle_advanced"
#define TECHWEB_NODE_TELECOMS "telecoms"
#define TECHWEB_NODE_UNREGULATED_BLUESPACE "unregulated_bluespace"
+#define TECHWEB_NODE_XENOARCHEOLOGY "xenoarcheology"
#define TECHWEB_NODE_XENOBIOLOGY "xenobiology"
#define TECHWEB_NODE_TELEPORTER "teleporter"
#define TECHWEB_NODE_CONFETTI "confetti"
diff --git a/code/_helpers/global_lists_vr.dm b/code/_helpers/global_lists_vr.dm
index 9484f44fe6..967187ebe6 100644
--- a/code/_helpers/global_lists_vr.dm
+++ b/code/_helpers/global_lists_vr.dm
@@ -1280,7 +1280,9 @@ GLOBAL_LIST_INIT(area_or_turf_fail_types, typecacheof(list(
/obj/item/disposable_teleporter/slime, \
/obj/item/slimepotion, \
/obj/item/slime_extract, \
- /obj/item/reagent_containers/food/snacks/monkeycube
+ /obj/item/reagent_containers/food/snacks/monkeycube, \
+ /obj/item/anomaly_releaser, \
+ /obj/item/research_sample
#define CIRCUIT_GRIPPER \
/obj/item/cell/device, \
diff --git a/code/datums/outfits/jobs/science.dm b/code/datums/outfits/jobs/science.dm
index 9552e84bdb..5311de6b03 100644
--- a/code/datums/outfits/jobs/science.dm
+++ b/code/datums/outfits/jobs/science.dm
@@ -50,3 +50,8 @@
backpack = /obj/item/storage/backpack
satchel_one = /obj/item/storage/backpack/satchel/norm
suit = /obj/item/clothing/suit/storage/toggle/labcoat/roboticist
+
+/datum/decl/hierarchy/outfit/job/science/scientist/anomalist
+ name = OUTFIT_JOB_NAME(JOB_ALT_ANOMALIST)
+ l_pocket = /obj/item/anomaly_scanner
+ r_pocket = /obj/item/anomaly_releaser/science
diff --git a/code/game/jobs/job/science.dm b/code/game/jobs/job/science.dm
index 7271181aa1..7c1fbe459f 100644
--- a/code/game/jobs/job/science.dm
+++ b/code/game/jobs/job/science.dm
@@ -103,6 +103,7 @@
title = JOB_ALT_ANOMALIST
title_blurb = "An " + JOB_ALT_ANOMALIST + " is a " + JOB_SCIENTIST + " whose expertise is analyzing alien artifacts. They are familar with the most common methods of testing artifact \
function. They work closely with " + JOB_ALT_XENOARCHAEOLOGIST + "s, or Miners, if either role is present."
+ title_outfit = /datum/decl/hierarchy/outfit/job/science/scientist/anomalist
/datum/alt_title/phoron_research
title = JOB_ALT_PHORON_RESEARCHER
diff --git a/code/game/objects/effects/anomalies/_anomalies.dm b/code/game/objects/effects/anomalies/_anomalies.dm
index 5afd62f428..9d882cb189 100644
--- a/code/game/objects/effects/anomalies/_anomalies.dm
+++ b/code/game/objects/effects/anomalies/_anomalies.dm
@@ -19,6 +19,10 @@
var/immortal = FALSE
var/move_chance = ANOMALY_MOVECHANCE
+ // Anomaly harvesting stuff
+ var/datum/anomaly_stats/stats
+ var/danger_mult = 1
+
/obj/effect/anomaly/Initialize(mapload, new_lifespan, drops_core = TRUE)
. = ..()
@@ -51,6 +55,7 @@
/obj/effect/anomaly/process(seconds_per_tick)
anomalyEffect(seconds_per_tick)
+ anomalyPulse()
if(death_time < world.time && !immortal)
if(loc)
detonate()
@@ -60,12 +65,27 @@
STOP_PROCESSING(SSobj, src)
QDEL_NULL(countdown)
QDEL_NULL(anomaly_core)
+ if(stats)
+ QDEL_NULL(stats)
return ..()
/obj/effect/anomaly/proc/anomalyEffect(seconds_per_tick)
- if(prob(move_chance))
+ if(prob(move_chance) && !locate(/obj/effect/suspension_field) in get_turf(src))
move_anomaly()
+// Used in anomaly harvesting - Normal anomalies shouldn't pulse
+/obj/effect/anomaly/proc/anomalyPulse()
+ if(!stats)
+ return FALSE
+ if(world.time < stats.next_activation)
+ return FALSE
+
+ stats.pulse_effect()
+ if(QDELETED(src))
+ return FALSE
+ stats.next_activation = world.time + rand(stats.min_activation, stats.max_activation)
+ return TRUE
+
/obj/effect/anomaly/proc/move_anomaly()
step(src, pick(GLOB.alldirs))
@@ -85,21 +105,56 @@
anomaly_core = null
qdel(src)
-/obj/effect/anomaly/proc/stabilize(anchor = FALSE, has_core = TRUE)
+/obj/effect/anomaly/proc/stabilize(anchor = FALSE, has_core = TRUE, add_stats = FALSE)
immortal = TRUE
name = (has_core ? "stable " : "hollow ") + name
if(!has_core)
QDEL_NULL(anomaly_core)
if(anchor)
move_chance = 0
+ if(!stats && add_stats)
+ stats = new /datum/anomaly_stats
+ stats.attached_anomaly = WEAKREF(src)
+ stats.calculate_points()
+ density = TRUE
+ return
/obj/effect/anomaly/attackby(obj/item/I, mob/user)
if(istype(I, /obj/item/analyzer))
if(anomaly_core)
to_chat(user, span_notice("Analyzing... [src]'s stabilized field is fluctuating along frequency [format_frequency(anomaly_core.frequency)], code [anomaly_core.code]."))
return TRUE
+ if(istype(I, /obj/item/anomaly_scanner))
+ var/obj/item/anomaly_scanner/scanner = I
+ if(stats)
+ if(!do_after(user, 1 SECOND, src))
+ return
+ scanner.buffered_anomaly = WEAKREF(src)
+ scanner.tgui_interact(user)
+ return TRUE
return ..()
+/obj/effect/anomaly/bullet_act(obj/item/projectile/proj)
+ if(stats && istype(stats.modifier, /datum/anomaly_modifiers/reflective) && prob(stats.severity/1.5))
+ balloon_alert_visible("reflected!")
+
+ var/new_x = proj.x = pick(0, 0, 0, -1, 1, -2, 2)
+ var/new_y = proj.y = pick(0, 0, 0, -1, 1, -2, 2)
+
+ var/turf/curloc = get_step(proj, get_dir(proj, proj.starting))
+
+ proj.penetrating += 1
+
+ proj.redirect(new_x, new_y, curloc, null)
+ return FALSE
+
+ if(istype(proj, /obj/item/projectile/energy/anomaly))
+ var/obj/item/projectile/energy/anomaly/anom_proj = proj
+ if(stats)
+ stats.particle_hit(anom_proj.particle_type)
+
+ return FALSE
+
/proc/generate_anomaly(turf/anomalycenter, type = FLUX_ANOMALY, anomalyrange = 5, has_changed_lifespan = TRUE, drops_core = TRUE)
var/turf/local_turf = pick(RANGE_TURFS(anomalyrange, anomalycenter))
if(!local_turf)
diff --git a/code/game/objects/effects/anomalies/anomalies_bioscrambler.dm b/code/game/objects/effects/anomalies/anomalies_bioscrambler.dm
index 2a64422a5d..9c5562ca0f 100644
--- a/code/game/objects/effects/anomalies/anomalies_bioscrambler.dm
+++ b/code/game/objects/effects/anomalies/anomalies_bioscrambler.dm
@@ -5,6 +5,7 @@
pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE
layer = ABOVE_MOB_LAYER
lifespan = ANOMALY_COUNTDOWN_TIMER * 2
+ danger_mult = 1.1
/// Who are we moving towards?
var/datum/weakref/pursuit_target
@@ -21,6 +22,8 @@
/obj/effect/anomaly/bioscrambler/anomalyEffect(seconds_per_tick)
. = ..()
+ if(stats)
+ return
if(!COOLDOWN_FINISHED(src, pulse_cooldown))
return
@@ -87,3 +90,15 @@
/obj/effect/anomaly/bioscrambler/detonate()
COOLDOWN_RESET(src, pulse_cooldown)
anomalyEffect()
+
+/obj/effect/anomaly/bioscrambler/anomalyPulse()
+ if(!..())
+ return
+
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ else
+ anomalyEffect()
diff --git a/code/game/objects/effects/anomalies/anomalies_bluespace.dm b/code/game/objects/effects/anomalies/anomalies_bluespace.dm
index a079eb15ce..b2379c52e9 100644
--- a/code/game/objects/effects/anomalies/anomalies_bluespace.dm
+++ b/code/game/objects/effects/anomalies/anomalies_bluespace.dm
@@ -7,6 +7,7 @@
var/teleport_range = 1
///Distance we can teleport someone passively
var/teleport_distance = 4
+ danger_mult = 1.1
/obj/effect/anomaly/bluespace/Initialize(mapload, new_lifespan)
. = ..()
@@ -92,3 +93,43 @@
color = COLOR_BLUE_LIGHT
duration = 1 SECONDS
amount_to_scale = 5
+
+/obj/effect/anomaly/bluespace/anomalyPulse()
+ if(!..())
+ return
+
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ if(16 to 33)
+ pulse_teleport(4, 1)
+ if(34 to 65)
+ pulse_teleport(5, 2)
+ else
+ pulse_teleport(6, 3)
+
+/obj/effect/anomaly/bluespace/proc/pulse_teleport(range, count)
+ var/list/possible = list()
+
+ for(var/obj/item/radio/beacon in GLOB.all_beacons)
+ var/turf/turf = get_turf(beacon)
+ if(!turf)
+ continue
+ if(!check_teleport_valid(src, turf))
+ continue
+ possible += beacon
+
+ var/chosen = pick(possible)
+
+ var/list/things = list()
+ for(var/atom/movable/thing in orange(range, get_turf(src)))
+ if(istype(thing, /obj/item/radio/beacon) || iseffect(thing) || isEye(thing))
+ continue
+ if(thing.anchored)
+ continue
+ things += thing
+
+ for(var/i in 1 to count)
+ do_teleport(pick(things), get_turf(chosen))
diff --git a/code/game/objects/effects/anomalies/anomalies_dimensional.dm b/code/game/objects/effects/anomalies/anomalies_dimensional.dm
index be5dcc94f7..9d50def69c 100644
--- a/code/game/objects/effects/anomalies/anomalies_dimensional.dm
+++ b/code/game/objects/effects/anomalies/anomalies_dimensional.dm
@@ -4,6 +4,7 @@
anomaly_core = /obj/item/assembly/signaler/anomaly/dimensional
lifespan = ANOMALY_COUNTDOWN_TIMER * 20
move_chance = 0
+ danger_mult = 0.8
/// Range of effect, if left alone anomaly will convert a 2(range)+1 squared area.
var/range = 3
/// List of turfs this anomaly will try to transform before relocating
@@ -40,6 +41,8 @@
/obj/effect/anomaly/dimensional/proc/transmute_area()
if(!theme)
prepare_area()
+ if(stats)
+ return
if(!target_turfs.len)
if(teleports_left <= 0 && !immortal)
detonate()
@@ -88,3 +91,26 @@
icon = 'icons/effects/effects.dmi'
icon_state = "shieldsparkles"
duration = 3
+
+/obj/effect/anomaly/dimensional/anomalyPulse()
+ if(!..())
+ return
+
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ if(16 to 33)
+ pulse_change(2)
+ if(34 to 65)
+ pulse_change(4)
+ else
+ pulse_change(8)
+
+/obj/effect/anomaly/dimensional/proc/pulse_change(count)
+ var/turf/turf
+
+ for(var/i in 1 to count)
+ turf = pick(target_turfs)
+ theme.apply_theme(turf, show_effect = TRUE)
diff --git a/code/game/objects/effects/anomalies/anomalies_dust.dm b/code/game/objects/effects/anomalies/anomalies_dust.dm
index 9997e15ba3..5e89063582 100644
--- a/code/game/objects/effects/anomalies/anomalies_dust.dm
+++ b/code/game/objects/effects/anomalies/anomalies_dust.dm
@@ -6,6 +6,7 @@
pass_flags = PASSTABLE | PASSGRILLE
layer = ABOVE_MOB_LAYER
lifespan = ANOMALY_COUNTDOWN_TIMER * 1.5
+ danger_mult = 0.75
move_chance = 80
@@ -71,3 +72,15 @@
/obj/effect/anomaly/dust/detonate()
COOLDOWN_RESET(src, pulse_cooldown)
anomalyEffect()
+
+/obj/effect/anomaly/dust/anomalyPulse()
+ if(!..())
+ return
+
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ else
+ anomalyEffect()
diff --git a/code/game/objects/effects/anomalies/anomalies_flux.dm b/code/game/objects/effects/anomalies/anomalies_flux.dm
index 97aa3dc13d..fc9a63c417 100644
--- a/code/game/objects/effects/anomalies/anomalies_flux.dm
+++ b/code/game/objects/effects/anomalies/anomalies_flux.dm
@@ -61,3 +61,24 @@
/obj/effect/anomaly/flux/minor/Initialize(mapload, new_lifespan, emp_zap = FLUX_NO_EMP)
return ..()
+
+/obj/effect/anomaly/flux/anomalyPulse()
+ if(!..())
+ return
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ if(16 to 33)
+ tesla_zap(src, 2, 1000, FALSE, FALSE)
+ if(34 to 65)
+ tesla_zap(src, 3, 1000, FALSE, FALSE)
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(tesla_zap), src, 3, 1500, FALSE, FALSE), 3 SECONDS)
+ else
+ tesla_zap(src, 4, 1000, FALSE, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(highSevPulse)), 3 SECONDS)
+
+/obj/effect/anomaly/flux/proc/highSevPulse()
+ tesla_zap(src, 4, 1250, FALSE, FALSE)
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(tesla_zap), src, 4, 1500, FALSE, FALSE), 3 SECONDS)
diff --git a/code/game/objects/effects/anomalies/anomalies_gravity.dm b/code/game/objects/effects/anomalies/anomalies_gravity.dm
index bb80156112..519c03f4b4 100644
--- a/code/game/objects/effects/anomalies/anomalies_gravity.dm
+++ b/code/game/objects/effects/anomalies/anomalies_gravity.dm
@@ -16,6 +16,8 @@
/obj/effect/anomaly/grav/anomalyEffect(seconds_per_tick)
..()
+ if(stats)
+ return
boing = TRUE
for(var/obj/O in orange(4, src))
if(!O.anchored)
@@ -93,3 +95,14 @@
*/
/obj/effect/temp_visual/circle_wave/gravity
color = COLOR_NAVY
+
+/obj/effect/anomaly/grav/anomalyPulse()
+ if(!..())
+ return
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ else
+ anomalyEffect()
diff --git a/code/game/objects/effects/anomalies/anomalies_hallucination.dm b/code/game/objects/effects/anomalies/anomalies_hallucination.dm
index 7390896a38..9cfe28a14e 100644
--- a/code/game/objects/effects/anomalies/anomalies_hallucination.dm
+++ b/code/game/objects/effects/anomalies/anomalies_hallucination.dm
@@ -2,6 +2,7 @@
name = "hallucination anomaly"
icon_state = "hallucination"
anomaly_core = /obj/item/assembly/signaler/anomaly/hallucination
+ danger_mult = 0.9
/// Time passed since the last effect, increased by seconds_per_tick of the SSobj
var/ticks = 0
/// How many seconds between each small hallucination pulses
@@ -23,6 +24,8 @@
/obj/effect/anomaly/hallucination/anomalyEffect(seconds_per_tick)
. = ..()
+ if(stats)
+ return
ticks += seconds_per_tick
if(ticks < release_delay)
return
@@ -109,3 +112,14 @@
///Subtype for the SM that doesn't spawn decoys, because otherwise the whole area gets flooded with dummies.
/obj/effect/anomaly/hallucination/supermatter
spawn_decoys = FALSE
+
+/obj/effect/anomaly/hallucination/anomalyPulse()
+ if(!..())
+ return
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ else
+ anomalyEffect()
diff --git a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm
index bac080489c..576fd154c0 100644
--- a/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm
+++ b/code/game/objects/effects/anomalies/anomalies_pyroclastic.dm
@@ -1,6 +1,7 @@
/obj/effect/anomaly/pyro
name = "pyroclastic anomaly"
icon_state = "pyroclastic"
+ danger_mult = 1.3
var/ticks = 0
/// How many seconds between each gas release
var/releasedelay = 15 SECONDS
@@ -12,23 +13,40 @@
/obj/effect/anomaly/pyro/anomalyEffect(seconds_per_tick)
..()
+ if(stats)
+ return
ticks += seconds_per_tick
if(ticks < releasedelay)
return FALSE
else
ticks -= releasedelay
- var/turf/simulated/floor/tile = get_turf(src)
- if(istype(tile))
- tile.assume_gas(GAS_PHORON, 10, T20C)
- tile.hotspot_expose(700, 400)
+ burst()
return TRUE
-/obj/effect/anomaly/pyro/detonate()
+/obj/effect/anomaly/pyro/proc/burst(moles = 10, temperature = 700, volume = 400)
var/turf/simulated/floor/tile = get_turf(src)
if(istype(tile))
- tile.assume_gas(GAS_PHORON, 10, T20C)
- tile.hotspot_expose(700, 400)
+ tile.assume_gas(GAS_PHORON, moles, T20C)
+ tile.hotspot_expose(temperature, volume)
+
+/obj/effect/anomaly/pyro/detonate()
+ burst()
var/new_colour = pick(/mob/living/simple_mob/slime/xenobio/red, /mob/living/simple_mob/slime/xenobio/orange)
- var/mob/living/simple_mob/slime/xenobio/pyro = new new_colour(tile)
+ var/mob/living/simple_mob/slime/xenobio/pyro = new new_colour(get_turf(src))
pyro.enrage()
+
+/obj/effect/anomaly/pyro/anomalyPulse()
+ if(!..())
+ return
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ if(16 to 33)
+ burst()
+ if(34 to 65)
+ burst(20, 900, 600)
+ else
+ burst(30, 110, 800)
diff --git a/code/game/objects/effects/anomalies/anomalies_weather.dm b/code/game/objects/effects/anomalies/anomalies_weather.dm
index baa4366de2..ca21a910c3 100644
--- a/code/game/objects/effects/anomalies/anomalies_weather.dm
+++ b/code/game/objects/effects/anomalies/anomalies_weather.dm
@@ -44,6 +44,12 @@
addtimer(CALLBACK(src, PROC_REF(start_weather)), telegraph, TIMER_DELETE_ME)
+/obj/effect/anomaly/weather/proc/add_turfs(list/turf/to_add)
+ for(var/turf/turf in to_add)
+ if(isspace(turf))
+ continue
+ affected_turfs.Add(turf)
+
/obj/effect/anomaly/weather/proc/find_adjacent_impacted_area(check_dir)
var/limit = 10
var/turf/next_turf = get_step(src, check_dir)
@@ -73,6 +79,9 @@
turf = GetBelow(turf)
selected_weather.affect_turf(turf)
+ if(stats)
+ return
+
for(var/mob/mob as anything in GLOB.player_list)
if(get_area(mob) in affected_areas)
selected_weather.hear_sounds(mob, TRUE)
@@ -104,13 +113,13 @@
if(selected_weather.sounds)
selected_weather.loop_sounds.stop()
+ for(var/turf/turf in affected_turfs)
+ selected_weather.remove_from_turf(turf)
+
for(var/area in affected_areas)
for(var/mob/mob in area)
selected_weather.hear_sounds(mob, FALSE)
- for(var/turf/turf in affected_turfs)
- selected_weather.remove_from_turf(turf)
-
/obj/effect/anomaly/weather/Destroy()
clear_weather()
. = ..()
@@ -118,6 +127,47 @@
/obj/effect/anomaly/weather/proc/update_reagent(reagent)
selected_weather.update_reagent(reagent)
+/obj/effect/anomaly/weather/anomalyPulse()
+ if(!..())
+ return
+ switch(stats.severity)
+ if(0 to 15)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ affected_areas.Cut()
+ affected_turfs.Cut()
+ if(16 to 33)
+ clear_weather()
+ affected_turfs.Cut()
+ if(!istype(selected_weather, /datum/anomalous_weather/rain))
+ selected_weather = new /datum/anomalous_weather/rain
+ update_reagent(REAGENT_ID_WATER)
+ add_turfs(circleviewturfs(src, 3))
+ start_weather()
+ if(34 to 65)
+ clear_weather()
+ affected_turfs.Cut()
+ if(!istype(selected_weather, /datum/anomalous_weather/rain))
+ selected_weather = new /datum/anomalous_weather/rain
+ update_reagent(pick(REAGENT_ID_WATER, REAGENT_ID_ICE, REAGENT_ID_ORANGEJUICE))
+ add_turfs(circlerangeturfs(src, 4))
+ start_weather()
+ else
+ clear_weather()
+ affected_turfs.Cut()
+ if(!istype(selected_weather, /datum/anomalous_weather/rain/storm))
+ selected_weather = new /datum/anomalous_weather/rain/storm
+
+ var/reagent_id = pick(SSchemistry.chemical_reagents)
+ if(reagent_id in GLOB.obtainable_chemical_blacklist)
+ update_reagent(REAGENT_ID_WATER) // You get WATER.
+ else
+ update_reagent(reagent_id)
+
+ add_turfs(circlerangeturfs(src, 5))
+ start_weather()
+
/obj/effect/anomaly/weather/rain
selected_weather = /datum/anomalous_weather/rain
diff --git a/code/game/objects/effects/anomalies/anomalies_weather_themes.dm b/code/game/objects/effects/anomalies/anomalies_weather_themes.dm
index 1afbb5d225..8290f6f11f 100644
--- a/code/game/objects/effects/anomalies/anomalies_weather_themes.dm
+++ b/code/game/objects/effects/anomalies/anomalies_weather_themes.dm
@@ -57,7 +57,7 @@
T.vis_contents -= visuals
/datum/anomalous_weather/proc/affect_turf(turf/to_affect)
- if(iswall(to_affect))
+ if(iswall(to_affect) || isopenturf(to_affect))
return
for(var/atom/thing in to_affect)
@@ -130,7 +130,7 @@
drench_message = "Rain falls on you, drenching you in water."
/datum/anomalous_weather/rain/do_special(turf/simulated/T)
- if(prob(2))
+ if(prob(2) && !isopenturf(T))
T.wet_floor(1)
/datum/anomalous_weather/rain/blood
@@ -153,9 +153,9 @@
drench_message = "Rain falls on you, completely soaking you."
/datum/anomalous_weather/rain/storm/do_special(turf/simulated/T)
- if(prob(3))
+ if(prob(3) && !isopenturf(T))
T.wet_floor(1)
- if(prob(0.025))
+ if(prob(0.025) && !isopenturf(T))
lightning_strike(T)
/datum/anomalous_weather/rain/acid
diff --git a/code/game/objects/effects/anomalies/anomaly_stats.dm b/code/game/objects/effects/anomalies/anomaly_stats.dm
new file mode 100644
index 0000000000..0174b331ed
--- /dev/null
+++ b/code/game/objects/effects/anomalies/anomaly_stats.dm
@@ -0,0 +1,191 @@
+/datum/anomaly_stats
+ var/severity = 1
+ var/stability
+ var/max_health
+
+ var/danger_type
+ var/unstable_type
+ var/containment_type
+ var/transformation_type
+
+ var/datum/anomaly_modifiers/modifier
+
+ var/datum/weakref/attached_anomaly
+ var/datum/weakref/attached_harvester
+
+ var/next_activation
+ // Total of points we'll get once the anomaly does a pulse
+ var/points
+ var/curr_health
+ var/flags
+
+ var/min_activation = 45 SECONDS
+ var/max_activation = 90 SECONDS
+
+/datum/anomaly_stats/New()
+ randomize_particle_types()
+ severity = rand(5, 15)
+ max_health = rand(50, 150)
+ curr_health = max_health
+ stability = ANOMALY_STABLE
+
+/datum/anomaly_stats/Destroy(force)
+ QDEL_NULL(modifier)
+ return ..()
+
+/datum/anomaly_stats/proc/randomize_particle_types()
+ var/list/particles = list(ANOMALY_PARTICLE_SIGMA, ANOMALY_PARTICLE_DELTA, ANOMALY_PARTICLE_ZETA, ANOMALY_PARTICLE_EPSILON)
+ particles = shuffle(particles)
+ danger_type = particles[1]
+ unstable_type = particles[2]
+ containment_type = particles[3]
+ transformation_type = particles[4]
+
+/datum/anomaly_stats/proc/calculate_points()
+ var/total = 10
+
+ var/obj/effect/anomaly/anomaly = attached_anomaly.resolve()
+
+ if(!anomaly)
+ return
+
+ total += severity/5
+ total *= curr_health/max_health
+ total *= anomaly.danger_mult
+
+ if(modifier)
+ total *= modifier.get_value()
+
+ points = total
+
+ return points
+
+/datum/anomaly_stats/proc/particle_hit(particle)
+ // I don't really like this, but switch() expects a constant expression
+ if(particle == danger_type)
+ update_severity(1, 5)
+ return
+ else if(particle == unstable_type)
+ update_state(TRUE)
+ return
+ else if(particle == containment_type)
+ update_state(FALSE)
+ update_health(-1, -10)
+ return
+ else if(particle == transformation_type)
+ update_modifiers()
+ return
+ return
+
+/datum/anomaly_stats/proc/update_severity(lower, upper)
+ var/obj/effect/anomaly/anom = attached_anomaly.resolve()
+ if(!istype(anom))
+ attached_anomaly = null
+ return
+ var/sev_change = rand(lower, upper)
+ severity += sev_change
+
+ if(severity >= 100)
+ kill_anomaly(TRUE)
+ return
+
+ var/scale_factor = 1.0+(severity/100)
+ var/matrix/M = matrix()
+ M.Scale(scale_factor, scale_factor)
+ animate(anom, transform = M, time = 1 SECOND)
+
+ // These don't have the wibbly filter
+ if(!istype(anom, /obj/effect/anomaly/bioscrambler) && !istype(anom, /obj/effect/anomaly/dimensional))
+ apply_wibbly_filters(anom)
+
+ calculate_points()
+ return
+
+/datum/anomaly_stats/proc/update_health(lower, upper)
+ var/health_change = rand(lower, upper)
+ curr_health += health_change
+
+ if(curr_health <= 0)
+ kill_anomaly(FALSE)
+ return
+
+ calculate_points()
+ return
+
+/datum/anomaly_stats/proc/kill_anomaly(critical)
+ var/obj/effect/anomaly/anom = attached_anomaly.resolve()
+ if(!istype(anom))
+ attached_anomaly = null
+ return
+ if(critical)
+ anom.detonate()
+ var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread
+ s.set_up(5, 1, src)
+ s.start()
+ QDEL_NULL(anom)
+ return
+
+/datum/anomaly_stats/proc/update_state(unstable)
+ var/obj/effect/anomaly/anom = attached_anomaly.resolve()
+
+ if(!istype(anom))
+ return
+
+ if(locate(/obj/effect/suspension_field) in get_turf(anom))
+ return
+
+ switch(stability)
+ if(ANOMALY_STABLE)
+ if(unstable && prob(15))
+ stability = ANOMALY_GROWING
+ else if(prob(15))
+ stability = ANOMALY_DECAYING
+ return
+ if(ANOMALY_GROWING)
+ if(!unstable && prob(15))
+ stability = ANOMALY_STABLE
+ return
+ if(ANOMALY_DECAYING)
+ if(unstable && prob(15))
+ stability = ANOMALY_STABLE
+ else
+ return
+ return
+
+/datum/anomaly_stats/proc/update_modifiers()
+ var/picked_mod = pick(subtypesof(/datum/anomaly_modifiers))
+
+ // Tough luck. No mod for you.
+ if(istype(modifier, picked_mod))
+ return
+
+ if(modifier)
+ modifier.on_remove(attached_anomaly)
+ modifier = null
+
+ modifier = new picked_mod
+ modifier.on_add(attached_anomaly)
+ calculate_points()
+ return
+
+/datum/anomaly_stats/proc/get_activation_countdown()
+ return (next_activation - world.time)/10
+
+/datum/anomaly_stats/proc/pulse_effect()
+ if(stability == ANOMALY_DECAYING)
+ update_health(-1, -10)
+ else if(stability == ANOMALY_GROWING)
+ update_severity(1, 10)
+
+ if(prob(5))
+ if(stability == ANOMALY_STABLE)
+ stability = pick(ANOMALY_DECAYING, ANOMALY_GROWING)
+ else
+ stability = ANOMALY_STABLE
+
+ if(attached_harvester)
+ var/obj/machinery/anomaly_harvester/harvester = attached_harvester.resolve()
+ if(!istype(harvester))
+ return
+
+ harvester.add_points(points)
diff --git a/code/game/objects/effects/anomalies/anomaly_stats_mods.dm b/code/game/objects/effects/anomalies/anomaly_stats_mods.dm
new file mode 100644
index 0000000000..c36a57c214
--- /dev/null
+++ b/code/game/objects/effects/anomalies/anomaly_stats_mods.dm
@@ -0,0 +1,87 @@
+/datum/anomaly_modifiers
+ var/name
+ var/description
+ var/value
+ var/obj/effect/anomaly/attached_anomaly = null
+
+/datum/anomaly_modifiers/proc/get_description()
+ return description
+
+/datum/anomaly_modifiers/proc/get_value()
+ return value
+
+/datum/anomaly_modifiers/proc/on_add(datum/weakref/anomaly)
+ attached_anomaly = anomaly.resolve()
+ if(!istype(attached_anomaly))
+ return FALSE
+ return TRUE
+
+/datum/anomaly_modifiers/proc/on_remove(datum/weakref/anomaly)
+ attached_anomaly = anomaly.resolve()
+ if(!istype(attached_anomaly))
+ return FALSE
+ return TRUE
+
+/datum/anomaly_modifiers/reflective
+ name = "Reflective"
+ description = "A protective coating was detected."
+ value = 1.2
+
+/datum/anomaly_modifiers/invisible
+ name = "Invisible"
+ description = "Light wave distortion was detected."
+ value = 1.5
+
+/datum/anomaly_modifiers/invisible/on_add(datum/weakref/anomaly)
+ if(!..())
+ return
+ addtimer(CALLBACK(attached_anomaly, TYPE_PROC_REF(/atom/movable, cloak)), 2 SECONDS)
+
+/datum/anomaly_modifiers/invisible/on_remove(datum/weakref/anomaly)
+ if(!..())
+ return
+ addtimer(CALLBACK(attached_anomaly, TYPE_PROC_REF(/atom/movable, uncloak)), 2 SECONDS)
+
+/*
+/datum/anomaly_modifiers/hidden
+ name = "Hidden"
+ description = "Interference detected. Some data cannot be read."
+ value = 0.15
+*/
+
+/datum/anomaly_modifiers/move
+ name = "Move"
+ description = "Anomalous anchoring could not be detected."
+ value = 1.4
+
+/datum/anomaly_modifiers/move/on_add(datum/weakref/anomaly)
+ if(!..())
+ return
+ attached_anomaly.move_chance = ANOMALY_MOVECHANCE
+
+/datum/anomaly_modifiers/move/on_remove(datum/weakref/anomaly)
+ if(!..())
+ return
+ attached_anomaly.move_chance = 0
+
+/datum/anomaly_modifiers/fast
+ name = "Faster Pulses"
+ description = "Anomalous pulses are more common."
+ value = 0.9
+
+/datum/anomaly_modifiers/fast/on_add(datum/weakref/anomaly)
+ if(!..())
+ return
+
+ var/datum/anomaly_stats/stats = attached_anomaly.stats
+ stats.min_activation = 25 SECONDS
+ stats.max_activation = 45 SECONDS
+
+/datum/anomaly_modifiers/fast/on_remove(datum/weakref/anomaly)
+ if(!..())
+ return
+
+ var/datum/anomaly_stats/stats = attached_anomaly.stats
+
+ stats.min_activation = initial(stats.min_activation)
+ stats.max_activation = initial(stats.max_activation)
diff --git a/code/game/objects/items/devices/anomaly.dm b/code/game/objects/items/devices/anomaly.dm
deleted file mode 100644
index 0fe6e77191..0000000000
--- a/code/game/objects/items/devices/anomaly.dm
+++ /dev/null
@@ -1,43 +0,0 @@
-/obj/item/anomaly_neutralizer
- name = "anomaly neutralizer"
- desc = "A one-use device capable of instantly neutralizing anomalous or otherworldly entities."
- icon = 'icons/obj/devices/tool.dmi'
- icon_state = "neutralyzer"
- w_class = ITEMSIZE_SMALL
- slot_flags = SLOT_BELT
- item_flags = NOBLUDGEON
-
-/obj/item/anomaly_neutralizer/Initialize(mapload)
- . = ..()
-
- AddComponent(/datum/component/effect_remover, \
- success_feedback = "You neutralize %THEEFFECT with %THEWEAPON, frying its circuitry in the process.", \
- on_clear_callback = CALLBACK(src, PROC_REF(on_anomaly_neutralized)), \
- effects_we_clear = list(/obj/effect/anomaly))
-
-/obj/item/anomaly_neutralizer/proc/on_anomaly_neutralized(obj/effect/anomaly/target, mob/living/user)
- target.anomalyNeutralize()
- on_use(target, user)
-
-/obj/item/anomaly_neutralizer/proc/on_use(obj/effect/target, mob/living/user)
- var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
- sparks.set_up(3, 1, src)
- sparks.start()
- qdel(src)
-
-/obj/item/anomaly_releaser
- icon = 'icons/obj/devices/syndie_gadget.dmi'
- icon_state = "anomaly_releaser"
- name = "anomaly releaser"
- desc = "Single-use injector that releases and stabilizes anomalies by injecting an unknown substance."
- throwforce = 0
- w_class = ITEMSIZE_SMALL
- throw_speed = 3
- throw_range = 5
-
- ///icon state after being used up
- var/used_icon_state = "anomaly_releaser_used"
- ///are we used? if used we can't be used again
- var/used = FALSE
- ///Can we be used infinitely?
- var/infinite = FALSE
diff --git a/code/game/objects/items/devices/multitool.dm b/code/game/objects/items/devices/multitool.dm
index d88b6ef31e..1f8406147e 100644
--- a/code/game/objects/items/devices/multitool.dm
+++ b/code/game/objects/items/devices/multitool.dm
@@ -100,3 +100,10 @@
icon_state = "multitool"
toolspeed = 0.1
origin_tech = list(TECH_MAGNET = 5, TECH_ENGINEERING = 5)
+
+// Alien multitool only has those icon states
+/obj/item/multitool/alien/update_icon()
+ if(accepting_refs)
+ icon_state = "multitool_ref_scan"
+ return
+ icon_state = "multitool"
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index a75eba2a57..c47c9ec042 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -546,6 +546,29 @@
to_chat(R, span_notice("Letter compartment upgraded!"))
return TRUE
+// Anomaly Gun
+/obj/item/borg/upgrade/restricted/anomalygun
+ name = "robot mounted particle gun"
+ desc = "A anomaly particle gun adapted for installation in cyborgs, allows them to shoot different particles upon anomalies."
+ icon_state = "cyborg_upgrade2"
+ item_state = "cyborg_upgrade"
+ module_flags = BORG_MODULE_SCIENCE
+ require_module = TRUE
+
+/obj/item/borg/upgrade/restricted/anomalygun/action(mob/user, mob/living/silicon/robot/R)
+ if(..()) return FALSE
+
+ if(!R.supports_upgrade(type))
+ generic_error(user, R, type)
+ return FALSE
+
+ if(R.has_restricted_upgrade(type))
+ generic_error(user, R, type)
+ return FALSE
+
+ R.module.modules += new/obj/item/gun/energy/anomaly/mounted(R.module)
+ return TRUE
+
/* ###############################################
# Unsorted section. All cargo modules go here.#
###############################################*/
diff --git a/code/game/objects/items/weapons/circuitboards/machinery/anomaly_harvester.dm b/code/game/objects/items/weapons/circuitboards/machinery/anomaly_harvester.dm
new file mode 100644
index 0000000000..a7a32ea1dd
--- /dev/null
+++ b/code/game/objects/items/weapons/circuitboards/machinery/anomaly_harvester.dm
@@ -0,0 +1,17 @@
+#ifndef T_BOARD
+#error T_BOARD macro is not defined but we need it!
+#endif
+
+/obj/item/circuitboard/anomaly_harvester
+ name = T_BOARD("anomaly harvester")
+ build_path = /obj/machinery/anomaly_harvester
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_DATA = 1)
+ req_components = list(
+ /obj/item/stock_parts/matter_bin = 2,
+ /obj/item/stock_parts/manipulator = 1,
+ /obj/item/stock_parts/micro_laser = 1,
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stack/material/phoron = 5,
+ /obj/item/stack/material/diamond = 1
+ )
diff --git a/code/game/objects/items/weapons/storage/belt.dm b/code/game/objects/items/weapons/storage/belt.dm
index 9383b94286..39d068656d 100644
--- a/code/game/objects/items/weapons/storage/belt.dm
+++ b/code/game/objects/items/weapons/storage/belt.dm
@@ -86,7 +86,9 @@
/obj/item/stack/nanopaste,
/obj/item/geiger,
/obj/item/reagent_scanner,
- /obj/item/lightpainter
+ /obj/item/lightpainter,
+ /obj/item/anomaly_releaser,
+ /obj/item/anomaly_scanner
)
/obj/item/storage/belt/utility/full
diff --git a/code/modules/anomalies/anomaly.dm b/code/modules/anomalies/anomaly.dm
new file mode 100644
index 0000000000..fa70de057b
--- /dev/null
+++ b/code/modules/anomalies/anomaly.dm
@@ -0,0 +1,162 @@
+/obj/item/anomaly_neutralizer
+ name = "anomaly neutralizer"
+ desc = "A one-use device capable of instantly neutralizing anomalous or otherworldly entities."
+ icon = 'icons/obj/devices/tool.dmi'
+ icon_state = "neutralyzer"
+ w_class = ITEMSIZE_SMALL
+ slot_flags = SLOT_BELT
+ item_flags = NOBLUDGEON
+
+/obj/item/anomaly_neutralizer/Initialize(mapload)
+ . = ..()
+
+ AddComponent(/datum/component/effect_remover, \
+ success_feedback = "You neutralize %THEEFFECT with %THEWEAPON, frying its circuitry in the process.", \
+ on_clear_callback = CALLBACK(src, PROC_REF(on_anomaly_neutralized)), \
+ effects_we_clear = list(/obj/effect/anomaly))
+
+/obj/item/anomaly_neutralizer/proc/on_anomaly_neutralized(obj/effect/anomaly/target, mob/living/user)
+ target.anomalyNeutralize()
+ on_use(target, user)
+
+/obj/item/anomaly_neutralizer/proc/on_use(obj/effect/target, mob/living/user)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread
+ sparks.set_up(3, 1, src)
+ sparks.start()
+ qdel(src)
+
+/obj/item/anomaly_releaser
+ icon = 'icons/obj/devices/syndie_gadget.dmi'
+ icon_state = "anomaly_releaser"
+ name = "advanced anomaly releaser"
+ desc = "Single-use injector that releases and stabilizes anomalies by injecting an unknown substance."
+ throwforce = 0
+ w_class = ITEMSIZE_SMALL
+ throw_speed = 3
+ throw_range = 5
+
+ ///icon state after being used up
+ var/used_icon_state = "anomaly_releaser_used"
+ ///are we used? if used we can't be used again
+ var/used = FALSE
+ ///Can we be used infinitely?
+ var/infinite = FALSE
+ //If the created anomaly leaves a core behind
+ var/has_core = TRUE
+ //If this will anchor the anomaly in place
+ var/will_anchor = TRUE
+ // If it will apply stats to it
+ var/gives_stats = TRUE
+
+/obj/item/anomaly_releaser/science
+ icon = 'icons/obj/devices/tool.dmi'
+ icon_state = "sci_releaser"
+ name = "scientific anomaly releaser"
+ used_icon_state = "sci_releaser_used"
+ has_core = FALSE
+
+// The one for antags and evil-doers
+/obj/item/anomaly_releaser/antag
+ has_core = TRUE
+ will_anchor = TRUE
+ gives_stats = FALSE // Evil and fucked up...
+ desc = "Single-use injector that releases and stabilizes anomalies by injecting an unknown substance. This one seems odd."
+
+/obj/item/anomaly_scanner
+ name = "anomaly scanner"
+ desc = "A hand-held anomaly scanner, able to distinguish the particles that might affect a stable anomaly."
+ icon = 'icons/obj/device.dmi'
+ icon_state = "anom_scanner"
+ slot_flags = SLOT_BELT
+ w_class = ITEMSIZE_SMALL
+ throw_speed = 5
+ throw_range = 10
+ matter = list(MAT_STEEL = 200)
+
+ pickup_sound = 'sound/items/pickup/device.ogg'
+ drop_sound = 'sound/items/drop/device.ogg'
+
+ var/datum/weakref/buffered_anomaly = null
+
+/obj/item/anomaly_scanner/attack_self(mob/living/user)
+ . = ..(user)
+ if(.)
+ return TRUE
+ tgui_interact(user)
+
+/obj/item/anomaly_scanner/tgui_static_data(mob/user)
+ . = ..()
+ if(isrobot(loc))
+ var/mob/living/silicon/robot/robot_owner = loc
+ .["theme"] = robot_owner.get_ui_theme()
+
+/obj/item/anomaly_scanner/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AnomalyScanner", name)
+ ui.open()
+
+/obj/item/anomaly_scanner/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
+ var/list/data = list()
+ var/obj/effect/anomaly/anom = buffered_anomaly.resolve()
+
+ if(!istype(anom))
+ return data
+
+ var/datum/anomaly_stats/stats = anom.stats
+
+ data["anomaly_name"] = anom.name
+ data["severity"] = stats.severity
+ data["stability"] = stats.stability
+ data["point_output"] = stats.points
+ data["danger_type"] = stats.danger_type
+ data["unstable_type"] = stats.unstable_type
+ data["containment_type"] = stats.containment_type
+ data["transformation_type"] = stats.transformation_type
+ if(stats.modifier)
+ data["modifier"] = stats.modifier.get_description()
+ data["countdown"] = stats.get_activation_countdown()
+
+ return data
+
+/obj/item/gun/energy/anomaly
+ name = "anomalous particle gun"
+ desc = "A handheld particle emitter, used to safely release a specific particle frequency."
+ icon_state = "taserblue"
+ fire_delay = 4
+ projectile_type = /obj/item/projectile/energy/anomaly
+ fire_sound = 'sound/weapons/taser2.ogg'
+ recoil_mode = 0
+ accuracy = 30
+
+ var/particle = ANOMALY_PARTICLE_SIGMA
+
+/obj/item/gun/energy/anomaly/mounted
+ name = "mounted particle gun"
+ self_recharge = 1
+ use_external_power = 1
+
+/obj/item/gun/energy/anomaly/attack_self(mob/user)
+ var/chosen_particle = tgui_input_list(user, "Select particle type", "Particle Selection", ANOMALY_PARTICLE_ALL)
+ if(!chosen_particle)
+ return
+
+ particle = chosen_particle
+ balloon_alert_visible("changed to [chosen_particle]")
+ ..()
+
+/obj/item/gun/energy/anomaly/consume_next_projectile()
+ var/obj/item/cell/battery = power_supply
+
+ if(use_external_power)
+ battery = get_external_power_supply()
+
+ if(!battery || !battery.checked_use(charge_cost))
+ return null
+ var/mob/living/M = loc
+ if(istype(M))
+ M.hud_used.update_ammo_hud(M, src)
+ var/obj/item/projectile/energy/anomaly/projectile = new
+ if(particle)
+ projectile.particle_type = particle
+ return projectile
diff --git a/code/modules/anomalies/anomaly_harvester.dm b/code/modules/anomalies/anomaly_harvester.dm
new file mode 100644
index 0000000000..ac04897dc8
--- /dev/null
+++ b/code/modules/anomalies/anomaly_harvester.dm
@@ -0,0 +1,200 @@
+/obj/machinery/anomaly_harvester
+ name = "anomaly harvester"
+ desc = "A strange device that condenses anomalous energy into tangible material."
+ icon = 'icons/obj/machines/anomaly_harvester.dmi'
+ icon_state = "harvester"
+ density = TRUE
+ circuit = /obj/item/circuitboard/anomaly_harvester
+ active_power_usage = 40000
+ idle_power_usage = 1000
+
+ var/points = 0
+ var/points_to_create = 100
+ var/efficiency = 1
+
+ var/datum/weakref/harvested
+ var/list/obj/item/research_sample/samples
+
+/obj/machinery/anomaly_harvester/Initialize(mapload)
+ . = ..()
+ default_apply_parts()
+
+/obj/machinery/anomaly_harvester/Destroy()
+ harvested = null
+ return ..()
+
+/obj/machinery/anomaly_harvester/RefreshParts()
+ var/efficient = 0
+ var/rating = 0
+ for(var/obj/item/stock_parts/manipulator/manipulator in component_parts)
+ efficient += manipulator.rating - 2
+ for(var/obj/item/stock_parts/micro_laser/laser in component_parts)
+ rating += laser.rating - 2
+
+ efficiency = max(1, (efficient/10+1))
+ points_to_create = min(100, (100 - (rating * 5)))
+
+/obj/machinery/anomaly_harvester/process()
+ ..()
+ if(stat & (NOPOWER|BROKEN) || !anchored)
+ update_use_power(USE_POWER_OFF)
+ else
+ update_use_power(USE_POWER_IDLE)
+ harvest_anomaly()
+ if(points && points >= points_to_create)
+ points -= points_to_create
+ generate_sample()
+ update_icon()
+
+/obj/machinery/anomaly_harvester/proc/add_points(add_points)
+ add_points *= efficiency
+ points += add_points
+ return
+
+/obj/machinery/anomaly_harvester/proc/harvest_anomaly()
+ if(!harvested)
+ return
+
+ var/obj/effect/anomaly/anom = harvested.resolve()
+ if(!istype(anom))
+ return
+
+ var/datum/anomaly_stats/stats = anom.stats
+
+ if(stats.stability == ANOMALY_DECAYING)
+ playsound(src, 'sound/machines/2beephigh.ogg', 75)
+ else if (stats.stability == ANOMALY_GROWING)
+ playsound(src, 'sound/machines/buzzbeep.ogg', 75)
+
+/obj/machinery/anomaly_harvester/attackby(obj/item/W, mob/user, attack_modifier, click_parameters)
+ add_fingerprint(user)
+
+ if(default_deconstruction_screwdriver(user, W))
+ return
+ if(default_deconstruction_crowbar(user, W))
+ return
+ if(default_part_replacement(user, W))
+ return
+ if(default_unfasten_wrench(user, W, 2 SECONDS))
+ return
+ if(istype(W, /obj/item/anomaly_scanner))
+ if(!anchored)
+ to_chat(user, span_danger("The [src] is not anchored!"))
+ return
+ var/obj/item/anomaly_scanner/scanner = W
+ if(scanner.buffered_anomaly && do_after(user, 2 SECONDS, src))
+ attach_anomaly(scanner.buffered_anomaly)
+ return
+
+ return ..()
+
+/obj/machinery/anomaly_harvester/default_unfasten_wrench(mob/user, obj/item/W, time)
+ . = ..()
+ harvested = null
+
+/obj/machinery/anomaly_harvester/proc/attach_anomaly(datum/weakref/anomaly)
+ var/obj/effect/anomaly/anom = anomaly.resolve()
+ if(!istype(anom))
+ return
+
+ var/datum/anomaly_stats/stats = anom.stats
+ if(stats.attached_harvester)
+ var/obj/machinery/anomaly_harvester/harvester = stats.attached_harvester.resolve()
+ harvester.harvested = null
+ harvester.update_icon()
+ stats.attached_harvester = null
+ harvested = anomaly
+ stats.attached_harvester = WEAKREF(src)
+ playsound(src, 'sound/machines/boobeebeep.ogg', 75, TRUE)
+ return TRUE
+
+/obj/machinery/anomaly_harvester/proc/generate_sample()
+ update_use_power(USE_POWER_ACTIVE)
+ playsound(src, 'sound/machines/ping.ogg', 50, TRUE)
+ switch(rand(1, 100))
+ if(1 to 60)
+ new /obj/item/research_sample/common(src)
+ if(61 to 85)
+ new /obj/item/research_sample/uncommon(src)
+ if(86 to 95)
+ new /obj/item/research_sample/rare(src)
+ if(96 to 100)
+ new /obj/item/research_sample/bluespace(src)
+ else
+ new /obj/item/research_sample/common(src)
+
+/obj/machinery/anomaly_harvester/update_icon()
+ cut_overlays()
+ if(stat & (NOPOWER|BROKEN) || !anchored)
+ add_overlay("harvester_off")
+ else
+ add_overlay("harvester_on")
+
+ if(harvested)
+ var/obj/effect/anomaly/anom = harvested.resolve()
+ if(!istype(anom))
+ return
+
+ var/datum/anomaly_stats/stats = anom.stats
+
+ switch(stats.stability)
+ if(ANOMALY_STABLE)
+ add_overlay("harvester_stable")
+ if(ANOMALY_DECAYING)
+ add_overlay("harvester_decay")
+ else
+ add_overlay("harvester_grow")
+
+/obj/machinery/anomaly_harvester/attack_ai(mob/user)
+ return attack_hand(user)
+
+/obj/machinery/anomaly_harvester/attack_hand(mob/user)
+ if(..())
+ return
+ tgui_interact(user)
+
+/obj/machinery/anomaly_harvester/tgui_state(mob/user)
+ return GLOB.tgui_default_state
+
+/obj/machinery/anomaly_harvester/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AnomalyHarvester", name)
+ ui.open()
+
+/obj/machinery/anomaly_harvester/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
+ var/list/sample_data = list()
+ for(var/obj/item/research_sample/sample in src)
+ UNTYPED_LIST_ADD(sample_data, list(
+ "name" = sample.name,
+ "icon" = sample.icon,
+ "icon_state" = sample.icon_state,
+ "ref" = REF(sample)
+ ))
+
+ var/obj/effect/anomaly/anom = harvested?.resolve()
+ var/list/data = list(
+ "name" = anom,
+ "points" = points,
+ "pointsToGenerate" = points_to_create,
+ "samples" = sample_data
+ )
+
+ return data
+
+/obj/machinery/anomaly_harvester/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("release_sample")
+ var/obj/item/research_sample/sample = locate(params["ref"]) in src
+ if(!istype(sample) || (sample.loc != src))
+ return FALSE
+ sample.forceMove(get_turf(src))
+ return TRUE
+ if("release_all")
+ for(var/obj/item/research_sample/sample in src)
+ sample.forceMove(get_turf(src))
+ return TRUE
diff --git a/code/modules/mob/living/silicon/robot/robot_modules/station.dm b/code/modules/mob/living/silicon/robot/robot_modules/station.dm
index db07152997..c8145770ec 100644
--- a/code/modules/mob/living/silicon/robot/robot_modules/station.dm
+++ b/code/modules/mob/living/silicon/robot/robot_modules/station.dm
@@ -680,7 +680,7 @@
/obj/item/robot_module/robot/research
name = "research module"
channels = list(CHANNEL_SCIENCE = 1)
- supported_upgrades = list(/obj/item/borg/upgrade/restricted/advrped)
+ supported_upgrades = list(/obj/item/borg/upgrade/restricted/advrped, /obj/item/borg/upgrade/restricted/anomalygun)
pto_type = PTO_SCIENCE
/obj/item/robot_module/robot/research/create_equipment(var/mob/living/silicon/robot/robot)
@@ -706,6 +706,7 @@
// Anomaly handling
src.modules += new /obj/item/analyzer(src)
src.modules += new /obj/item/assembly/signaler(src)
+ src.modules += new /obj/item/anomaly_scanner(src)
src.emag += new /obj/item/hand_tele(src)
diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm
index e0634cb52f..eb807b54d7 100644
--- a/code/modules/power/singularity/emitter.dm
+++ b/code/modules/power/singularity/emitter.dm
@@ -23,6 +23,10 @@
var/state = 0
var/locked = 0
+ // Anomaly harvesting stuff
+ var/anomalous = FALSE
+ var/particle = ANOMALY_PARTICLE_SIGMA
+
var/burst_delay = 2
var/initial_fire_delay = 100
@@ -59,13 +63,13 @@
if(!src.locked)
if(src.active==1)
src.active = 0
- to_chat(user, "You turn off [src].")
- message_admins("Emitter turned off by [key_name(user, user.client)](?) in ([x],[y],[z] - JMP)")
+ balloon_alert_visible("turned off")
+ message_admins("Emitter turned off by [key_name(user, user.client)](?) in ([x],[y],[z] - JMP)",0,1)
log_game("EMITTER([x],[y],[z]) OFF by [key_name(user)]")
investigate_log("turned " + span_red("off") + " by [user.key]","singulo")
else
src.active = 1
- to_chat(user, "You turn on [src].")
+ balloon_alert_visible("turned on")
src.shot_number = 0
src.fire_delay = get_initial_fire_delay()
message_admins("Emitter turned on by [key_name(user, user.client)](?) in ([x],[y],[z] - JMP)")
@@ -223,6 +227,18 @@
else
to_chat(user, span_warning("Access denied."))
return
+ if(istype(W, /obj/item/anomaly_scanner))
+ anomalous = !anomalous
+ burst_delay = anomalous ? 3 : 8
+ to_chat(user, span_notice("The beam is now set to [anomalous ? "anomalous." : "normal."]"))
+ return
+ if(W.has_tool_quality(TOOL_MULTITOOL) && anomalous)
+ var/chosen_particle = tgui_input_list(user, "Select particle type", "Particle Selection", ANOMALY_PARTICLE_ALL)
+ if(!chosen_particle)
+ return
+ particle = chosen_particle
+ balloon_alert_visible("changed to [chosen_particle]")
+ return
..()
return
@@ -282,4 +298,8 @@
return burst_delay
/obj/machinery/power/emitter/proc/get_emitter_beam()
+ if(anomalous)
+ var/obj/item/projectile/energy/anomaly/projectile = new /obj/item/projectile/energy/anomaly(get_turf(src))
+ projectile.particle_type = particle
+ return projectile
return new /obj/item/projectile/beam/emitter(get_turf(src))
diff --git a/code/modules/projectiles/projectile/energy.dm b/code/modules/projectiles/projectile/energy.dm
index d68009bbf3..9c5ef5fbc6 100644
--- a/code/modules/projectiles/projectile/energy.dm
+++ b/code/modules/projectiles/projectile/energy.dm
@@ -335,3 +335,16 @@
/obj/item/projectile/energy/flash/flare
flash_range = 2
hud_state = "grenade_dummy"
+
+/obj/item/projectile/energy/anomaly
+ name = "anomaly emitter projectile"
+ light_color = "#be008f"
+ icon_state = "purple_laser"
+ damage = 5
+ speed = 1.6
+ var/particle_type
+
+/obj/item/projectile/energy/anomaly/Initialize(mapload, particle)
+ . = ..()
+ if(particle)
+ particle_type = particle
diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm
index 84eb0f7e17..bc885a955c 100644
--- a/code/modules/research/anomaly/anomaly_core.dm
+++ b/code/modules/research/anomaly/anomaly_core.dm
@@ -44,12 +44,12 @@
return FALSE
var/obj/effect/anomaly/anomaly = new core.anomaly_type(get_turf(core))
- anomaly.stabilize()
+ anomaly.stabilize(releaser.will_anchor, releaser.has_core, releaser.gives_stats)
if(!releaser.infinite)
releaser.icon_state = releaser.used_icon_state
releaser.used = TRUE
- releaser.name = "used " + name
+ releaser.name = "used " + releaser.name
qdel(src)
return ..()
diff --git a/code/modules/research/tg/designs/boards/science.dm b/code/modules/research/tg/designs/boards/science.dm
index fbf9bb1adf..d9563771ee 100644
--- a/code/modules/research/tg/designs/boards/science.dm
+++ b/code/modules/research/tg/designs/boards/science.dm
@@ -205,3 +205,12 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_RESEARCH
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design_techweb/board/anomaly_harvester
+ name = "Anomaly Harvester"
+ id = "anomaly_harvester"
+ build_path = /obj/item/circuitboard/anomaly_harvester
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_RESEARCH
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/tg/designs/misc_designs.dm b/code/modules/research/tg/designs/misc_designs.dm
index 44a5eaada2..bd53173d1b 100644
--- a/code/modules/research/tg/designs/misc_designs.dm
+++ b/code/modules/research/tg/designs/misc_designs.dm
@@ -289,8 +289,20 @@
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+/datum/design_techweb/sci_anomaly_releaser
+ name = "Scientific Anomaly Releaser"
+ desc = "A one-time use device, reactivates anomalies and stabilizes them, but leaves no core behind."
+ id = "sci_anomaly_releaser"
+ build_type = PROTOLATHE
+ materials = list(MAT_STEEL = SHEET_MATERIAL_AMOUNT, MAT_PHORON = SHEET_MATERIAL_AMOUNT * 1.5)
+ build_path = /obj/item/anomaly_releaser/science
+ category = list(
+ RND_CATEGORY_EQUIPMENT + RND_SUBCATEGORY_EQUIPMENT_SCIENCE
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
/datum/design_techweb/anomaly_releaser
- name = "Anomaly Releaser"
+ name = "Advanced Anomaly Releaser"
desc = "A one-time use device, reactivates anomalies and stabilizes them."
id = "anomaly_releaser"
build_type = PROTOLATHE
diff --git a/code/modules/research/tg/designs/prosfab_designs.dm b/code/modules/research/tg/designs/prosfab_designs.dm
index 83a84b2bdb..dc80674309 100644
--- a/code/modules/research/tg/designs/prosfab_designs.dm
+++ b/code/modules/research/tg/designs/prosfab_designs.dm
@@ -702,3 +702,10 @@
id = "prosthetic_teshari"
// req_tech = list(TECH_DATA = 3, TECH_BIO = 4)
build_path = /obj/item/disk/species/teshari
+
+/datum/design_techweb/prosfab/robot_upgrade/advanced/anomgun
+ name = "Anomalous Particle Gun Module"
+ desc = "An anomalous particle gun, for anomaly harvesting."
+ id = "borg_anomgun_module"
+ materials = list(MAT_STEEL = 4000, MAT_GLASS = 3500, MAT_PHORON = 1500, MAT_DIAMOND = 1500, MAT_URANIUM = 1500)
+ build_path = /obj/item/borg/upgrade/restricted/anomalygun
diff --git a/code/modules/research/tg/designs/tool_designs.dm b/code/modules/research/tg/designs/tool_designs.dm
index 18540c10bc..a0be2be50d 100644
--- a/code/modules/research/tg/designs/tool_designs.dm
+++ b/code/modules/research/tg/designs/tool_designs.dm
@@ -1015,3 +1015,14 @@
RND_CATEGORY_TOOLS
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design_techweb/anomaly_scanner
+ name = "Anomaly Scanner"
+ id = "anomaly_scanner"
+ materials = list(MAT_STEEL = 1000, MAT_GLASS = 500)
+ build_path = /obj/item/anomaly_scanner
+ build_type = PROTOLATHE
+ category = list(
+ RND_CATEGORY_TOOLS
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/tg/designs/weapon_designs.dm b/code/modules/research/tg/designs/weapon_designs.dm
index b0a551dd29..8a02203730 100644
--- a/code/modules/research/tg/designs/weapon_designs.dm
+++ b/code/modules/research/tg/designs/weapon_designs.dm
@@ -960,3 +960,14 @@
RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_RANGED
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design_techweb/anomaly_gun
+ name = "Handheld Anomalous Particle Releaser"
+ id = "anom_gun"
+ build_type = PROTOLATHE
+ materials = list(MAT_STEEL = 4000, MAT_GLASS = 3500, MAT_PHORON = 1500, MAT_DIAMOND = 1500, MAT_URANIUM = 1500)
+ build_path = /obj/item/gun/energy/anomaly
+ category = list(
+ RND_CATEGORY_WEAPONS + RND_SUBCATEGORY_WEAPONS_RANGED
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/tg/experisci/experiment/instances/misc.dm b/code/modules/research/tg/experisci/experiment/instances/misc.dm
index 906105e6c2..8a18e94b51 100644
--- a/code/modules/research/tg/experisci/experiment/instances/misc.dm
+++ b/code/modules/research/tg/experisci/experiment/instances/misc.dm
@@ -87,3 +87,19 @@
/obj/item/clothing/suit/bomb_suit = 1,
/obj/item/clothing/suit/bio_suit = 1,
)
+
+/datum/experiment/scanning/points/anomaly
+ name = "Anomaly Research Scans"
+ description = "Anomalies. What are they? Where do they come from? Why are they here? Get close and personal with some of them, and observe their strange nature."
+ required_points = 4
+ required_atoms = list(
+ /obj/effect/anomaly/bioscrambler = 1,
+ /obj/effect/anomaly/bluespace = 1,
+ /obj/effect/anomaly/dimensional = 1,
+ /obj/effect/anomaly/dust = 1,
+ /obj/effect/anomaly/flux = 1,
+ /obj/effect/anomaly/grav = 1,
+ /obj/effect/anomaly/hallucination = 1,
+ /obj/effect/anomaly/pyro = 1,
+ /obj/effect/anomaly/weather = 1,
+ )
diff --git a/code/modules/research/tg/techwebs/nodes/research_nodes.dm b/code/modules/research/tg/techwebs/nodes/research_nodes.dm
index f91f4e8900..3ef323b425 100644
--- a/code/modules/research/tg/techwebs/nodes/research_nodes.dm
+++ b/code/modules/research/tg/techwebs/nodes/research_nodes.dm
@@ -99,24 +99,59 @@
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS)
announce_channels = list(CHANNEL_SCIENCE)
+/datum/techweb_node/xenoarch
+ id = TECHWEB_NODE_XENOARCHEOLOGY
+ display_name = "Xenoarcheology Research"
+ description = "Researching those who came before us, extracting artifacts of great value, and harnessing their powers."
+ prereq_ids = list(TECHWEB_NODE_FUNDIMENTAL_SCI)
+ design_ids = list(
+ "ano_scanner",
+ "xenoarch_multitool",
+ "excavationdrill",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_2_POINTS)
+ announce_channels = list(CHANNEL_SCIENCE)
+
/datum/techweb_node/anomaly_research
id = TECHWEB_NODE_ANOMALY_RESEARCH
display_name = "Anomaly Research"
description = "Delving into the study of mysterious anomalies to investigate methods to refine and harness their unpredictable energies."
prereq_ids = list(TECHWEB_NODE_APPLIED_BLUESPACE)
design_ids = list(
- "xenoarch_multitool",
- "excavationdrill",
- "ano_scanner",
- //"anomaly_refinery",
"anomaly_neutralizer",
- "anomaly_releaser",
+ "sci_anomaly_releaser",
"reactive_armour",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS)
announce_channels = list(CHANNEL_SCIENCE)
-// Decided that we were not keen on this being able to be printed freely as we immediately saw undesirable behaviour //CHOMPEdit - Re-enable
+/datum/techweb_node/anomaly_harvesting
+ id = TECHWEB_NODE_ANOMALY_HARVESTING
+ display_name = "Anomaly Harvesting"
+ description = "Harness the power of the mysterious anomalies, refining their energies into tangible materials."
+ prereq_ids = list(TECHWEB_NODE_ANOMALY_RESEARCH)
+ design_ids = list(
+ "anomaly_harvester",
+ "anomaly_scanner",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS)
+ announce_channels = list(CHANNEL_SCIENCE)
+
+/datum/techweb_node/applied_anomaly_harvesting
+ id = TECHWEB_NODE_APPLIED_ANOMALY_HARVESTING
+ display_name = "Applied Anomaly Harvesting"
+ description = "Advanced research in the anomaly field, allowing sophisticated tools to aid in their study."
+ prereq_ids = list(TECHWEB_NODE_ANOMALY_HARVESTING)
+ design_ids = list(
+ "anomaly_releaser",
+ "anom_gun",
+ "borg_anomgun_module"
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS)
+ announce_channels = list(CHANNEL_SCIENCE)
+ discount_experiments = list(/datum/experiment/scanning/points/anomaly = TECHWEB_TIER_3_POINTS)
+
+// Decided that we were not keen on this being able to be printed freely as we immediately saw undesirable behaviour // CHOMPEnable Start
/datum/techweb_node/telekinetics
id = TECHWEB_NODE_TELEKINETIC_RESEARCH
display_name = "Applied Telekinetics Research"
@@ -127,4 +162,4 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS)
announce_channels = list(CHANNEL_SCIENCE)
-// //CHOMPEdit - Re-enable
+// CHOMPEnable End
diff --git a/code/modules/telesci/telesci_computer.dm b/code/modules/telesci/telesci_computer.dm
index 9aab06a9b4..05e708ce56 100644
--- a/code/modules/telesci/telesci_computer.dm
+++ b/code/modules/telesci/telesci_computer.dm
@@ -189,17 +189,10 @@
/obj/machinery/computer/telescience/proc/telefail()
COOLDOWN_START(src, teleport_cooldown, (2 SECONDS))
switch(rand(99))
- if(0 to 80)
+ if(0 to 85)
sparks()
visible_message(span_warning("The telepad weakly fizzles."))
return
- if(81 to 85)
- sparks()
- var/anomaly = pick(FLUX_ANOMALY, GRAVITATIONAL_ANOMALY, PYRO_ANOMALY, HALLUCINATION_ANOMALY, BIOSCRAMBLER_ANOMALY, DIMENSIONAL_ANOMALY, WEATHER_ANOMALY)
- generate_anomaly(get_turf(telepad), anomaly, 1, FALSE)
- for(var/mob/living/carbon/human/human in viewers(telepad, null))
- to_chat(human, span_warning("The telepad crackles with energy, as a tear in reality is created!"))
- return
if(86 to 90)
// Irradiate everyone in telescience!
for(var/obj/machinery/telepad/E in GLOB.machines)
diff --git a/code/modules/xenoarcheaology/tools/suspension_generator.dm b/code/modules/xenoarcheaology/tools/suspension_generator.dm
index c269f56f6a..214526e9da 100644
--- a/code/modules/xenoarcheaology/tools/suspension_generator.dm
+++ b/code/modules/xenoarcheaology/tools/suspension_generator.dm
@@ -1,6 +1,6 @@
/obj/machinery/suspension_gen
name = "suspension field generator"
- desc = "It has stubby bolts up against it's treads for stabilising. Used to be required for artifact removal but now merely works as a monster deterrant."
+ desc = "It has stubby bolts up against it's treads for stabilising. Used to hold anomalies stable in place."
icon = 'icons/obj/xenoarchaeology.dmi'
icon_state = "suspension"
density = 1
@@ -8,7 +8,7 @@
var/obj/item/cell/cell
var/obj/item/card/id/auth_card
var/locked = 1
- var/power_use = 5
+ var/power_use = 15
var/obj/effect/suspension_field/suspension_field
/obj/machinery/suspension_gen/Initialize(mapload)
@@ -153,6 +153,12 @@
M.Weaken(5)
M.visible_message(span_blue("[icon2html(M,viewers(M))] [M] begins to float in the air!"),"You feel tingly and light, but it is difficult to move.")
+ for(var/obj/effect/anomaly/anom in T)
+ anom.immortal = TRUE
+ anom.move_chance = 0
+ if(!anom.stats)
+ anom.stats = new /datum/anomaly_stats(anom)
+
suspension_field = new(T)
visible_message(span_blue("[icon2html(src,viewers(src))] [src] activates with a low hum."))
icon_state = "suspension_on"
@@ -181,6 +187,15 @@
to_chat(M, span_info("You no longer feel like floating."))
M.Weaken(3)
+ for(var/obj/effect/anomaly/anom in T)
+ if(anom.stats)
+ var/datum/anomaly_stats/anom_stats = anom.stats
+ if(istype(anom_stats.modifier, /datum/anomaly_modifiers/move))
+ anom.move_chance = initial(anom.move_chance)
+ continue
+ else
+ anom.move_chance = initial(anom.move_chance)
+
visible_message(span_blue("[icon2html(src,viewers(src))] [src] deactivates with a gentle shudder."))
qdel(suspension_field)
suspension_field = null
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index 9855dbcdc2..62eeac68c5 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/icons/obj/devices/tool.dmi b/icons/obj/devices/tool.dmi
index 0d89f29676..59ae27ca5b 100644
Binary files a/icons/obj/devices/tool.dmi and b/icons/obj/devices/tool.dmi differ
diff --git a/icons/obj/machines/anomaly_harvester.dmi b/icons/obj/machines/anomaly_harvester.dmi
new file mode 100644
index 0000000000..6c5b8a76f3
Binary files /dev/null and b/icons/obj/machines/anomaly_harvester.dmi differ
diff --git a/maps/groundbase/gb-z2.dmm b/maps/groundbase/gb-z2.dmm
index bc6ab3f3d6..9efe0bea88 100644
--- a/maps/groundbase/gb-z2.dmm
+++ b/maps/groundbase/gb-z2.dmm
@@ -6512,6 +6512,7 @@
/obj/machinery/requests_console/preset/research{
pixel_y = 30
},
+/obj/machinery/anomaly_harvester,
/turf/simulated/floor/tiled,
/area/groundbase/science/rnd)
"rI" = (
@@ -7172,6 +7173,7 @@
/turf/simulated/floor/tiled/white,
/area/groundbase/medical/triage)
"tI" = (
+/obj/machinery/rnd/destructive_analyzer,
/turf/simulated/floor/tiled/dark,
/area/groundbase/science/rnd)
"tJ" = (
@@ -10461,13 +10463,32 @@
/area/groundbase/dorms/room1)
"Dm" = (
/obj/structure/table/standard,
-/obj/item/retail_scanner/science,
/obj/machinery/atmospherics/unary/vent_scrubber/on{
dir = 8
},
/obj/structure/noticeboard{
pixel_x = 32
},
+/obj/item/assembly/signaler/anomaly/flux{
+ pixel_y = 13;
+ pixel_x = 7
+ },
+/obj/item/anomaly_scanner{
+ pixel_x = 2;
+ pixel_y = -2
+ },
+/obj/item/anomaly_scanner{
+ pixel_x = 5;
+ pixel_y = 3
+ },
+/obj/item/anomaly_releaser/science{
+ pixel_y = -2;
+ pixel_x = -5
+ },
+/obj/item/anomaly_releaser/science{
+ pixel_y = -5;
+ pixel_x = -5
+ },
/turf/simulated/floor/tiled,
/area/groundbase/science/rnd)
"Dn" = (
@@ -11032,6 +11053,7 @@
dir = 8
},
/obj/machinery/light,
+/obj/machinery/power/emitter,
/turf/simulated/floor/tiled,
/area/groundbase/science/rnd)
"EN" = (
@@ -17896,6 +17918,7 @@
dir = 1;
pixel_y = -30
},
+/obj/item/retail_scanner/science,
/turf/simulated/floor/tiled,
/area/groundbase/science/rnd)
"Yc" = (
diff --git a/maps/stellar_delight/stellar_delight1.dmm b/maps/stellar_delight/stellar_delight1.dmm
index 27c648a57a..0f6034c37e 100644
--- a/maps/stellar_delight/stellar_delight1.dmm
+++ b/maps/stellar_delight/stellar_delight1.dmm
@@ -1300,6 +1300,7 @@
/obj/structure/reagent_dispensers/acid{
pixel_x = 32
},
+/obj/machinery/power/emitter,
/turf/simulated/floor/tiled/steel_grid,
/area/rnd/research)
"cx" = (
@@ -9831,6 +9832,8 @@
/obj/item/pen,
/obj/item/packageWrap,
/obj/item/packageWrap,
+/obj/item/multitool/scioutpost,
+/obj/item/retail_scanner/science,
/turf/simulated/floor/tiled/steel_grid,
/area/rnd/research)
"tP" = (
@@ -11863,8 +11866,26 @@
"yq" = (
/obj/structure/table/standard,
/obj/machinery/recharger,
-/obj/item/retail_scanner/science,
-/obj/item/multitool/scioutpost,
+/obj/item/assembly/signaler/anomaly/flux{
+ pixel_y = 13;
+ pixel_x = 7
+ },
+/obj/item/anomaly_scanner{
+ pixel_x = 5;
+ pixel_y = 3
+ },
+/obj/item/anomaly_scanner{
+ pixel_x = 2;
+ pixel_y = -2
+ },
+/obj/item/anomaly_releaser/science{
+ pixel_y = -5;
+ pixel_x = -5
+ },
+/obj/item/anomaly_releaser/science{
+ pixel_y = -2;
+ pixel_x = -5
+ },
/turf/simulated/floor/tiled/steel_grid,
/area/rnd/research)
"yr" = (
@@ -11906,6 +11927,7 @@
/obj/machinery/disposal/wall{
dir = 1
},
+/obj/machinery/anomaly_harvester,
/turf/simulated/floor/tiled/steel_grid,
/area/rnd/research)
"yw" = (
@@ -22240,6 +22262,7 @@
"UA" = (
/obj/machinery/atmospherics/pipe/simple/hidden/supply,
/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,
+/obj/machinery/rnd/destructive_analyzer,
/turf/simulated/floor/tiled/dark,
/area/rnd/research)
"UB" = (
diff --git a/maps/tether/tether-01-surface1.dmm b/maps/tether/tether-01-surface1.dmm
index 814db18360..7da3136b52 100644
--- a/maps/tether/tether-01-surface1.dmm
+++ b/maps/tether/tether-01-surface1.dmm
@@ -3101,6 +3101,7 @@
/obj/machinery/atmospherics/pipe/simple/hidden/supply{
dir = 5
},
+/obj/machinery/anomaly_harvester,
/turf/simulated/floor/tiled,
/area/rnd/hardstorage)
"afh" = (
@@ -8602,6 +8603,7 @@
/area/tether/surfacebase/surface_one_hall)
"aoR" = (
/obj/machinery/light/small,
+/obj/machinery/power/emitter,
/turf/simulated/floor/tiled,
/area/rnd/hardstorage)
"aoS" = (
@@ -28631,6 +28633,14 @@
/obj/machinery/disposal,
/turf/simulated/floor/plating,
/area/crew_quarters/sleep/maintDorm2)
+"aYy" = (
+/obj/structure/table/rack/shelf/steel,
+/obj/item/assembly/signaler/anomaly/flux{
+ pixel_y = 13;
+ pixel_x = 7
+ },
+/turf/simulated/floor/tiled,
+/area/rnd/hardstorage)
"aYz" = (
/obj/structure/sink{
pixel_y = 20
@@ -28640,6 +28650,18 @@
},
/turf/simulated/floor/plating,
/area/crew_quarters/sleep/maintDorm2)
+"aYA" = (
+/obj/structure/table/rack/shelf/steel,
+/obj/item/anomaly_releaser/science{
+ pixel_y = 2;
+ pixel_x = 3
+ },
+/obj/item/anomaly_releaser/science{
+ pixel_y = 1;
+ pixel_x = -3
+ },
+/turf/simulated/floor/tiled,
+/area/rnd/hardstorage)
"aYB" = (
/obj/structure/disposalpipe/trunk{
dir = 4
@@ -28647,6 +28669,18 @@
/obj/machinery/disposal,
/turf/simulated/floor/plating,
/area/crew_quarters/sleep/maintDorm1)
+"aYC" = (
+/obj/structure/table/rack/shelf/steel,
+/obj/item/anomaly_scanner{
+ pixel_x = 2;
+ pixel_y = -2
+ },
+/obj/item/anomaly_scanner{
+ pixel_x = 5;
+ pixel_y = 3
+ },
+/turf/simulated/floor/tiled,
+/area/rnd/hardstorage)
"aYE" = (
/obj/structure/toilet{
pixel_y = 10
@@ -45942,8 +45976,8 @@ auK
avh
abm
abw
-abw
-abw
+aYC
+aYy
abv
afA
abv
@@ -46227,7 +46261,7 @@ avh
abm
abw
abw
-abw
+aYA
adM
afU
abI
diff --git a/tgui/packages/tgui/interfaces/AnomalyHarvester/Sample.tsx b/tgui/packages/tgui/interfaces/AnomalyHarvester/Sample.tsx
new file mode 100644
index 0000000000..95a6a5cef9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AnomalyHarvester/Sample.tsx
@@ -0,0 +1,62 @@
+import { useBackend } from 'tgui/backend';
+import { Box, Button, DmIcon, Section, Stack } from 'tgui-core/components';
+import { capitalize } from 'tgui-core/string';
+import type { Data } from './types';
+
+export const SampleDisplay = (props) => {
+ const { act, data } = useBackend();
+ const { samples } = data;
+
+ return (
+ act('release_all')}
+ >
+ Release All
+
+ }
+ >
+
+ {samples.length > 0 &&
+ samples.map((sample) => {
+ return (
+
+
+
+
+
+
+
+ {capitalize(sample.name)}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AnomalyHarvester/index.tsx b/tgui/packages/tgui/interfaces/AnomalyHarvester/index.tsx
new file mode 100644
index 0000000000..932222bc4d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AnomalyHarvester/index.tsx
@@ -0,0 +1,45 @@
+import { useBackend } from 'tgui/backend';
+import { Window } from 'tgui/layouts';
+import { LabeledList, ProgressBar, Stack } from 'tgui-core/components';
+import { capitalize } from 'tgui-core/string';
+import { SampleDisplay } from './Sample';
+import type { Data } from './types';
+
+export const AnomalyHarvester = (props) => {
+ const { act, data } = useBackend();
+ const { points, pointsToGenerate, name } = data;
+
+ const progress = (points / pointsToGenerate) * 100;
+ return (
+
+
+
+
+
+
+
+
+ {!!name && {capitalize(name)}}
+ {`${progress.toFixed()}%`}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AnomalyHarvester/types.ts b/tgui/packages/tgui/interfaces/AnomalyHarvester/types.ts
new file mode 100644
index 0000000000..bd4531143e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AnomalyHarvester/types.ts
@@ -0,0 +1,13 @@
+export type Data = {
+ name: string | null;
+ points: number;
+ pointsToGenerate: number;
+ samples: Sample[];
+};
+
+type Sample = {
+ name: string;
+ icon: string;
+ icon_state: string;
+ ref: string;
+};
diff --git a/tgui/packages/tgui/interfaces/AnomalyScanner.tsx b/tgui/packages/tgui/interfaces/AnomalyScanner.tsx
new file mode 100644
index 0000000000..fddc7bc0dc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AnomalyScanner.tsx
@@ -0,0 +1,104 @@
+import { useBackend } from 'tgui/backend';
+import { Window } from 'tgui/layouts';
+import { LabeledList, NoticeBox, Section, Stack } from 'tgui-core/components';
+import { round } from 'tgui-core/math';
+import { capitalizeAll } from 'tgui-core/string';
+
+type Data = {
+ anomaly_name: string;
+ severity: number;
+ stability: string;
+ point_output: number;
+ danger_type: string;
+ unstable_type: string;
+ containment_type: string;
+ transformation_type: string;
+ modifier?: string;
+ countdown: number;
+};
+
+const colorMap = {
+ Decaying: 'yellow',
+ Stable: 'green',
+} as const;
+
+function getColor(stability: string): string {
+ return colorMap[stability] ?? 'bad';
+}
+
+export const AnomalyScanner = (props) => {
+ const { data } = useBackend();
+
+ const {
+ anomaly_name,
+ severity,
+ stability,
+ point_output,
+ danger_type,
+ unstable_type,
+ containment_type,
+ transformation_type,
+ modifier,
+ countdown,
+ theme,
+ } = data;
+
+ return (
+
+
+ {anomaly_name ? (
+
+
+
+
+
+ {severity}%
+
+
+ {stability}
+
+
+ {round(point_output, 0)}
+
+
+ {round(countdown, 0)} seconds
+
+
+
+
+
+
+
+
+ {danger_type}
+
+
+ {unstable_type}
+
+
+ {containment_type}
+
+
+ {transformation_type}
+
+
+
+
+ {modifier && (
+
+
+
+ )}
+
+ ) : (
+ No anomaly scanned
+ )}
+
+
+ );
+};
diff --git a/vorestation.dme b/vorestation.dme
index dfc17497ee..bb39e1de6d 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -1637,6 +1637,8 @@
#include "code\game\objects\effects\anomalies\anomalies_weather_themes.dm"
#include "code\game\objects\effects\anomalies\anomaly_placer.dm"
#include "code\game\objects\effects\anomalies\anomaly_spawner.dm"
+#include "code\game\objects\effects\anomalies\anomaly_stats.dm"
+#include "code\game\objects\effects\anomalies\anomaly_stats_mods.dm"
#include "code\game\objects\effects\chem\chemsmoke.dm"
#include "code\game\objects\effects\chem\coating.dm"
#include "code\game\objects\effects\chem\foam.dm"
@@ -1721,7 +1723,6 @@
#include "code\game\objects\items\devices\advnifrepair.dm"
#include "code\game\objects\items\devices\ai_detector.dm"
#include "code\game\objects\items\devices\aicard.dm"
-#include "code\game\objects\items\devices\anomaly.dm"
#include "code\game\objects\items\devices\binoculars.dm"
#include "code\game\objects\items\devices\body_snatcher_vr.dm"
#include "code\game\objects\items\devices\chameleonproj.dm"
@@ -1893,6 +1894,7 @@
#include "code\game\objects\items\weapons\circuitboards\computer\shuttle.dm"
#include "code\game\objects\items\weapons\circuitboards\computer\supply.dm"
#include "code\game\objects\items\weapons\circuitboards\computer\telecomms.dm"
+#include "code\game\objects\items\weapons\circuitboards\machinery\anomaly_harvester.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\biogenerator.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\cloning.dm"
#include "code\game\objects\items\weapons\circuitboards\machinery\engineering.dm"
@@ -2377,6 +2379,8 @@
#include "code\modules\alarm\fire_alarm.dm"
#include "code\modules\alarm\motion_alarm.dm"
#include "code\modules\alarm\power_alarm.dm"
+#include "code\modules\anomalies\anomaly.dm"
+#include "code\modules\anomalies\anomaly_harvester.dm"
#include "code\modules\artifice\cursedform.dm"
#include "code\modules\artifice\deadringer.dm"
#include "code\modules\artifice\telecube.dm"