diff --git a/_maps/map_files/RandomRuins/SpaceRuins/alien_cache_site.dmm b/_maps/map_files/RandomRuins/SpaceRuins/alien_cache_site.dmm
new file mode 100644
index 00000000000..0154741ff81
--- /dev/null
+++ b/_maps/map_files/RandomRuins/SpaceRuins/alien_cache_site.dmm
@@ -0,0 +1,1195 @@
+//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE
+"cK" = (
+/obj/structure/closet,
+/obj/item/soap,
+/obj/item/soap,
+/obj/item/clothing/under/rank/cargo/expedition/skirt,
+/obj/item/clothing/under/rank/cargo/expedition/skirt,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"dV" = (
+/obj/structure/lightable/bonfire,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"ec" = (
+/obj/machinery/hydroponics/soil,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"en" = (
+/obj/structure/table/abductor,
+/obj/item/kitchen/knife,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"et" = (
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site/cache_room)
+"eF" = (
+/obj/effect/decal/cleanable/dirt,
+/obj/item/pickaxe,
+/obj/item/weldingtool/largetank,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"fA" = (
+/obj/structure/fans/tiny/invisible,
+/obj/machinery/door/airlock/abductor/secure,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"gf" = (
+/obj/effect/spawner/random/glowstick,
+/obj/structure/table/abductor,
+/obj/effect/spawner/random/engineering/tools,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"hj" = (
+/obj/effect/spawner/random/snacks,
+/obj/effect/spawner/random/snacks,
+/obj/effect/spawner/random/snacks,
+/obj/structure/table/wood,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"jZ" = (
+/obj/item/reagent_containers/glass/bottle/nutrient/empty,
+/obj/item/reagent_containers/glass/bucket,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"kY" = (
+/obj/effect/simon_says,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"ml" = (
+/obj/structure/table/abductor,
+/obj/item/reagent_containers/glass/bucket,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"mQ" = (
+/turf/space,
+/area/space)
+"ok" = (
+/obj/effect/spawner/random/food_or_drink,
+/obj/effect/spawner/random/food_or_drink,
+/obj/structure/table/wood,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"pX" = (
+/obj/item/assembly/igniter,
+/obj/effect/decal/cleanable/dirt,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"uD" = (
+/obj/structure/bed/abductor,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"wy" = (
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"zB" = (
+/obj/structure/closet,
+/obj/item/clothing/under/rank/cargo/expedition,
+/obj/item/clothing/under/rank/cargo/expedition,
+/obj/item/soap,
+/obj/item/reagent_containers/glass/bucket,
+/obj/item/reagent_containers/glass/bucket,
+/obj/item/reagent_containers/glass/bucket,
+/obj/item/gps{
+ gpstag = "ALN0";
+ tracking = 1
+ },
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"Bl" = (
+/obj/structure/reagent_dispensers/watertank,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"CK" = (
+/turf/simulated/wall/indestructible/abductor,
+/area/ruin/powered/alien_cache_site)
+"Do" = (
+/obj/machinery/shower/directional/east,
+/obj/structure/curtain/open/shower,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"Ey" = (
+/obj/machinery/alien_cache,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site/cache_room)
+"Ha" = (
+/turf/simulated/wall/indestructible/abductor,
+/area/ruin/powered/alien_cache_site/cache_room)
+"JC" = (
+/obj/structure/curtain/open,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"LK" = (
+/obj/effect/mapping_helpers/turfs/damage,
+/turf/simulated/wall/indestructible/abductor{
+ desc = "Effectively impervious to conventional methods of destruction. It looks like someone tried to break it, but failed"
+ },
+/area/ruin/powered/alien_cache_site)
+"TW" = (
+/turf/simulated/wall/mineral/wood,
+/area/ruin/powered/alien_cache_site)
+"Yg" = (
+/obj/machinery/door/airlock/abductor,
+/obj/structure/fans/tiny/invisible,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+"YA" = (
+/obj/effect/spawner/random/engineering/toolbox,
+/obj/structure/table/abductor,
+/turf/simulated/floor/plating/abductor,
+/area/ruin/powered/alien_cache_site)
+
+(1,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(2,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(3,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(4,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(5,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(6,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(7,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(8,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(9,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(10,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+CK
+CK
+CK
+CK
+CK
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(11,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+CK
+wy
+wy
+hj
+ok
+pX
+dV
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(12,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+gf
+wy
+wy
+wy
+wy
+wy
+wy
+ml
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(13,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+Ha
+Ha
+Ha
+Ha
+CK
+YA
+wy
+wy
+wy
+wy
+wy
+wy
+en
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(14,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+Ha
+Ha
+et
+et
+et
+Ha
+CK
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(15,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+et
+et
+et
+et
+et
+et
+LK
+eF
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(16,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+Ey
+et
+et
+et
+et
+et
+fA
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+kY
+wy
+Yg
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(17,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+et
+et
+et
+et
+et
+et
+CK
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+jZ
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(18,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+Ha
+Ha
+et
+et
+et
+Ha
+CK
+wy
+wy
+wy
+wy
+wy
+wy
+wy
+ec
+Bl
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(19,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+Ha
+Ha
+Ha
+Ha
+Ha
+CK
+TW
+TW
+JC
+TW
+JC
+TW
+TW
+TW
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(20,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+zB
+wy
+wy
+TW
+wy
+wy
+wy
+Do
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(21,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+CK
+uD
+uD
+TW
+uD
+uD
+cK
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(22,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+CK
+CK
+CK
+CK
+CK
+CK
+CK
+CK
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(23,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(24,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(25,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(26,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(27,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(28,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(29,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(30,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
+(31,1,1) = {"
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+mQ
+"}
diff --git a/code/__DEFINES/_math.dm b/code/__DEFINES/_math.dm
index 9e6a3e8be9a..4a6a54d626a 100644
--- a/code/__DEFINES/_math.dm
+++ b/code/__DEFINES/_math.dm
@@ -209,8 +209,11 @@
#define LORENTZ_DISTRIBUTION(x, s) ( s*tan(TODEGREES(PI*(rand()-0.5))) + x )
#define LORENTZ_CUMULATIVE_DISTRIBUTION(x, y, s) ( (1/PI)*TORADIANS(arctan((x-y)/s)) + 1/2 )
-
#define RULE_OF_THREE(a, b, x) ((a*x)/b)
+
+/// Returns probability based on deviation from the mean(m) and sigma(s)
+#define normal_distribution(x, m, s) ((1 / ((2 * PI * s**2)**0.5)) * NUM_E ** -(((x - m) ** 2) / (2 * s ** 2)))
+
// )
/proc/RaiseToPower(num, power)
diff --git a/code/__HELPERS/lists.dm b/code/__HELPERS/lists.dm
index 414926c99ca..d00977bfb74 100644
--- a/code/__HELPERS/lists.dm
+++ b/code/__HELPERS/lists.dm
@@ -293,6 +293,28 @@
return null
+/**
+ * Picks an element based on its weight. Weight can be any real number
+ * L - The input list
+ *
+ * example: list("a" = 0.33, "b" = 0.67) will have a 67% chance to pick "b"
+ */
+/proc/pickweight_fraction(list/L)
+ var/total = 0
+ var/item
+ for(item in L)
+ if(L[item] < 0)
+ continue
+ total += L[item]
+
+ total = rand() * total
+ for(item in L)
+ total -=L [item]
+ if(total <= 0)
+ return item
+
+ return null
+
//Pick a random element from the list and remove it from the list.
/proc/pick_n_take(list/listfrom)
if(length(listfrom) > 0)
diff --git a/code/datums/mapping/templates/space_map_templates.dm b/code/datums/mapping/templates/space_map_templates.dm
index e81ac14e3d6..4c32628448d 100644
--- a/code/datums/mapping/templates/space_map_templates.dm
+++ b/code/datums/mapping/templates/space_map_templates.dm
@@ -400,3 +400,11 @@
description = "Nanotrasen uses relays like these to further extend telecommunications around an area of space, as well as long range beacons for easier deployment in the future. \
Unfortunately, the anomalous activity around Epsilon Eridani, along with orbital debris and space-faring hostiles, has rendered many of these relay stations inoperable, leaving their communications and teleportation systems offline."
never_spawn_on_the_same_level = list("bluespace_relay_beacon")
+
+/datum/map_template/ruin/space/alien_cache_site
+ id = "alien_cache_site"
+ suffix = "alien_cache_site.dmm"
+ name = "Alien Cache Site"
+ description = "A site containing a cache of alien design, who knows what valuable technology is hiding inside?"
+ never_spawn_on_the_same_level = list("alien_cache_site")
+ allow_duplicates = FALSE
diff --git a/code/game/area/areas/ruins/alien_cache_site_areas.dm b/code/game/area/areas/ruins/alien_cache_site_areas.dm
new file mode 100644
index 00000000000..d1a492d3bbc
--- /dev/null
+++ b/code/game/area/areas/ruins/alien_cache_site_areas.dm
@@ -0,0 +1,7 @@
+/area/ruin/powered/alien_cache_site
+ name = "Alien Cache Site"
+ report_alerts = FALSE
+ dynamic_lighting = FALSE
+
+/area/ruin/powered/alien_cache_site/cache_room
+ name = "Alien Cache Chamber"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index d435c5bfc1e..893d04bb7af 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1510,3 +1510,38 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons)
attack_info.last_attacker_ckey = attacker.ckey
if(istype(weapon))
attack_info.last_attacker_weapon = "[weapon] ([weapon.type])"
+
+// MARK: PTL PROCS
+
+// Called when the target is selected
+/atom/proc/on_ptl_target(obj/machinery/power/transmission_laser/ptl)
+ if(ptl.firing)
+ on_ptl_fire()
+ return
+
+// Called for each process of the PTL
+/atom/proc/on_ptl_tick(obj/machinery/power/transmission_laser/ptl, output_level)
+ return
+
+// Called when no longer targeted by the ptl
+/atom/proc/on_ptl_untarget(obj/machinery/power/transmission_laser/ptl)
+ return
+
+// Called when the PTL starts firing on the target
+/atom/proc/on_ptl_fire(obj/machinery/power/transmission_laser/ptl)
+ return
+
+// Called when the PTL stops firing on the target
+/atom/proc/on_ptl_stop(obj/machinery/power/transmission_laser/ptl)
+ return
+
+// Used in the PTL ui
+/atom/proc/ptl_data()
+ return name
+
+// Called if an atom untargets itself
+/atom/proc/untarget_self(obj/machinery/power/transmission_laser/ptl)
+ on_ptl_untarget(ptl)
+ if(ptl)
+ ptl.target = null
+ return
diff --git a/code/game/objects/effects/spawners/random/alien_cache_random_spawners.dm b/code/game/objects/effects/spawners/random/alien_cache_random_spawners.dm
new file mode 100644
index 00000000000..b5e216b2aa5
--- /dev/null
+++ b/code/game/objects/effects/spawners/random/alien_cache_random_spawners.dm
@@ -0,0 +1,150 @@
+// Canister
+#define COMPRESSED_GAS_AMOUNT 300000
+#define AGENT_B_AMOUNT 100000
+#define NAME_O2 "O2"
+#define NAME_CO2 "CO2"
+#define NAME_N2 "N2"
+#define NAME_N2O "N2O"
+#define NAME_TOX "Plasma"
+#define NAME_AGENT_B "Agent B"
+// Lazarus Capsule
+#define MOB_TYPE (1 << 0)
+#define MOB_PROTOTYPE (1 << 1)
+
+// MARK: Random Spanwers
+
+/obj/effect/spawner/random/alien_cache
+ name = "alien cache loot spawner"
+
+// Random alien tools
+/obj/effect/spawner/random/alien_cache/alien_tool
+ name = "alien tool spawner"
+ loot = list(
+ /obj/item/screwdriver/abductor,
+ /obj/item/wrench/abductor,
+ /obj/item/multitool/abductor,
+ /obj/item/wirecutters/abductor,
+ /obj/item/crowbar/abductor,
+
+ )
+
+// Random alien surgical tools
+/obj/effect/spawner/random/alien_cache/alien_surgical_tool
+ name = "alien surgical tool spawner"
+ loot = list(
+ /obj/item/bonegel/alien,
+ /obj/item/scalpel/laser/alien,
+ /obj/item/bonesetter/alien,
+ /obj/item/circular_saw/alien,
+ /obj/item/hemostat/alien,
+ /obj/item/fix_o_vein/alien,
+ /obj/item/retractor/alien,
+ )
+
+// Random alien tool implant
+/obj/effect/spawner/random/alien_cache/alien_tool_implant
+ name = "alien tool implant spawner"
+ loot = list(
+ /obj/item/organ/internal/cyberimp/arm/toolset_abductor,
+ /obj/item/organ/internal/cyberimp/arm/toolset_abductor/l,
+ /obj/item/organ/internal/cyberimp/arm/janitorial_abductor,
+ /obj/item/organ/internal/cyberimp/arm/janitorial_abductor/l,
+ /obj/item/organ/internal/cyberimp/arm/surgical_abductor,
+ /obj/item/organ/internal/cyberimp/arm/surgical_abductor/l,
+ )
+
+// Random alien tool borg upgrade
+/obj/effect/spawner/random/alien_cache/alien_tool_borg
+ name = "alien borg tool upgrade"
+ loot = list(
+ /obj/item/borg/upgrade/abductor_engi,
+ /obj/item/borg/upgrade/abductor_medi,
+ /obj/item/borg/upgrade/abductor_jani,
+ )
+
+//MARK: Canister Spawners
+
+// Gas canister with random gas
+/obj/effect/spawner/alien_cache/gas_canister
+ name = "random gas canister spawner"
+ var/list/gasses = list(
+ NAME_O2 = COMPRESSED_GAS_AMOUNT,
+ NAME_N2 = COMPRESSED_GAS_AMOUNT,
+ NAME_CO2 = COMPRESSED_GAS_AMOUNT,
+ NAME_TOX = COMPRESSED_GAS_AMOUNT,
+ NAME_N2O = COMPRESSED_GAS_AMOUNT)
+
+// Agent B canister
+/obj/effect/spawner/alien_cache/gas_canister/agent_b
+ name = "agent b canister spawner"
+ gasses = list(NAME_AGENT_B = AGENT_B_AMOUNT)
+
+/obj/effect/spawner/alien_cache/gas_canister/proc/add_gas(obj/machinery/atmospherics/portable/canister/spawned)
+ var/picked = pick(gasses)
+ switch(picked)
+ if(NAME_O2)
+ spawned.air_contents.set_oxygen(gasses["[picked]"])
+ if(NAME_N2)
+ spawned.air_contents.set_nitrogen(gasses["[picked]"])
+ if(NAME_N2O)
+ spawned.air_contents.set_sleeping_agent(gasses["[picked]"])
+ if(NAME_CO2)
+ spawned.air_contents.set_carbon_dioxide(gasses["[picked]"])
+ if(NAME_TOX)
+ spawned.air_contents.set_toxins(gasses["[picked]"])
+ if(NAME_AGENT_B)
+ spawned.air_contents.set_agent_b(gasses["[picked]"])
+
+/obj/effect/spawner/alien_cache/gas_canister/Initialize(mapload)
+ . = ..()
+ var/obj/machinery/atmospherics/portable/canister/spawned = new(get_turf(src))
+ add_gas(spawned)
+ update_icon()
+ qdel(src)
+
+// MARK: Lazarus Spawners
+// Lazarus Capsule with a mob inside
+/obj/effect/spawner/alien_cache/mob_capsule
+ var/list/mob_types = list()
+
+// Lazarus Capsule with a megafauna inside
+/obj/effect/spawner/alien_cache/mob_capsule/megafauna
+ mob_types = list(/mob/living/simple_animal/hostile/megafauna = MOB_PROTOTYPE)
+
+/obj/effect/spawner/alien_cache/mob_capsule/Initialize(mapload)
+ . = ..()
+ var/obj/item/mobcapsule/capsule = new(get_turf(src))
+ var/type
+ if(length(mob_types))
+ var/list/pool = list()
+ for(var/mob_type in mob_types)
+ if(mob_types[mob_type] & MOB_TYPE)
+ pool += mob_type
+ if(mob_types[mob_type] & MOB_PROTOTYPE)
+ pool += subtypesof(mob_type)
+ type = pick(pool)
+ else
+ type = pick(subtypesof(/mob/living/simple_animal))
+ var/mob/living/simple_animal/my_mob = new type(loc)
+ my_mob.faction = list("neutral")
+ capsule.captured = my_mob
+ my_mob.forceMove(capsule)
+
+// TEG crate for the cache
+/obj/structure/closet/crate/teg
+
+/obj/structure/closet/crate/teg/populate_contents()
+ new /obj/machinery/power/teg(src)
+ new /obj/item/pipe/circulator(src)
+ new /obj/item/pipe/circulator(src)
+
+#undef COMPRESSED_GAS_AMOUNT
+#undef AGENT_B_AMOUNT
+#undef NAME_O2
+#undef NAME_CO2
+#undef NAME_N2
+#undef NAME_N2O
+#undef NAME_TOX
+#undef NAME_AGENT_B
+#undef MOB_TYPE
+#undef MOB_PROTOTYPE
diff --git a/code/goonstation/modules/power/power_transmission_laser/transmission_laser.dm b/code/goonstation/modules/power/power_transmission_laser/transmission_laser.dm
index 58a6a32f051..ec2eeda3ab3 100644
--- a/code/goonstation/modules/power/power_transmission_laser/transmission_laser.dm
+++ b/code/goonstation/modules/power/power_transmission_laser/transmission_laser.dm
@@ -71,10 +71,8 @@
/area/mine/outpost,
/area/shuttle/mining,
)
- /// Megafauna being targeted
- var/mob/living/simple_animal/hostile/megafauna/target
- /// Overlay that goes over the mob that gets beamed
- var/image/orbital_strike
+ /// PTL target
+ var/atom/target
/obj/machinery/power/transmission_laser/north
pixel_x = -64
@@ -276,7 +274,7 @@
data["accepting_power"] = turned_on
data["sucking_power"] = inputting
data["firing"] = firing
- data["target"] = target ? target.internal_gps.gpstag : ""
+ data["target"] = target ? target.ptl_data() : ""
data["power_format"] = power_format_multi
data["input_number"] = input_number
@@ -338,23 +336,24 @@
for(var/area_type in targetable_areas)
if(istype(boss_loc, area_type))
target_list[monster.internal_gps.gpstag] = monster
+ for(var/obj/machinery/power/laser_terminal/receptacle in GLOB.laser_terminals)
+ target_list["[receptacle.name]: [receptacle.id]"] = receptacle
// Target CC to sell power
target_list["Collection Terminal"] = null
var/choose = tgui_input_list(user, "Select target", "Target", target_list)
if(!choose)
return
+ untarget()
target = target_list[choose]
- RegisterSignal(target, COMSIG_MOB_DEATH, PROC_REF(untarget))
- if(firing && target)
- orbital_strike = image(target.icon, target, "orbital_strike", FLY_LAYER, SOUTH)
- target.add_overlay(orbital_strike)
+ if(target)
+ target.on_ptl_target(src)
/// Stop targeting a mob once it dies
/obj/machinery/power/transmission_laser/proc/untarget()
- SIGNAL_HANDLER
- target.cut_overlay(orbital_strike)
- UnregisterSignal(target, COMSIG_MOB_DEATH)
+ SIGNAL_HANDLER // This can be called via various signals depending on what we register on the a target
+ if(target)
+ target.on_ptl_untarget(src)
target = null
/obj/machinery/power/transmission_laser/process()
@@ -394,14 +393,8 @@
if(!target)
sell_power(output_level * WATT_TICK_TO_JOULE)
else
- if(!QDELETED(target)) // Just for safety.
- target.loot = list() // disable loot drops form the target to prevent cheese
- if(10 * output_level * target.damage_coeff[BURN] / (1 MW) > target.health) // If we would kill the target dust it.
- target.health = 0 // We need this so can_die() won't prevent dusting
- visible_message("\The [src] is reduced to dust by the beam!")
- target.dust()
- else
- target.adjustFireLoss(10 * output_level / (1 MW))
+ if(!QDELETED(target)) // Make sure our target still exists
+ INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, on_ptl_tick), src, output_level)
else
target = null
if(output_level > EYE_DAMAGE_THRESHOLD)
@@ -481,8 +474,7 @@
/obj/machinery/power/transmission_laser/proc/setup_lasers()
if(target)
- orbital_strike = image(target.icon, target, "orbital_strike", FLY_LAYER, SOUTH)
- target.add_overlay(orbital_strike)
+ target.on_ptl_fire()
var/turf/last_step = get_step(get_front_turf(), dir)
for(var/num in 1 to range)
if(!(locate(/obj/effect/transmission_beam) in last_step))
@@ -495,7 +487,7 @@
/obj/machinery/power/transmission_laser/proc/destroy_lasers()
if(target)
- target.cut_overlay(orbital_strike)
+ target.on_ptl_stop()
for(var/obj/effect/transmission_beam/listed_beam as anything in laser_effects)
laser_effects -= listed_beam
qdel(listed_beam)
diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 19c95eec056..475134099ef 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -439,12 +439,12 @@ GLOBAL_DATUM_INIT(canister_icon_container, /datum/canister_icons, new())
name = "Canister \[Air\]"
icon_state = "grey" //See Initialize()
can_label = FALSE
+
/obj/machinery/atmospherics/portable/canister/custom_mix
name = "Canister \[Custom\]"
icon_state = "whiters" //See Initialize()
can_label = FALSE
-
/obj/machinery/atmospherics/portable/canister/toxins/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
index eae50b6984e..dc9aa895a7a 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
@@ -193,6 +193,36 @@
difficulty_ore_modifier -= 4
enraged = FALSE
+// MARK: PTL Interaction
+/mob/living/simple_animal/hostile/megafauna/on_ptl_target(obj/machinery/power/transmission_laser/ptl)
+ ptl.RegisterSignal(src, COMSIG_MOB_DEATH, TYPE_PROC_REF(/obj/machinery/power/transmission_laser, untarget), ptl)
+ if(ptl?.firing)
+ on_ptl_fire(ptl)
+ return
+
+/mob/living/simple_animal/hostile/megafauna/on_ptl_tick(obj/machinery/power/transmission_laser/ptl, output_level)
+ loot = list() // disable loot drops form the target to prevent cheese
+ if(10 * output_level * damage_coeff[BURN] / (1 MW) > health) // If we would kill the target dust it.
+ health = 0 // We need this so can_die() won't prevent dusting
+ visible_message("\The [src] is reduced to dust by the beam!")
+ dust()
+ else
+ adjustFireLoss(10 * output_level / (1 MW))
+
+/mob/living/simple_animal/hostile/megafauna/on_ptl_untarget(obj/machinery/power/transmission_laser/ptl)
+ on_ptl_stop(ptl)
+ if(ptl)
+ ptl.UnregisterSignal(src, COMSIG_MOB_DEATH)
+
+/mob/living/simple_animal/hostile/megafauna/on_ptl_fire(obj/machinery/power/transmission_laser/ptl)
+ var/orbital_strike = image(icon, src, "orbital_strike", FLY_LAYER, SOUTH)
+ add_overlay(orbital_strike)
+
+/mob/living/simple_animal/hostile/megafauna/on_ptl_stop(obj/machinery/power/transmission_laser/ptl)
+ for(var/image/overlay in overlays)
+ if(overlay.name == "orbital_strike")
+ cut_overlay(overlay)
+
/mob/living/simple_animal/hostile/megafauna/DestroySurroundings()
. = ..()
for(var/turf/simulated/floor/chasm/C in circlerangeturfs(src, 1))
diff --git a/code/modules/power/alien_cache.dm b/code/modules/power/alien_cache.dm
new file mode 100644
index 00000000000..6e302053b06
--- /dev/null
+++ b/code/modules/power/alien_cache.dm
@@ -0,0 +1,259 @@
+#define CACHE_MAX_LEVEL 10
+#define LEVEL_DISTRIBUTION_SIGMA 0.5
+#define LEVEL_REQUIREMENT(level) ((300 MJ) * ((level) ** 3))
+
+/obj/machinery/alien_cache
+ name = "Alien Cache"
+ desc = "A strange storage device of alien design"
+ icon = 'icons/obj/machines/alien_cache.dmi'
+ icon_state = "cache_0"
+ base_icon_state = "cache_0"
+ pixel_x = -32
+ pixel_y = -32
+ density = TRUE
+ anchored = TRUE
+ // Alien stuff is pretty tough
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ /// The maximum reachable level
+ var/max_level = CACHE_MAX_LEVEL
+ /// The deepest level we opened
+ var/level_reached = 0
+ /// The total amount of energy consumed
+ var/total_energy = 0
+ /// Assoc list of level to rewards found at that level with a weight for each
+ var/list/random_rewards = list(
+ // level 1
+ list(
+ /obj/item/stack/sheet/mineral/abductor/fifty = 1,
+ /obj/machinery/the_singularitygen/tesla = 0.3,
+ /obj/machinery/the_singularitygen = 0.3,
+ /obj/item/stack/sheet/mineral/bananium/thirty = 1,
+ /obj/item/stack/sheet/mineral/tranquillite/thirty = 1,
+ ),
+ // level 2
+ list(
+ /obj/effect/spawner/random/alien_cache/alien_surgical_tool = 1,
+ /obj/item/mod/module/dispenser = 1,
+ /obj/effect/spawner/alien_cache/mob_capsule = 1,
+ /obj/item/storage/pill_bottle/random_meds/labelled = 1,
+ ),
+ // level 3
+ list(
+ /obj/item/mod/module/jetpack/advanced = 1,
+ /obj/effect/spawner/alien_cache/gas_canister = 1,
+ /obj/effect/spawner/random/alien_cache/alien_tool_borg = 1,
+ ),
+ // level 4
+ list(
+ /obj/effect/spawner/random/alien_cache/alien_tool = 1,
+ /obj/item/assembly/signaler/anomaly/random = 1,
+ /obj/effect/spawner/alien_cache/gas_canister/agent_b = 1,
+ ),
+ // level 5
+ list(
+ /obj/item/storage/belt/military/abductor/full = 1,
+ /obj/item/guardiancreator/biological = 1,
+ ),
+ )
+ /// List of guaranteed rewards you get from the last stage
+ var/list/open_rewards = list(
+ /obj/item/circuitboard/machine/bluespace_tap = 1,
+ )
+ /// Terminal for receiving power through
+ var/obj/machinery/power/terminal/terminal
+
+/obj/machinery/alien_cache/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/multitile, list(
+ list(1, MACH_CENTER, 1),
+ list(1, 0, 1),
+ ))
+
+/obj/machinery/alien_cache/update_overlays()
+ . = ..()
+ var/my_alpha = 0
+ for(var/cache_level in 1 to CACHE_MAX_LEVEL)
+ if(total_energy <= 0 || cache_level > level_reached + 1)
+ break
+ if(cache_level < level_reached + 1)
+ my_alpha = 255
+ if(cache_level == level_reached + 1)
+ my_alpha = clamp(1 - ((LEVEL_REQUIREMENT(cache_level) - total_energy) / (LEVEL_REQUIREMENT(cache_level + 1) - LEVEL_REQUIREMENT(cache_level))), 0, 1)
+ my_alpha *= 255
+ . += mutable_appearance(icon, "cache_level_[cache_level]", alpha = my_alpha)
+
+/obj/machinery/alien_cache/examine(mob/user)
+ . = ..()
+ if(panel_open)
+ . += "The panel is open, revealing unterminated internal wiring."
+ else
+ . += "There's a loose panel on the front that could be pried open with a screwdriver"
+
+/obj/machinery/alien_cache/display_parts(user)
+ return list("ERROR: UNIDENTIFIED MACHINE DESIGN")
+
+/// generate a list with weights for tiers of loot depending on the reached level
+/obj/machinery/alien_cache/proc/tier_weights()
+ var/list/tiers = list()
+ // Generate a weighted list of all possible tiers using normal distribution
+ var/avg_tier = (length(random_rewards) * level_reached / max_level)
+ for(var/i in 1 to length(random_rewards))
+ var/tier_prob = normal_distribution(i, avg_tier, LEVEL_DISTRIBUTION_SIGMA)
+ if(tier_prob > 0)
+ tiers["[i]"] = tier_prob
+
+ return tiers
+
+/// Returns a list of rewards with length equal to amount
+/obj/machinery/alien_cache/proc/pick_rewards(amount = 1)
+ . = list()
+ if(!amount || amount < 1)
+ return
+
+ var/list/tiers = tier_weights()
+
+ for(var/i in 1 to amount)
+ var/selected_level = text2num(pickweight_fraction(tiers))
+ if(length(random_rewards[selected_level]))
+ . += pickweight_fraction(random_rewards[selected_level])
+ else
+ . += "no rewards in level [selected_level]"
+
+/obj/machinery/alien_cache/process()
+ if(terminal && level_reached < max_level)
+ var/available = terminal.get_surplus()
+ terminal.consume_direct_power(available)
+ total_energy += available * WATT_TICK_TO_JOULE
+ if(total_energy >= LEVEL_REQUIREMENT(level_reached + 1))
+ level_reached++
+ var/list/rewards = pick_rewards(3)
+ to_chat(world, "Level: [level_reached]\nRewards: [english_list(rewards)]")
+ spawn_loot(rewards)
+ if(level_reached >= max_level)
+ spawn_loot(open_rewards)
+ update_icon(UPDATE_OVERLAYS)
+
+
+/obj/machinery/alien_cache/proc/spawn_loot(list/loot)
+ var/spawn_side = prob(50) ? WEST : EAST
+ if(dir & (EAST | WEST))
+ // We can turn EAST/WEST into NORTH/SOUTH simply by right shifting
+ spawn_side = spawn_side >> 2
+ var/turf/spawnloc = get_step(get_step(get_step(src, dir), dir), spawn_side)
+ new /obj/effect/portal(spawnloc, null, src, 10)
+ for(var/lootpath in loot)
+ new lootpath(spawnloc)
+
+/// Items interaction mostly stolen from SMES
+/obj/machinery/alien_cache/item_interaction(mob/living/user, obj/item/used, list/modifiers)
+ // Building and linking a terminal
+ if(istype(used, /obj/item/stack/cable_coil))
+ var/dir = get_dir(user, src)
+ if(dir & (dir - 1)) // Checks for diagonal interaction
+ return ITEM_INTERACT_COMPLETE
+
+ if(terminal) // Checks for an existing terminal
+ to_chat(user, "[src] already has a terminal!")
+ return ITEM_INTERACT_COMPLETE
+
+ if(!panel_open) // Checks to see if the panel is closed
+ to_chat(user, "You must open the panel first!")
+ return ITEM_INTERACT_COMPLETE
+
+ var/turf/T = get_turf(user)
+ if(T.intact) // Checks to see if floor plating is present
+ to_chat(user, "You must first remove the floor plating!")
+ return ITEM_INTERACT_COMPLETE
+
+ var/obj/item/stack/cable_coil/C = used
+ if(C.get_amount() < 10)
+ to_chat(user, "You need more wires.")
+ return ITEM_INTERACT_COMPLETE
+
+ if(user.loc == loc)
+ to_chat(user, "You must not be on the same tile as [src].")
+ return ITEM_INTERACT_COMPLETE
+
+ // Direction the terminal will face to
+ var/turf/temporary_location = get_step(src, REVERSE_DIR(dir))
+
+ if(isspaceturf(temporary_location))
+ to_chat(user, "You can't build a terminal on space.")
+ return ITEM_INTERACT_COMPLETE
+
+ else if(istype(temporary_location))
+ if(temporary_location.intact)
+ to_chat(user, "You must remove the floor plating first.")
+ return ITEM_INTERACT_COMPLETE
+
+ to_chat(user, "You try to connect the cable to [src]")
+ playsound(loc, C.usesound, 50, TRUE)
+
+ if(do_after(user, 5 SECONDS, target = get_turf(src)))
+ if(!terminal && panel_open)
+ T = get_turf(user)
+ var/obj/structure/cable/N = T.get_cable_node() //get the connecting node cable, if there's one
+ if(prob(50) && electrocute_mob(usr, N, N, 1, TRUE)) //animate the electrocution if uncautious and unlucky
+ do_sparks(5, TRUE, src)
+ return
+
+ C.use(10) // make sure the cable gets used up
+ user.visible_message(\
+ "[user.name] adds the cables and connects the power terminal.",\
+ "You add the cables and connect the power terminal.")
+
+ make_terminal(user, temporary_location)
+ terminal.connect_to_network()
+ stat &= ~BROKEN
+ return ITEM_INTERACT_COMPLETE
+
+ return ..()
+
+/obj/machinery/alien_cache/multitool_act(mob/living/user, obj/item/I)
+ . = TRUE
+ to_chat(user, chat_box_examine("Total energy: [DisplayJoules(total_energy)]
Levels opened: [level_reached]"))
+
+/obj/machinery/alien_cache/screwdriver_act(mob/living/user, obj/item/I)
+ // Opening using screwdriver
+ return default_deconstruction_screwdriver(user, "[icon_state]-o", initial(icon_state), I)
+
+/obj/machinery/alien_cache/wirecutter_act(mob/living/user, obj/item/I)
+ // No terminal to cut, just bonk
+ if(!terminal)
+ return FALSE
+ // If we have a terminal assume we are trying to disassemble it
+ . = TRUE
+ var/turf/T = get_turf(terminal)
+ if(T.intact) //is the floor plating removed ?
+ to_chat(user, "You must first expose the power terminal!")
+ return
+ if(!panel_open)
+ to_chat(user, "You must first open the panel")
+ to_chat(user, "You begin to dismantle the power terminal...")
+ playsound(src.loc, I.usesound, 50, TRUE)
+
+ if(do_after(user, 5 SECONDS * I.toolspeed, target = src))
+ if(terminal && panel_open)
+ if(prob(50) && electrocute_mob(usr, terminal.powernet, terminal, 1, TRUE)) // Animate the electrocution if uncautious and unlucky
+ do_sparks(5, TRUE, src)
+ return
+
+ // Returns wires on deletion of the terminal
+ new /obj/item/stack/cable_coil(T, 10)
+ user.visible_message(\
+ "[user.name] cuts the cables and dismantles the power terminal.",\
+ "You cut the cables and dismantle the power terminal.")
+ qdel(terminal)
+ return
+
+/// Terminal creation proc stolen from SMES
+/obj/machinery/alien_cache/proc/make_terminal(user, temporary_location)
+ // Create a terminal object at the same position as original turf loc
+ // Wires will attach to this
+ terminal = new /obj/machinery/power/terminal(temporary_location)
+ terminal.master = src
+ terminal.dir = REVERSE_DIR(dir)
+
+#undef CACHE_MAX_LEVEL
+#undef LEVEL_DISTRIBUTION_SIGMA
+#undef LEVEL_REQUIREMENT
diff --git a/code/modules/power/laser_terminal.dm b/code/modules/power/laser_terminal.dm
new file mode 100644
index 00000000000..c1ad00c2b13
--- /dev/null
+++ b/code/modules/power/laser_terminal.dm
@@ -0,0 +1,108 @@
+GLOBAL_LIST_EMPTY(laser_terminals)
+
+/obj/machinery/power/laser_terminal
+ name = "Laser Terminal"
+ desc = "A terminal designed to capture power sent by the power transmission laser"
+ icon = 'icons/obj/machines/ptl_terminal.dmi'
+ icon_state = "ptl_terminal_0"
+ base_icon_state = "ptl_terminal_0"
+ pixel_y = 10
+ density = TRUE
+ /// How much of the received energy we end up outputting to the grid
+ var/conversion_efficiency = 0.5
+ /// List of lasers targetting us
+ var/list/lasers = list()
+ /// The amount of power coming into the PTL
+ var/total_input
+ /// id of the terminal so you know which one you're targetting
+ var/id = 0
+
+/obj/item/circuitboard/machine/laser_terminal
+ board_name = "Laser Terminal"
+ icon_state = "engineering"
+ build_path = /obj/machinery/power/laser_terminal
+ origin_tech = "plasmatech=3; powerstorage=4"
+ req_components = list(
+ /obj/item/stack/cable_coil = 5,
+ /obj/item/stock_parts/capacitor = 2,
+ )
+
+/obj/machinery/power/laser_terminal/Initialize(mapload)
+ . = ..()
+ id = copytext(UID(), findlasttext(UID(), "_") + 1)
+ GLOB.laser_terminals += src
+ component_parts = list()
+ component_parts += new /obj/item/circuitboard/machine/laser_terminal(null)
+ component_parts += new /obj/item/stock_parts/capacitor(null)
+ component_parts += new /obj/item/stock_parts/capacitor(null)
+ component_parts += new /obj/item/stack/cable_coil(null, 5)
+ RefreshParts()
+ connect_to_network()
+
+/obj/machinery/power/laser_terminal/Destroy()
+ GLOB.laser_terminals -= src
+ . = ..()
+
+/obj/machinery/power/laser_terminal/RefreshParts()
+ var/cap_rating = 0
+ for(var/obj/item/stock_parts/capacitor/cap in component_parts)
+ cap_rating += cap.rating
+ // Goes from half to full conversion depending on capacitor level
+ conversion_efficiency = cap_rating / 8
+
+/obj/machinery/power/laser_terminal/on_ptl_target(obj/machinery/power/transmission_laser/ptl)
+ lasers |= ptl
+
+/obj/machinery/power/laser_terminal/on_ptl_untarget(obj/machinery/power/transmission_laser/ptl)
+ lasers -= ptl
+
+/obj/machinery/power/laser_terminal/update_overlays()
+ . = ..()
+ if(total_input > 0)
+ . += "ptl_terminal_graph"
+ . += "ptl_terminal_blinkers"
+ . += "ptl_terminal_light"
+ switch(total_input)
+ if(1 MW to 10 MW)
+ . += "ptl_terminal_bar_1"
+ if(10 MW to 20 MW)
+ . += "ptl_terminal_bar_2"
+ if(20 MW to 50 MW)
+ . += "ptl_terminal_bar_3"
+ if(50 MW to 100 MW)
+ . += "ptl_terminal_bar_4"
+ if(100 MW to 1 GW)
+ . += "ptl_terminal_bar_5"
+ if(1 GW to INFINITY)
+ . += "ptl_terminal_bar_6"
+
+/obj/machinery/power/laser_terminal/process()
+ var/tick_input = 0
+ for(var/obj/machinery/power/transmission_laser/ptl in lasers)
+ if(ptl.firing)
+ tick_input += ptl.output_level
+ // We use another var so we don't zero total input since it's also used for other things that could potentially happen mid-process
+ total_input = tick_input
+ produce_direct_power(total_input * conversion_efficiency)
+ update_icon(UPDATE_OVERLAYS)
+
+/obj/machinery/power/laser_terminal/screwdriver_act(mob/living/user, obj/item/I)
+ return default_deconstruction_screwdriver(user, initial(icon_state), initial(icon_state), I)
+
+/obj/machinery/power/laser_terminal/wrench_act(mob/living/user, obj/item/I)
+ . = TRUE
+ if(panel_open)
+ to_chat(user, "Close the maintenance panel first!")
+ return
+ . = default_unfasten_wrench(user, I, 2)
+ if(anchored)
+ connect_to_network()
+ else
+ disconnect_from_network()
+
+/obj/machinery/power/laser_terminal/crowbar_act(mob/living/user, obj/item/I)
+ return default_deconstruction_crowbar(user, I, FALSE)
+
+/obj/machinery/power/laser_terminal/multitool_act(mob/living/user, obj/item/I)
+ . = TRUE
+ to_chat(user, chat_box_examine("Unit ID: [id]
Input Power: [DisplayPower(total_input)]
Output Power: [DisplayPower(total_input * conversion_efficiency)]"))
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index 9957eba1f62..e548e7d3038 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -12,6 +12,16 @@
build_path = /obj/item/circuitboard/thermomachine
category = list ("Engineering Machinery")
+/datum/design/laser_terminal
+ name = "Machine Board (Laser Terminal)"
+ desc = "The circuit board for a PTL terminal."
+ id = "thermomachine"
+ req_tech = list("powerstorage" = 4, "plasmatech" = 3)
+ build_type = IMPRINTER
+ materials = list(MAT_GLASS = 1000)
+ build_path = /obj/item/circuitboard/machine/laser_terminal
+ category = list ("Engineering Machinery")
+
/datum/design/space_heater
name = "Machine Board (Space Heater)"
desc = "The circuit board for a space heater"
diff --git a/code/modules/ruins/objects_and_mobs/secure_alien_airlock.dm b/code/modules/ruins/objects_and_mobs/secure_alien_airlock.dm
new file mode 100644
index 00000000000..4b820846491
--- /dev/null
+++ b/code/modules/ruins/objects_and_mobs/secure_alien_airlock.dm
@@ -0,0 +1,15 @@
+/obj/machinery/door/airlock/abductor/secure
+ security_level = 6
+ autoclose = FALSE
+ flags = INDESTRUCTIBLE
+ locked = TRUE
+ req_access = list()
+
+/obj/machinery/door/airlock/abductor/secure/Initialize(mapload)
+ . = ..()
+ // Randomize the wires so they aren't the same as the station
+ wires.randomize()
+
+/// Always returns false since you aren't supposed to open it normally
+/obj/machinery/door/airlock/abductor/secure/check_access(obj/item/I)
+ return FALSE
diff --git a/code/modules/ruins/objects_and_mobs/simon_says.dm b/code/modules/ruins/objects_and_mobs/simon_says.dm
new file mode 100644
index 00000000000..18e535adef2
--- /dev/null
+++ b/code/modules/ruins/objects_and_mobs/simon_says.dm
@@ -0,0 +1,184 @@
+/obj/effect/simon_says
+ name = "strange platform"
+ desc = "A strange platform of alien design."
+ icon = 'icons/effects/simon_says_32x32.dmi'
+ icon_state = "pad"
+ var/fail_sound = 'sound/effects/simon_says_wrong.ogg'
+ var/success_sound = 'sound/effects/simon_says_success.ogg'
+ /// The chosen rhythm to play
+ var/list/rhythm = list()
+ /// List of all rhythms to choose from, made from lists of time from the start in deciseconds
+ var/static/list/rhythms = list(
+ // 7 Elevens are like the greatest thing
+ list(0, 7, 11, 14, 21, 22, 28, 33, 35, 42, 44, 49, 55, 56, 63, 66, 70),
+ list(0, 2.5, 7.5, 10, 15, 17.5, 22.5, 27.5, 30, 35, 40),
+ list(0, 8, 10, 16, 20, 24, 30, 32, 40),
+ list(0, 6, 10, 12, 18, 20, 24, 30),
+ list(0, 3, 6, 12, 18, 30, 33, 36, 39, 45, 51),
+ )
+ var/static/list/pad_colors = list(
+ "#b45e5e",
+ "#3c6474",
+ "#8a7d44",
+ "#b45e5e",
+ "#3c6474",
+ "#8a7d44",
+ )
+ var/static/list/pad_sounds = list(
+ 'sound/effects/simon_says_1.ogg',
+ 'sound/effects/simon_says_2.ogg',
+ 'sound/effects/simon_says_3.ogg',
+ 'sound/effects/simon_says_4.ogg',
+ 'sound/effects/simon_says_5.ogg',
+ 'sound/effects/simon_says_6.ogg',)
+ /// The time at which we should finish playing the sequence
+ var/playing_until = 0
+ /// Buttons pressed by the player so far
+ var/list/pressed_buttons = list()
+ /// The sequence we generated
+ var/list/sequence = list()
+ /// List of connected pads(normally generated on init)
+ var/list/pads = list()
+ /// current index in the sequence we check our pressed button against
+ var/current_index = 0
+ /// ID for autolinking an airlock on init
+ var/autolink_id
+ /// UID of the linked airlock
+ var/airlock_uid
+
+/obj/effect/simon_says/Initialize(mapload)
+ . = ..()
+ var/turf/place = get_turf(src)
+ place = locate(place.x, place.y + 4, place.z)
+ if(place.x >= world.maxx - 1 || place.x <= 2 || place.y >= world.maxy - 1 || place.y <= 2)
+ qdel(src)
+ return
+ var/id = 1
+ for(var/cardinal in list(NORTHWEST, NORTHEAST, EAST, SOUTHEAST, SOUTHWEST, WEST))
+ var/turf/curr_place = place
+ if(cardinal & (NORTH | SOUTH))
+ curr_place = get_step(place, cardinal & (NORTH | SOUTH))
+ else
+ curr_place = get_step(place, cardinal & (EAST | WEST))
+ pads += new /obj/effect/simon_says_pad(get_step(curr_place, cardinal), id++, src)
+
+/obj/effect/simon_says/Destroy()
+ for(var/obj/effect/simon_says_pad/pad in pads)
+ qdel(pad)
+ return ..()
+
+/obj/effect/simon_says/Cross(atom/movable/crossed_atom)
+ . = ..()
+ if(!ismob(crossed_atom) || (playing_until >= world.time))
+ return
+ play_music()
+
+/obj/effect/simon_says/proc/reset_buttons()
+ pressed_buttons = list()
+ current_index = 0
+
+/obj/effect/simon_says/proc/play_music()
+ if(!length(sequence))
+ generate_sequence()
+ playing_until = world.time + rhythm[length(rhythm)]
+ reset_buttons()
+ for(var/i in 1 to length(sequence))
+ var/obj/effect/simon_says_pad/curr_pad = pads[sequence[i]]
+ addtimer(CALLBACK(curr_pad, TYPE_PROC_REF(/obj/effect/simon_says_pad, play_note)), rhythm[i])
+ for(var/obj/machinery/door/airlock/to_link in range(10, get_turf(src)))
+ if(to_link.id_tag == autolink_id)
+ airlock_uid = to_link.UID()
+
+/obj/effect/simon_says/proc/generate_sequence()
+ sequence = list()
+ if(!length(rhythm))
+ rhythm = pick(rhythms)
+ var/list/pad_options = list()
+
+ // The reason we do all this instead of just rand(1, length(pads)) is so we can exclude pads that were played too recently
+ for(var/i in 1 to length(pads))
+ pad_options += list(list(i, -5))
+
+ for(var/beat in rhythm)
+ // Exclude pads that have played too recently
+ var/list/excluded = list()
+ for(var/option in pad_options)
+ if(beat - option[2] < 3)
+ excluded += list(option)
+ pad_options -= excluded
+ var/picked = pick(pad_options)
+ sequence += picked[1]
+ picked[2] = beat
+ pad_options += excluded
+
+/obj/effect/simon_says/proc/check_completion()
+ if(!sequence || !length(sequence))
+ return
+ if(sequence[current_index] != pressed_buttons[current_index])
+ do_failure()
+ return
+ if(current_index == length(sequence))
+ do_success()
+
+
+/obj/effect/simon_says/proc/play_sound(sound, blink_color)
+ animate_flash(src, blink_color, 2)
+ playsound(src, sound, 100, FALSE, falloff_exponent = SOUND_FALLOFF_EXPONENT / 4)
+
+/obj/effect/simon_says/proc/do_failure()
+ reset_buttons()
+ play_sound(fail_sound, "#ff1100")
+
+/obj/effect/simon_says/proc/do_success()
+ reset_buttons()
+ play_sound(success_sound, "#00e600")
+ var/obj/machinery/door/airlock/activated = locateUID(airlock_uid)
+ activated.airlock_cycle_callback("secure_open")
+
+
+/obj/effect/simon_says_pad
+ name = "strange platform"
+ desc = "A strange platform of alien design, it looks like something will happen if you stand on it"
+ icon = 'icons/effects/simon_says_32x32.dmi'
+ icon_state = "pad"
+ //pixel_x = -8
+ //pixel_y = -8
+ /// The simon says minigame we are a part of
+ var/obj/effect/simon_says/owner
+ /// Sound we make when playing
+ var/sound = 'sound/effects/simon_says_1.ogg'
+ /// ID used for comparing what we pressed against the sequence
+ var/id = 1
+ /// Time the pad was last played
+ var/last_played = 0
+
+/obj/effect/simon_says_pad/Initialize(mapload, _id, _owner)
+ . = ..()
+ owner = _owner
+ id = _id
+ if(owner)
+ color = owner.pad_colors[id]
+ sound = owner.pad_sounds[id]
+
+/obj/effect/simon_says_pad/proc/play_note()
+ last_played = world.time
+ var/list/hsl = rgb2num(color, COLORSPACE_HSL)
+ hsl[3] = hsl[3] + (100 - hsl[3]) * 0.7
+ var/target = rgb(hsl[1], hsl[2], hsl[3], space= COLORSPACE_HSL)
+ playsound(src, sound, 100, FALSE, falloff_exponent = SOUND_FALLOFF_EXPONENT / 4)
+ animate_flash(src, target, 1.5)
+
+/obj/effect/simon_says_pad/Cross(atom/movable/crossed_atom)
+ . = ..()
+ if(!ismob(crossed_atom) || (owner.playing_until >= world.time) || world.time < last_played + 3)
+ return
+ play_note()
+ owner.pressed_buttons += id
+ owner.current_index++
+ owner.check_completion()
+
+/proc/animate_flash(atom/to_flash , color, length)
+ var/my_color = to_flash.color
+ animate(to_flash, color = color, time = length, easing = SINE_EASING)
+ spawn(length)
+ animate(to_flash, color = my_color, time = length, easing = SINE_EASING)
diff --git a/config/example/config.toml b/config/example/config.toml
index 86230f38660..b2fe19e30eb 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -674,6 +674,7 @@ active_space_ruins = [
"_maps/map_files/RandomRuins/SpaceRuins/way_home.dmm",
"_maps/map_files/RandomRuins/SpaceRuins/wizardcrash.dmm",
"_maps/map_files/RandomRuins/SpaceRuins/wreckedcargoship.dmm",
+ "_maps/map_files/RandomRuins/SpaceRuins/alien_cache_site.dmm",
### The following ruins are based from past pre-spawned Zlevel content ###
"_maps/map_files/RandomRuins/SpaceRuins/abandonedtele.dmm",
diff --git a/icons/effects/simon_says_32x32.dmi b/icons/effects/simon_says_32x32.dmi
new file mode 100644
index 00000000000..a76856b5baf
Binary files /dev/null and b/icons/effects/simon_says_32x32.dmi differ
diff --git a/icons/obj/machines/alien_cache.dmi b/icons/obj/machines/alien_cache.dmi
new file mode 100644
index 00000000000..d3649e496e3
Binary files /dev/null and b/icons/obj/machines/alien_cache.dmi differ
diff --git a/icons/obj/machines/ptl_terminal.dmi b/icons/obj/machines/ptl_terminal.dmi
new file mode 100644
index 00000000000..1774edd1104
Binary files /dev/null and b/icons/obj/machines/ptl_terminal.dmi differ
diff --git a/paradise.dme b/paradise.dme
index d8d715bca1c..5d605c95be8 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -850,6 +850,7 @@
#include "code\game\area\shuttle_areas.dm"
#include "code\game\area\areas\depot-areas.dm"
#include "code\game\area\areas\mining_areas.dm"
+#include "code\game\area\areas\ruins\alien_cache_site_areas.dm"
#include "code\game\area\areas\ruins\blackmarketpackers.dm"
#include "code\game\area\areas\ruins\lavaland_areas.dm"
#include "code\game\area\areas\ruins\space_areas.dm"
@@ -1194,6 +1195,7 @@
#include "code\game\objects\effects\spawners\random_barrier.dm"
#include "code\game\objects\effects\spawners\vaultspawner.dm"
#include "code\game\objects\effects\spawners\windowspawner.dm"
+#include "code\game\objects\effects\spawners\random\alien_cache_random_spawners.dm"
#include "code\game\objects\effects\spawners\random\bluespace_tap_spawners.dm"
#include "code\game\objects\effects\spawners\random\decal_spawners.dm"
#include "code\game\objects\effects\spawners\random\engineering_spawners.dm"
@@ -2874,8 +2876,10 @@
#include "code\modules\pda\games\minesweeper.dm"
#include "code\modules\persistence\persistence.dm"
#include "code\modules\point\point.dm"
+#include "code\modules\power\alien_cache.dm"
#include "code\modules\power\cell.dm"
#include "code\modules\power\gravitygenerator.dm"
+#include "code\modules\power\laser_terminal.dm"
#include "code\modules\power\lights.dm"
#include "code\modules\power\power_machine.dm"
#include "code\modules\power\smes.dm"
@@ -3087,6 +3091,8 @@
#include "code\modules\ruins\objects_and_mobs\gym.dm"
#include "code\modules\ruins\objects_and_mobs\id_upgrader.dm"
#include "code\modules\ruins\objects_and_mobs\necropolis_gate.dm"
+#include "code\modules\ruins\objects_and_mobs\secure_alien_airlock.dm"
+#include "code\modules\ruins\objects_and_mobs\simon_says.dm"
#include "code\modules\ruins\spaceruin_code\voyager.dm"
#include "code\modules\security_levels\keycard_authentication.dm"
#include "code\modules\security_levels\security_level_datums.dm"
diff --git a/sound/effects/simon_says_1.ogg b/sound/effects/simon_says_1.ogg
new file mode 100644
index 00000000000..a5fce7c329a
Binary files /dev/null and b/sound/effects/simon_says_1.ogg differ
diff --git a/sound/effects/simon_says_2.ogg b/sound/effects/simon_says_2.ogg
new file mode 100644
index 00000000000..b223084c912
Binary files /dev/null and b/sound/effects/simon_says_2.ogg differ
diff --git a/sound/effects/simon_says_3.ogg b/sound/effects/simon_says_3.ogg
new file mode 100644
index 00000000000..aa54566cb4d
Binary files /dev/null and b/sound/effects/simon_says_3.ogg differ
diff --git a/sound/effects/simon_says_4.ogg b/sound/effects/simon_says_4.ogg
new file mode 100644
index 00000000000..bff11a3b6c2
Binary files /dev/null and b/sound/effects/simon_says_4.ogg differ
diff --git a/sound/effects/simon_says_5.ogg b/sound/effects/simon_says_5.ogg
new file mode 100644
index 00000000000..1ce4b870ce3
Binary files /dev/null and b/sound/effects/simon_says_5.ogg differ
diff --git a/sound/effects/simon_says_6.ogg b/sound/effects/simon_says_6.ogg
new file mode 100644
index 00000000000..9c482b8dd0b
Binary files /dev/null and b/sound/effects/simon_says_6.ogg differ
diff --git a/sound/effects/simon_says_success.ogg b/sound/effects/simon_says_success.ogg
new file mode 100644
index 00000000000..a50a32c0c3e
Binary files /dev/null and b/sound/effects/simon_says_success.ogg differ
diff --git a/sound/effects/simon_says_wrong.ogg b/sound/effects/simon_says_wrong.ogg
new file mode 100644
index 00000000000..e3943888dc8
Binary files /dev/null and b/sound/effects/simon_says_wrong.ogg differ