diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm
index c4933d5260..17bc605ae7 100644
--- a/code/_helpers/lists.dm
+++ b/code/_helpers/lists.dm
@@ -56,6 +56,68 @@ proc/isemptylist(list/list)
return 1
return 0
+//////////////////////////////////////////////////////
+// "typecache" utilities - Making and searching them
+//////////////////////////////////////////////////////
+
+//Checks for specific types in specifically structured (Assoc "type" = TRUE) lists ('typecaches')
+/proc/is_type_in_typecache(atom/A, list/L)
+ if(!LAZYLEN(L) || !A)
+ return FALSE
+ return L[A.type]
+
+//returns a new list with only atoms that are in typecache L
+/proc/typecache_filter_list(list/atoms, list/typecache)
+ . = list()
+ for(var/thing in atoms)
+ var/atom/A = thing
+ if(typecache[A.type])
+ . += A
+
+/proc/typecache_filter_list_reverse(list/atoms, list/typecache)
+ . = list()
+ for(var/thing in atoms)
+ var/atom/A = thing
+ if(!typecache[A.type])
+ . += A
+
+/proc/typecache_filter_multi_list_exclusion(list/atoms, list/typecache_include, list/typecache_exclude)
+ . = list()
+ for(var/thing in atoms)
+ var/atom/A = thing
+ if(typecache_include[A.type] && !typecache_exclude[A.type])
+ . += A
+
+//Like typesof() or subtypesof(), but returns a typecache instead of a list
+/proc/typecacheof(path, ignore_root_path, only_root_path = FALSE)
+ if(ispath(path))
+ var/list/types = list()
+ if(only_root_path)
+ types = list(path)
+ else
+ types = ignore_root_path ? subtypesof(path) : typesof(path)
+ var/list/L = list()
+ for(var/T in types)
+ L[T] = TRUE
+ return L
+ else if(islist(path))
+ var/list/pathlist = path
+ var/list/L = list()
+ if(ignore_root_path)
+ for(var/P in pathlist)
+ for(var/T in subtypesof(P))
+ L[T] = TRUE
+ else
+ for(var/P in pathlist)
+ if(only_root_path)
+ L[P] = TRUE
+ else
+ for(var/T in typesof(P))
+ L[T] = TRUE
+ return L
+
+//////////////////////////////////////////////////////
+
//Empties the list by setting the length to 0. Hopefully the elements get garbage collected
proc/clearlist(list/list)
if(istype(list))
diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm
index 5bf9a73954..e52cc857c6 100644
--- a/code/_helpers/unsorted.dm
+++ b/code/_helpers/unsorted.dm
@@ -1094,6 +1094,15 @@ var/global/list/common_tools = list(
return 1
return 0
+/proc/is_wire_tool(obj/item/I)
+ if(istype(I, /obj/item/device/multitool))
+ return TRUE
+ if(istype(I, /obj/item/weapon/wirecutters))
+ return TRUE
+ if(istype(I, /obj/item/device/assembly/signaler))
+ return TRUE
+ return
+
proc/is_hot(obj/item/W as obj)
switch(W.type)
if(/obj/item/weapon/weldingtool)
diff --git a/code/controllers/subsystems/orbits.dm b/code/controllers/subsystems/orbits.dm
new file mode 100644
index 0000000000..80c16c9ce5
--- /dev/null
+++ b/code/controllers/subsystems/orbits.dm
@@ -0,0 +1,44 @@
+SUBSYSTEM_DEF(orbit)
+ name = "Orbits"
+ priority = 8 // FIRE_PRIORITY_ORBIT
+ wait = 2
+ flags = SS_NO_INIT|SS_TICKER
+
+ var/list/currentrun = list()
+ var/list/processing = list()
+
+/datum/controller/subsystem/orbit/stat_entry()
+ ..("P:[processing.len]")
+
+
+/datum/controller/subsystem/orbit/fire(resumed = 0)
+ if (!resumed)
+ src.currentrun = processing.Copy()
+
+ //cache for sanic speed (lists are references anyways)
+ var/list/currentrun = src.currentrun
+
+ while (currentrun.len)
+ var/datum/orbit/O = currentrun[currentrun.len]
+ currentrun.len--
+ if (!O)
+ processing -= O
+ if (MC_TICK_CHECK)
+ return
+ continue
+ if (!O.orbiter)
+ qdel(O)
+ if (MC_TICK_CHECK)
+ return
+ continue
+ if (O.lastprocess >= world.time) //we already checked recently
+ if (MC_TICK_CHECK)
+ return
+ continue
+ var/targetloc = get_turf(O.orbiting)
+ if (targetloc != O.lastloc || O.orbiter.loc != targetloc)
+ O.Check(targetloc)
+ if (MC_TICK_CHECK)
+ return
+
+
diff --git a/code/datums/orbit.dm b/code/datums/orbit.dm
new file mode 100644
index 0000000000..5d457ff4ac
--- /dev/null
+++ b/code/datums/orbit.dm
@@ -0,0 +1,132 @@
+/datum/orbit
+ var/atom/movable/orbiter
+ var/atom/orbiting
+ var/lock = TRUE
+ var/turf/lastloc
+ var/lastprocess
+
+/datum/orbit/New(_orbiter, _orbiting, _lock)
+ orbiter = _orbiter
+ orbiting = _orbiting
+ SSorbit.processing += src
+ if (!orbiting.orbiters)
+ orbiting.orbiters = list()
+ orbiting.orbiters += src
+
+ if (orbiter.orbiting)
+ orbiter.stop_orbit()
+ orbiter.orbiting = src
+ Check()
+ lock = _lock
+
+//do not qdel directly, use stop_orbit on the orbiter. (This way the orbiter can bind to the orbit stopping)
+/datum/orbit/Destroy(force = FALSE)
+ SSorbit.processing -= src
+ if (orbiter)
+ orbiter.orbiting = null
+ orbiter = null
+ if (orbiting)
+ if (orbiting.orbiters)
+ orbiting.orbiters -= src
+ if (!orbiting.orbiters.len)//we are the last orbit, delete the list
+ orbiting.orbiters = null
+ orbiting = null
+ return ..()
+
+/datum/orbit/proc/Check(turf/targetloc, list/checked_already = list())
+ //Avoid infinite loops for people who end up orbiting themself through another orbiter
+ checked_already[src] = TRUE
+ if (!orbiter)
+ qdel(src)
+ return
+ if (!orbiting)
+ orbiter.stop_orbit()
+ return
+ if (!orbiter.orbiting) //admin wants to stop the orbit.
+ orbiter.orbiting = src //set it back to us first
+ orbiter.stop_orbit()
+ var/atom/movable/AM = orbiting
+ if(istype(AM) && AM.orbiting && AM.orbiting.orbiting == orbiter)
+ orbiter.stop_orbit()
+ return
+ lastprocess = world.time
+ if (!targetloc)
+ targetloc = get_turf(orbiting)
+ if (!targetloc || (!lock && orbiter.loc != lastloc && orbiter.loc != targetloc))
+ orbiter.stop_orbit()
+ return
+ orbiter.loc = targetloc
+ //TODO-LESH-DEL orbiter.update_parallax_contents()
+ orbiter.update_light()
+ lastloc = orbiter.loc
+ for(var/other_orbit in orbiter.orbiters)
+ var/datum/orbit/OO = other_orbit
+ //Skip if checked already
+ if(checked_already[OO])
+ continue
+ OO.Check(targetloc, checked_already)
+
+/atom/movable/var/datum/orbit/orbiting = null
+/atom/var/list/orbiters = null
+
+//A: atom to orbit
+//radius: range to orbit at, radius of the circle formed by orbiting (in pixels)
+//clockwise: whether you orbit clockwise or anti clockwise
+//rotation_speed: how fast to rotate (how many ds should it take for a rotation to complete)
+//rotation_segments: the resolution of the orbit circle, less = a more block circle, this can be used to produce hexagons (6 segments) triangles (3 segments), and so on, 36 is the best default.
+//pre_rotation: Chooses to rotate src 90 degress towards the orbit dir (clockwise/anticlockwise), useful for things to go "head first" like ghosts
+//lockinorbit: Forces src to always be on A's turf, otherwise the orbit cancels when src gets too far away (eg: ghosts)
+
+/atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE, lockinorbit = FALSE)
+ if (!istype(A))
+ return
+
+ new/datum/orbit(src, A, lockinorbit)
+ if (!orbiting) //something failed, and our orbit datum deleted itself
+ return
+ var/matrix/initial_transform = matrix(transform)
+
+ //Head first!
+ if (pre_rotation)
+ var/matrix/M = matrix(transform)
+ var/pre_rot = 90
+ if(!clockwise)
+ pre_rot = -90
+ M.Turn(pre_rot)
+ transform = M
+
+ var/matrix/shift = matrix(transform)
+ shift.Translate(0,radius)
+ transform = shift
+
+ SpinAnimation(rotation_speed, -1, clockwise, rotation_segments)
+
+ //we stack the orbits up client side, so we can assign this back to normal server side without it breaking the orbit
+ transform = initial_transform
+
+/atom/movable/proc/stop_orbit()
+ SpinAnimation(0,0)
+ qdel(orbiting)
+
+/atom/Destroy(force = FALSE)
+ . = ..()
+ if (orbiters)
+ for (var/thing in orbiters)
+ var/datum/orbit/O = thing
+ if (O.orbiter)
+ O.orbiter.stop_orbit()
+
+/atom/movable/Destroy(force = FALSE)
+ . = ..()
+ if (orbiting)
+ stop_orbit()
+
+/*
+/atom/movable/proc/transfer_observers_to(atom/movable/target)
+ if(orbiters)
+ for(var/thing in orbiters)
+ var/datum/orbit/O = thing
+ if(O.orbiter && isobserver(O.orbiter))
+ var/mob/dead/observer/D = O.orbiter
+ D.ManualFollow(target)
+*/
diff --git a/code/datums/wires/tesla_coil.dm b/code/datums/wires/tesla_coil.dm
new file mode 100644
index 0000000000..f176b8f139
--- /dev/null
+++ b/code/datums/wires/tesla_coil.dm
@@ -0,0 +1,18 @@
+/datum/wires/tesla_coil
+ wire_count = 1
+ holder_type = /obj/machinery/power/tesla_coil
+
+var/const/WIRE_ZAP = 1
+
+/datum/wires/tesla_coil/CanUse(mob/living/L)
+ var/obj/machinery/power/tesla_coil/T = holder
+ if(T && T.panel_open)
+ return 1
+ return 0
+
+/datum/wires/tesla_coil/UpdatePulsed(index)
+ var/obj/machinery/power/tesla_coil/T = holder
+ switch(index)
+ if(WIRE_ZAP)
+ T.zap()
+ ..()
diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm
index 7e867af9a7..2e881f6583 100644
--- a/code/game/machinery/machinery.dm
+++ b/code/game/machinery/machinery.dm
@@ -317,6 +317,27 @@ Class Procs:
user << " [C.name]"
return 1
+// Default behavior for wrenching down machines. Supports both delay and instant modes.
+/obj/machinery/proc/default_unfasten_wrench(var/mob/user, var/obj/item/weapon/wrench/W, var/time = 0)
+ if(!istype(W))
+ return FALSE
+ if(panel_open)
+ return FALSE // Close panel first!
+ playsound(loc, W.usesound, 50, 1)
+ var/actual_time = W.toolspeed * time
+ if(actual_time != 0)
+ user.visible_message( \
+ "\The [user] begins [anchored ? "un" : ""]securing \the [src].", \
+ "You start [anchored ? "un" : ""]securing \the [src].")
+ if(actual_time == 0 || do_after(user, actual_time, target = src))
+ user.visible_message( \
+ "\The [user] has [anchored ? "un" : ""]secured \the [src].", \
+ "You [anchored ? "un" : ""]secure \the [src].")
+ anchored = !anchored
+ power_change() //Turn on or off the machine depending on the status of power in the new area.
+ update_icon()
+ return TRUE
+
/obj/machinery/proc/default_deconstruction_crowbar(var/mob/user, var/obj/item/weapon/crowbar/C)
if(!istype(C))
return 0
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 11b61d0a1a..8ce3c2d266 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -517,11 +517,15 @@
E.state = 2
E.connect_to_network()
E.active = TRUE
-
for(var/obj/machinery/field_generator/F in world)
if(istype(get_area(F), /area/space))
F.Varedit_start = 1
-
+ for(var/obj/machinery/power/grounding_rod/GR in world)
+ GR.anchored = TRUE
+ GR.update_icon()
+ for(var/obj/machinery/power/tesla_coil/TC in world)
+ TC.anchored = TRUE
+ TC.update_icon()
for(var/obj/structure/particle_accelerator/PA in world)
PA.anchored = TRUE
PA.construction_state = 3
@@ -531,25 +535,6 @@
PA.construction_state = 3
PA.update_icon()
- spawn(30)
- for(var/obj/machinery/the_singularitygen/G in world)
- if(G.anchored)
- var/obj/singularity/S = new /obj/singularity(get_turf(G), 50)
- spawn(0)
- qdel(G)
- S.energy = 1750
- S.current_size = 7
- S.icon = 'icons/effects/224x224.dmi'
- S.icon_state = "singularity_s7"
- S.pixel_x = -96
- S.pixel_y = -96
- S.grav_pull = 0
- //S.consume_range = 3
- S.dissipate = 0
- //S.dissipate_delay = 10
- //S.dissipate_track = 0
- //S.dissipate_strength = 10
-
for(var/obj/machinery/power/rad_collector/Rad in world)
if(Rad.anchored)
if(!Rad.P)
diff --git a/code/modules/power/power.dm b/code/modules/power/power.dm
index ba833d858f..112f5a482e 100644
--- a/code/modules/power/power.dm
+++ b/code/modules/power/power.dm
@@ -138,6 +138,14 @@
..()
return
+// Power machinery should also connect/disconnect from the network.
+/obj/machinery/power/default_unfasten_wrench(var/mob/user, var/obj/item/weapon/wrench/W, var/time = 20)
+ if((. = ..()))
+ if(anchored)
+ connect_to_network()
+ else
+ disconnect_from_network()
+
// Used for power spikes by the engine, has specific effects on different machines.
/obj/machinery/power/proc/overload(var/obj/machinery/power/source)
return
diff --git a/code/modules/power/singularity/generator.dm b/code/modules/power/singularity/generator.dm
index 5de10c12b9..2f9bc83cf4 100644
--- a/code/modules/power/singularity/generator.dm
+++ b/code/modules/power/singularity/generator.dm
@@ -8,11 +8,12 @@
density = 1
use_power = 0
var/energy = 0
+ var/creation_type = /obj/singularity
/obj/machinery/the_singularitygen/process()
var/turf/T = get_turf(src)
if(src.energy >= 200)
- new /obj/singularity/(T, 50)
+ new creation_type(T, 50)
if(src) qdel(src)
/obj/machinery/the_singularitygen/attackby(obj/item/W, mob/user)
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 46f7102f4c..246dbba6c7 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -29,15 +29,11 @@
var/chained = 0//Adminbus chain-grab
-/obj/singularity/New(loc, var/starting_energy = 50, var/temp = 0)
+/obj/singularity/New(loc, var/starting_energy = 50)
//CARN: admin-alert for chuckle-fuckery.
admin_investigate_setup()
energy = starting_energy
- if (temp)
- spawn (temp)
- qdel(src)
-
..()
processing_objects += src
for(var/obj/machinery/power/singularity_beacon/singubeacon in machines)
diff --git a/code/modules/power/tesla/coil.dm b/code/modules/power/tesla/coil.dm
new file mode 100644
index 0000000000..07dedf5b2b
--- /dev/null
+++ b/code/modules/power/tesla/coil.dm
@@ -0,0 +1,133 @@
+/obj/machinery/power/tesla_coil
+ name = "tesla coil"
+ desc = "For the union!"
+ icon = 'icons/obj/tesla_engine/tesla_coil.dmi'
+ icon_state = "coil0"
+ anchored = FALSE
+ density = TRUE
+
+ // Executing a traitor caught releasing tesla was never this fun!
+ can_buckle = TRUE
+ buckle_lying = FALSE
+
+ circuit = /obj/item/weapon/circuitboard/tesla_coil
+
+ var/power_loss = 2
+ var/input_power_multiplier = 1
+ var/zap_cooldown = 100
+ var/last_zap = 0
+ var/datum/wires/tesla_coil/wires = null
+
+/obj/machinery/power/tesla_coil/New()
+ ..()
+ wires = new(src)
+
+/obj/machinery/power/tesla_coil/initialize()
+ . = ..()
+ default_apply_parts()
+
+/obj/machinery/power/tesla_coil/Destroy()
+ qdel_null(wires)
+ return ..()
+
+/obj/machinery/power/tesla_coil/RefreshParts()
+ var/power_multiplier = 0
+ zap_cooldown = 100
+ for(var/obj/item/weapon/stock_parts/capacitor/C in component_parts)
+ power_multiplier += C.rating
+ zap_cooldown -= (C.rating * 20)
+ input_power_multiplier = power_multiplier
+
+
+/obj/machinery/power/tesla_coil/update_icon()
+ if(panel_open)
+ icon_state = "coil_open[anchored]"
+ else
+ icon_state = "coil[anchored]"
+
+/obj/machinery/power/tesla_coil/attackby(obj/item/W, mob/user, params)
+ src.add_fingerprint(user)
+
+ //if(default_deconstruction_screwdriver(user, "coil_open[anchored]", "coil[anchored]", W))
+ if(default_deconstruction_screwdriver(user, W))
+ return
+ if(default_part_replacement(user, W))
+ return
+ if(default_unfasten_wrench(user, W))
+ return
+ if(default_deconstruction_crowbar(user, W))
+ return
+ if(is_wire_tool(W))
+ return wires.Interact(user)
+ return ..()
+
+/obj/machinery/power/tesla_coil/attack_hand(mob/user)
+ if(user.a_intent == I_GRAB && user_buckle_mob(user.pulling, user))
+ return
+ ..()
+
+/obj/machinery/power/tesla_coil/tesla_act(var/power)
+ if(anchored && !panel_open)
+ being_shocked = TRUE
+ //don't lose arc power when it's not connected to anything
+ //please place tesla coils all around the station to maximize effectiveness
+ var/power_produced = powernet ? power / power_loss : power
+ add_avail(power_produced*input_power_multiplier)
+ flick("coilhit", src)
+ playsound(src.loc, 'sound/effects/lightningshock.ogg', 100, 1, extrarange = 5)
+ tesla_zap(src, 5, power_produced)
+ //addtimer(CALLBACK(src, .proc/reset_shocked), 10)
+ spawn(10) reset_shocked()
+ else
+ ..()
+
+/obj/machinery/power/tesla_coil/proc/zap()
+ if((last_zap + zap_cooldown) > world.time || !powernet)
+ return FALSE
+ last_zap = world.time
+ var/coeff = (20 - ((input_power_multiplier - 1) * 3))
+ coeff = max(coeff, 10)
+ var/power = (powernet.avail/2)
+ draw_power(power)
+ playsound(src.loc, 'sound/effects/lightningshock.ogg', 100, 1, extrarange = 5)
+ tesla_zap(src, 10, power/(coeff/2))
+
+/obj/machinery/power/grounding_rod
+ name = "grounding rod"
+ desc = "Keep an area from being fried from Edison's Bane."
+ icon = 'icons/obj/tesla_engine/tesla_coil.dmi'
+ icon_state = "grounding_rod0"
+ anchored = FALSE
+ density = TRUE
+
+ can_buckle = TRUE
+ buckle_lying = FALSE
+
+/obj/machinery/power/grounding_rod/update_icon()
+ if(panel_open)
+ icon_state = "grounding_rod_open[anchored]"
+ else
+ icon_state = "grounding_rod[anchored]"
+
+/obj/machinery/power/grounding_rod/attackby(obj/item/W, mob/user, params)
+ //if(default_deconstruction_screwdriver(user, "grounding_rod_open[anchored]", "grounding_rod[anchored]", W))
+ if(default_deconstruction_screwdriver(user, W))
+ return
+ if(default_part_replacement(user, W))
+ return
+ if(default_unfasten_wrench(user, W))
+ return
+ if(default_deconstruction_crowbar(user, W))
+ return
+ return ..()
+
+/obj/machinery/power/grounding_rod/attack_hand(mob/user)
+ if(user.a_intent == I_GRAB && user_buckle_mob(user.pulling, user))
+ return
+ ..()
+
+/obj/machinery/power/grounding_rod/tesla_act(var/power)
+ if(anchored && !panel_open)
+ flick("grounding_rodhit", src)
+ else
+ ..()
diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm
new file mode 100644
index 0000000000..9b89e35b77
--- /dev/null
+++ b/code/modules/power/tesla/energy_ball.dm
@@ -0,0 +1,298 @@
+#define TESLA_DEFAULT_POWER 1738260
+#define TESLA_MINI_POWER 869130
+
+/obj/singularity/energy_ball
+ name = "energy ball"
+ desc = "An energy ball."
+ icon = 'icons/obj/tesla_engine/energy_ball.dmi'
+ icon_state = "energy_ball"
+ pixel_x = -32
+ pixel_y = -32
+ current_size = STAGE_TWO
+ move_self = 1
+ grav_pull = 0
+ contained = 0
+ density = TRUE
+ energy = 0
+ dissipate = 1
+ dissipate_delay = 5
+ dissipate_strength = 1
+ var/list/orbiting_balls = list()
+ var/miniball = FALSE
+ var/produced_power
+ var/energy_to_raise = 32
+ var/energy_to_lower = -20
+
+/obj/singularity/energy_ball/New(loc, starting_energy = 50, is_miniball = FALSE)
+ ..()
+ miniball = is_miniball
+
+/obj/singularity/energy_ball/initialize()
+ . = ..()
+ if(!miniball)
+ set_light(10, 7, "#EEEEFF")
+
+/obj/singularity/energy_ball/ex_act(severity, target)
+ return
+
+/obj/singularity/energy_ball/Destroy()
+ if(orbiting && istype(orbiting.orbiting, /obj/singularity/energy_ball))
+ var/obj/singularity/energy_ball/EB = orbiting.orbiting
+ EB.orbiting_balls -= src
+
+ for(var/ball in orbiting_balls)
+ var/obj/singularity/energy_ball/EB = ball
+ qdel(EB)
+
+ . = ..()
+
+/obj/singularity/energy_ball/admin_investigate_setup()
+ if(miniball)
+ return //don't annnounce miniballs
+ ..()
+
+
+/obj/singularity/energy_ball/process(var/wait = 20)
+ set waitfor = FALSE
+ if(!orbiting)
+ handle_energy()
+
+ move_the_basket_ball(max(wait - 5, 4 + orbiting_balls.len * 1.5))
+
+ playsound(src.loc, 'sound/effects/lightningbolt.ogg', 100, 1, extrarange = 30)
+
+ set_dir(tesla_zap(src.loc, 7, TESLA_DEFAULT_POWER, TRUE))
+
+ for (var/ball in orbiting_balls)
+ var/range = rand(1, Clamp(orbiting_balls.len, 3, 7))
+ tesla_zap(ball, range, TESLA_MINI_POWER/7*range, TRUE)
+ else
+ energy = 0 // ensure we dont have miniballs of miniballs
+
+/obj/singularity/energy_ball/examine(mob/user)
+ ..()
+ if(orbiting_balls.len)
+ to_chat(user, "The amount of orbiting mini-balls is [orbiting_balls.len].")
+
+
+/obj/singularity/energy_ball/proc/move_the_basket_ball(var/move_amount)
+ //we face the last thing we zapped, so this lets us favor that direction a bit
+ var/move_bias = dir
+ for(var/i in 0 to move_amount)
+ var/move_dir = pick(global.alldirs + move_bias) //ensures large-ball teslas don't just sit around
+ if(target && prob(10))
+ move_dir = get_dir(src,target)
+ var/turf/T = get_step(src, move_dir)
+ if(can_move(T))
+ forceMove(T)
+ set_dir(move_dir)
+ for(var/mob/living/carbon/C in loc)
+ dust_mobs(C)
+ sleep(1) // So movement is smooth
+
+/obj/singularity/energy_ball/proc/handle_energy()
+ if(energy >= energy_to_raise)
+ energy_to_lower = energy_to_raise - 20
+ energy_to_raise = energy_to_raise * 1.25
+
+ playsound(src.loc, 'sound/effects/lightning_chargeup.ogg', 100, 1, extrarange = 30)
+ //addtimer(CALLBACK(src, .proc/new_mini_ball), 100)
+ spawn(100) new_mini_ball()
+
+ else if(energy < energy_to_lower && orbiting_balls.len)
+ energy_to_raise = energy_to_raise / 1.25
+ energy_to_lower = (energy_to_raise / 1.25) - 20
+
+ var/Orchiectomy_target = pick(orbiting_balls)
+ qdel(Orchiectomy_target)
+
+ else if(orbiting_balls.len)
+ dissipate() //sing code has a much better system.
+
+/obj/singularity/energy_ball/proc/new_mini_ball()
+ if(!loc)
+ return
+ var/obj/singularity/energy_ball/EB = new(loc, 0, TRUE)
+
+ EB.transform *= pick(0.3, 0.4, 0.5, 0.6, 0.7)
+ var/icon/I = icon(icon,icon_state,dir)
+
+ var/orbitsize = (I.Width() + I.Height()) * pick(0.4, 0.5, 0.6, 0.7, 0.8)
+ orbitsize -= (orbitsize / world.icon_size) * (world.icon_size * 0.25)
+
+ EB.orbit(src, orbitsize, pick(FALSE, TRUE), rand(10, 25), pick(3, 4, 5, 6, 36))
+
+
+/obj/singularity/energy_ball/Bump(atom/A)
+ dust_mobs(A)
+
+/obj/singularity/energy_ball/Bumped(atom/movable/AM)
+ dust_mobs(AM)
+
+/obj/singularity/energy_ball/orbit(obj/singularity/energy_ball/target)
+ if (istype(target))
+ target.orbiting_balls += src
+ //TODO-LESH-DEL global.poi_list -= src
+ target.dissipate_strength = target.orbiting_balls.len
+
+ . = ..()
+/obj/singularity/energy_ball/stop_orbit()
+ if (orbiting && istype(orbiting.orbiting, /obj/singularity/energy_ball))
+ var/obj/singularity/energy_ball/orbitingball = orbiting.orbiting
+ orbitingball.orbiting_balls -= src
+ orbitingball.dissipate_strength = orbitingball.orbiting_balls.len
+ ..()
+ if (!loc && !QDELETED(src))
+ qdel(src)
+
+
+/obj/singularity/energy_ball/proc/dust_mobs(atom/A)
+ if(isliving(A))
+ var/mob/living/L = A
+ if(L.incorporeal_move)
+ return
+ if(!iscarbon(A))
+ return
+ for(var/obj/machinery/power/grounding_rod/GR in orange(src, 2))
+ if(GR.anchored)
+ return
+ var/mob/living/carbon/C = A
+ // C.dust() - Changing to do fatal elecrocution instead
+ C.electrocute_act(500, src, def_zone = BP_TORSO)
+
+/proc/tesla_zap(atom/source, zap_range = 3, power, explosive = FALSE, stun_mobs = TRUE)
+ . = source.dir
+ if(power < 1000)
+ return
+
+ var/closest_dist = 0
+ var/closest_atom
+ var/obj/machinery/power/tesla_coil/closest_tesla_coil
+ var/obj/machinery/power/grounding_rod/closest_grounding_rod
+ var/mob/living/closest_mob
+ var/obj/machinery/closest_machine
+ var/obj/structure/closest_structure
+ var/obj/structure/blob/closest_blob
+ var/static/things_to_shock = typecacheof(list(/obj/machinery, /mob/living, /obj/structure))
+ var/static/blacklisted_tesla_types = typecacheof(list(
+ /obj/machinery/atmospherics,
+ /obj/machinery/power/emitter,
+ /obj/machinery/field_generator,
+ /mob/living/simple_animal,
+ /obj/machinery/door/blast,
+ /obj/machinery/particle_accelerator/control_box,
+ /obj/structure/particle_accelerator/fuel_chamber,
+ /obj/structure/particle_accelerator/particle_emitter/center,
+ /obj/structure/particle_accelerator/particle_emitter/left,
+ /obj/structure/particle_accelerator/particle_emitter/right,
+ /obj/structure/particle_accelerator/power_box,
+ /obj/structure/particle_accelerator/end_cap,
+ /obj/machinery/containment_field,
+ /obj/structure/disposalpipe,
+ /obj/structure/sign,
+ /obj/machinery/gateway,
+ /obj/structure/lattice,
+ /obj/structure/grille,
+ /obj/machinery/the_singularitygen/tesla))
+
+ for(var/A in typecache_filter_multi_list_exclusion(oview(source, zap_range+2), things_to_shock, blacklisted_tesla_types))
+ if(istype(A, /obj/machinery/power/tesla_coil))
+ var/dist = get_dist(source, A)
+ var/obj/machinery/power/tesla_coil/C = A
+ if(dist <= zap_range && (dist < closest_dist || !closest_tesla_coil) && !C.being_shocked)
+ closest_dist = dist
+
+ //we use both of these to save on istype and typecasting overhead later on
+ //while still allowing common code to run before hand
+ closest_tesla_coil = C
+ closest_atom = C
+
+
+ else if(closest_tesla_coil)
+ continue //no need checking these other things
+
+ else if(istype(A, /obj/machinery/power/grounding_rod))
+ var/dist = get_dist(source, A)-2
+ if(dist <= zap_range && (dist < closest_dist || !closest_grounding_rod))
+ closest_grounding_rod = A
+ closest_atom = A
+ closest_dist = dist
+
+ else if(closest_grounding_rod)
+ continue
+
+ else if(isliving(A))
+ var/dist = get_dist(source, A)
+ var/mob/living/L = A
+ if(dist <= zap_range && (dist < closest_dist || !closest_mob) && L.stat != DEAD && !(L.status_flags & GODMODE))
+ closest_mob = L
+ closest_atom = A
+ closest_dist = dist
+
+ else if(closest_mob)
+ continue
+
+ else if(istype(A, /obj/machinery))
+ var/obj/machinery/M = A
+ var/dist = get_dist(source, A)
+ if(dist <= zap_range && (dist < closest_dist || !closest_machine) && !M.being_shocked)
+ closest_machine = M
+ closest_atom = A
+ closest_dist = dist
+
+ else if(closest_mob)
+ continue
+
+ else if(istype(A, /obj/structure/blob))
+ var/obj/structure/blob/B = A
+ var/dist = get_dist(source, A)
+ if(dist <= zap_range && (dist < closest_dist || !closest_tesla_coil) && !B.being_shocked)
+ closest_blob = B
+ closest_atom = A
+ closest_dist = dist
+
+ else if(closest_blob)
+ continue
+
+ else if(istype(A, /obj/structure))
+ var/obj/structure/S = A
+ var/dist = get_dist(source, A)
+ if(dist <= zap_range && (dist < closest_dist || !closest_tesla_coil) && !S.being_shocked)
+ closest_structure = S
+ closest_atom = A
+ closest_dist = dist
+
+ //Alright, we've done our loop, now lets see if was anything interesting in range
+ if(closest_atom)
+ //common stuff
+ source.Beam(closest_atom, icon_state="lightning[rand(1,12)]", time=5, maxdistance = INFINITY)
+ var/zapdir = get_dir(source, closest_atom)
+ if(zapdir)
+ . = zapdir
+
+ //per type stuff:
+ if(closest_tesla_coil)
+ closest_tesla_coil.tesla_act(power, explosive, stun_mobs)
+
+ else if(closest_grounding_rod)
+ closest_grounding_rod.tesla_act(power, explosive, stun_mobs)
+
+ else if(closest_mob)
+ var/shock_damage = Clamp(round(power/400), 10, 90) + rand(-5, 5)
+ closest_mob.electrocute_act(shock_damage, source, 1/*, tesla_shock = 1, stun = stun_mobs*/)
+ if(issilicon(closest_mob))
+ var/mob/living/silicon/S = closest_mob
+ if(stun_mobs)
+ S.emp_act(3 /*EMP_LIGHT*/)
+ tesla_zap(closest_mob, 7, power / 1.5, explosive, stun_mobs) // metallic folks bounce it further
+ else
+ tesla_zap(closest_mob, 5, power / 1.5, explosive, stun_mobs)
+
+ else if(closest_machine)
+ closest_machine.tesla_act(power, explosive, stun_mobs)
+
+ else if(closest_blob)
+ closest_blob.tesla_act(power, explosive, stun_mobs)
+
+ else if(closest_structure)
+ closest_structure.tesla_act(power, explosive, stun_mobs)
diff --git a/code/modules/power/tesla/generator.dm b/code/modules/power/tesla/generator.dm
new file mode 100644
index 0000000000..63bce099fa
--- /dev/null
+++ b/code/modules/power/tesla/generator.dm
@@ -0,0 +1,10 @@
+/obj/machinery/the_singularitygen/tesla
+ name = "energy ball generator"
+ desc = "Makes the wardenclyffe look like a child's plaything when shot with a particle accelerator."
+ icon = 'icons/obj/tesla_engine/tesla_generator.dmi'
+ icon_state = "TheSingGen"
+ creation_type = /obj/singularity/energy_ball
+
+/obj/machinery/the_singularitygen/tesla/tesla_act(power, explosive = FALSE)
+ if(explosive)
+ energy += power
diff --git a/code/modules/power/tesla/telsa_construction.dm b/code/modules/power/tesla/telsa_construction.dm
new file mode 100644
index 0000000000..5cb11b9fe6
--- /dev/null
+++ b/code/modules/power/tesla/telsa_construction.dm
@@ -0,0 +1,31 @@
+//////////////////////////
+// Circuits and Research
+//////////////////////////
+
+// Tesla coils are built as machines using a circuit researchable in RnD
+/obj/item/weapon/circuitboard/tesla_coil
+ name = T_BOARD("tesla coil")
+ build_path = /obj/machinery/power/tesla_coil
+ board_type = new /datum/frame/frame_types/machine
+ origin_tech = list(TECH_MAGNET = 2, TECH_POWER = 4)
+ req_components = list(/obj/item/weapon/stock_parts/capacitor = 1)
+
+/datum/design/circuit/tesla_coil
+ name = "Machine Design (Tesla Coil Board)"
+ desc = "The circuit board for a tesla coil."
+ id = "tesla_coil"
+ build_path = /obj/item/weapon/circuitboard/tesla_coil
+ req_tech = list(TECH_MAGNET = 2, TECH_POWER = 4)
+ sort_string = "MAAAC"
+
+// Grounding rods can be built as machines using a circuit made in an autolathe.
+/obj/item/weapon/circuitboard/grounding_rod
+ name = T_BOARD("grounding rod")
+ build_path = /obj/machinery/power/grounding_rod
+ board_type = new /datum/frame/frame_types/machine
+ matter = list(DEFAULT_WALL_MATERIAL = 50, "glass" = 50)
+ req_components = list()
+
+/datum/category_item/autolathe/engineering/grounding_rod
+ name = "grounding rod electronics"
+ path = /obj/item/weapon/circuitboard/grounding_rod
diff --git a/code/modules/power/tesla/tesla_act.dm b/code/modules/power/tesla/tesla_act.dm
new file mode 100644
index 0000000000..8e3f1c6996
--- /dev/null
+++ b/code/modules/power/tesla/tesla_act.dm
@@ -0,0 +1,68 @@
+////////////////////////////////////////
+// Vars and Default tesla_act behavior
+////////////////////////////////////////
+
+/obj
+ var/being_shocked = FALSE
+
+/obj/proc/tesla_act(var/power)
+ being_shocked = TRUE
+ var/power_bounced = power / 2
+ tesla_zap(src, 3, power_bounced)
+ //addtimer(CALLBACK(src, .proc/reset_shocked), 10)
+ //schedule_task_with_source_in(10, src, .proc/reset_shocked)
+ spawn(10) reset_shocked()
+
+/obj/proc/reset_shocked()
+ being_shocked = FALSE
+
+// Overrides for behavior on specific types
+
+/obj/structure/blob/tesla_act(power)
+ ..()
+ adjust_integrity(-power/400)
+
+/obj/machinery/nuclearbomb/tesla_act(power, explosive)
+ ..()
+ if(explosive)
+ qdel(src)//like the singulo, tesla deletes it. stops it from exploding over and over
+
+/obj/machinery/tesla_act(power, explosive = FALSE)
+ ..()
+ if(prob(85) && explosive)
+ explosion(loc, 1, 2, 4, /*flame_range = 2,*/ adminlog = FALSE/*, smoke = FALSE*/)
+ else if(prob(50))
+ emp_act(2)
+ else
+ ex_act(2)
+
+/obj/machinery/camera/tesla_act(var/power)//EMP proof upgrade also makes it tesla immune
+ if(isEmpProof())
+ return
+ ..()
+ qdel(src) //to prevent bomb testing camera from exploding over and over forever
+
+/obj/machinery/light/tesla_act(power, explosive = FALSE)
+ if(explosive)
+ explosion(loc, 0, 0, 0/*, flame_range = 5*/, adminlog = FALSE)
+ qdel(src)
+
+/obj/structure/closet/tesla_act(var/power)
+ ..() //extend the zap
+ visible_message("[src] is blown apart by the bolt of electricity!", "You hear a metallic screeching sound.")
+ dump_contents()
+ qdel(src)
+
+/obj/structure/reagent_dispensers/fueltank/tesla_act()
+ ..() //extend the zap
+ explode()
+
+
+
+
+
+
+
+
+
+
diff --git a/html/changelogs/Leshana-tesla.yml b/html/changelogs/Leshana-tesla.yml
new file mode 100644
index 0000000000..8910bebb4b
--- /dev/null
+++ b/html/changelogs/Leshana-tesla.yml
@@ -0,0 +1,6 @@
+author: Leshana
+delete-after: True
+ - soundadd: "Sounds of Tesla engine lighting bolts"
+ - imageadd: "Sprites for grounding rod and Tesla coil"
+ - imageadd: "Sprites for lighting bolts"
+ - rscadd: "The Tesla Engine, Tesla Coils, and Grounding Rods"
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index dc0c7b9c51..e7f61d605d 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/obj/tesla_engine/energy_ball.dmi b/icons/obj/tesla_engine/energy_ball.dmi
new file mode 100644
index 0000000000..22866252a9
Binary files /dev/null and b/icons/obj/tesla_engine/energy_ball.dmi differ
diff --git a/icons/obj/tesla_engine/tesla_coil.dmi b/icons/obj/tesla_engine/tesla_coil.dmi
new file mode 100644
index 0000000000..96a6c636d8
Binary files /dev/null and b/icons/obj/tesla_engine/tesla_coil.dmi differ
diff --git a/icons/obj/tesla_engine/tesla_generator.dmi b/icons/obj/tesla_engine/tesla_generator.dmi
new file mode 100644
index 0000000000..adf5fbaa8e
Binary files /dev/null and b/icons/obj/tesla_engine/tesla_generator.dmi differ
diff --git a/polaris.dme b/polaris.dme
index da4ed4e6a3..209c8afd15 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -176,6 +176,7 @@
#include "code\controllers\subsystems\garbage.dm"
#include "code\controllers\subsystems\lighting.dm"
#include "code\controllers\subsystems\machines.dm"
+#include "code\controllers\subsystems\orbits.dm"
#include "code\datums\ai_law_sets.dm"
#include "code\datums\ai_laws.dm"
#include "code\datums\beam.dm"
@@ -191,6 +192,7 @@
#include "code\datums\mixed.dm"
#include "code\datums\modules.dm"
#include "code\datums\mutable_appearance.dm"
+#include "code\datums\orbit.dm"
#include "code\datums\organs.dm"
#include "code\datums\progressbar.dm"
#include "code\datums\recipe.dm"
@@ -314,6 +316,7 @@
#include "code\datums\wires\smartfridge.dm"
#include "code\datums\wires\smes.dm"
#include "code\datums\wires\suit_storage_unit.dm"
+#include "code\datums\wires\tesla_coil.dm"
#include "code\datums\wires\vending.dm"
#include "code\datums\wires\wires.dm"
#include "code\defines\gases.dm"
@@ -2034,6 +2037,11 @@
#include "code\modules\power\singularity\particle_accelerator\particle_power.dm"
#include "code\modules\power\supermatter\setup_supermatter.dm"
#include "code\modules\power\supermatter\supermatter.dm"
+#include "code\modules\power\tesla\coil.dm"
+#include "code\modules\power\tesla\energy_ball.dm"
+#include "code\modules\power\tesla\generator.dm"
+#include "code\modules\power\tesla\telsa_construction.dm"
+#include "code\modules\power\tesla\tesla_act.dm"
#include "code\modules\projectiles\ammunition.dm"
#include "code\modules\projectiles\dnalocking.dm"
#include "code\modules\projectiles\effects.dm"
diff --git a/sound/effects/lightning_chargeup.ogg b/sound/effects/lightning_chargeup.ogg
new file mode 100644
index 0000000000..0af4449502
Binary files /dev/null and b/sound/effects/lightning_chargeup.ogg differ
diff --git a/sound/effects/lightningbolt.ogg b/sound/effects/lightningbolt.ogg
new file mode 100644
index 0000000000..e5f6a5731a
Binary files /dev/null and b/sound/effects/lightningbolt.ogg differ
diff --git a/sound/effects/lightningshock.ogg b/sound/effects/lightningshock.ogg
new file mode 100644
index 0000000000..341a2934d6
Binary files /dev/null and b/sound/effects/lightningshock.ogg differ