diff --git a/code/datums/wires/particle_accelerator.dm b/code/datums/wires/particle_accelerator.dm
new file mode 100644
index 000000000000..dfaf6fdb47f2
--- /dev/null
+++ b/code/datums/wires/particle_accelerator.dm
@@ -0,0 +1,49 @@
+/datum/wires/particle_accelerator/control_box
+ holder_type = /obj/machinery/particle_accelerator/control_box
+ proper_name = "Particle Accelerator"
+
+/datum/wires/particle_accelerator/control_box/New(atom/holder)
+ wires = list(
+ WIRE_POWER, WIRE_STRENGTH, WIRE_LIMIT,
+ WIRE_INTERFACE
+ )
+ add_duds(2)
+ ..()
+
+/datum/wires/particle_accelerator/control_box/interactable(mob/user)
+ . = ..()
+ var/obj/machinery/particle_accelerator/control_box/C = holder
+ if(C.construction_state == 2)
+ return TRUE
+
+/datum/wires/particle_accelerator/control_box/on_pulse(wire)
+ var/obj/machinery/particle_accelerator/control_box/C = holder
+ switch(wire)
+ if(WIRE_POWER)
+ C.toggle_power()
+ if(WIRE_STRENGTH)
+ C.add_strength()
+ if(WIRE_INTERFACE)
+ C.interface_control = !C.interface_control
+ if(WIRE_LIMIT)
+ C.visible_message("[icon2html(C, viewers(holder))][C] makes a large whirring noise.")
+
+/datum/wires/particle_accelerator/control_box/on_cut(wire, mend)
+ var/obj/machinery/particle_accelerator/control_box/C = holder
+ switch(wire)
+ if(WIRE_POWER)
+ if(C.active == !mend)
+ C.toggle_power()
+ if(WIRE_STRENGTH)
+ for(var/i = 1; i < 3; i++)
+ C.remove_strength()
+ if(WIRE_INTERFACE)
+ if(!mend)
+ C.interface_control = FALSE
+ if(WIRE_LIMIT)
+ C.strength_upper_limit = (mend ? 2 : 3)
+ if(C.strength_upper_limit < C.strength)
+ C.remove_strength()
+
+/datum/wires/particle_accelerator/control_box/emp_pulse() // to prevent singulo from pulsing wires
+ return
diff --git a/code/game/machinery/computer/arcade/orion.dm b/code/game/machinery/computer/arcade/orion.dm
index d32c6596786e..a958d0d4c38e 100644
--- a/code/game/machinery/computer/arcade/orion.dm
+++ b/code/game/machinery/computer/arcade/orion.dm
@@ -59,9 +59,7 @@ GLOBAL_LIST_INIT(orion_events, generate_orion_events())
/obj/machinery/computer/arcade/orion_trail/kobayashi
name = "Kobayashi Maru control computer"
desc = "A test for cadets."
- icon = 'icons/obj/machines/particle_accelerator.dmi'
- icon_keyboard = null
- icon_screen = null
+ icon = 'icons/obj/particle_accelerator.dmi'
icon_state = "control_boxp"
//kobatashi has a smaller list of events, so we copy from the global list and cut whatever isn't here
var/list/event_whitelist = list(
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index d1cc5601991e..3dd5f5bfa8a4 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -209,6 +209,12 @@
/obj/item/circuitboard/machine/turbine_compressor
name = "Turbine - Inlet Compressor"
+/obj/item/circuitboard/machine/pacman/mrs
+ name = "MRSPACMAN-type Generator (Machine Board)"
+ build_path = /obj/machinery/power/port_gen/pacman/mrs
+
+/obj/item/circuitboard/machine/power_compressor
+ name = "Power Compressor (Machine Board)"
greyscale_colors = CIRCUIT_COLOR_ENGINEERING
build_path = /obj/machinery/power/turbine/inlet_compressor/constructed
req_components = list(
diff --git a/code/modules/awaymissions/bluespaceartillery.dm b/code/modules/awaymissions/bluespaceartillery.dm
new file mode 100644
index 000000000000..8adcf8823f55
--- /dev/null
+++ b/code/modules/awaymissions/bluespaceartillery.dm
@@ -0,0 +1,55 @@
+
+
+/obj/machinery/artillerycontrol
+ var/reload = 120
+ var/reload_cooldown = 120
+ var/explosiondev = 3
+ var/explosionmed = 6
+ var/explosionlight = 12
+ name = "bluespace artillery control"
+ icon_state = "control_boxp1"
+ icon = 'icons/obj/particle_accelerator.dmi'
+ density = TRUE
+
+/obj/machinery/artillerycontrol/process(delta_time)
+ if(reload < reload_cooldown)
+ reload += delta_time
+
+/obj/structure/artilleryplaceholder
+ name = "artillery"
+ icon = 'icons/obj/machines/artillery.dmi'
+ anchored = TRUE
+ density = TRUE
+
+/obj/structure/artilleryplaceholder/decorative
+ density = FALSE
+
+/obj/machinery/artillerycontrol/ui_interact(mob/user)
+ . = ..()
+ var/dat = "Bluespace Artillery Control:
"
+ dat += "Locked on
"
+ dat += "Charge progress: [reload]/[reload_cooldown]:
"
+ dat += "Open Fire
"
+ dat += "Deployment of weapon authorized by
Nanotrasen Naval Command
Remember, friendly fire is grounds for termination of your contract and life.
"
+ user << browse(dat, "window=scroll")
+ onclose(user, "scroll")
+
+/obj/machinery/artillerycontrol/Topic(href, href_list)
+ if(..())
+ return
+ var/A
+ A = input("Area to bombard", "Open Fire", A) in GLOB.teleportlocs
+ var/area/thearea = GLOB.teleportlocs[A]
+ if(usr.stat != CONSCIOUS || HAS_TRAIT(usr, TRAIT_HANDS_BLOCKED))
+ return
+ if(reload < reload_cooldown)
+ return
+ if(usr.contents.Find(src) || (in_range(src, usr) && isturf(loc)) || issilicon(usr))
+ priority_announce("Bluespace artillery fire detected. Brace for impact.")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] has launched an artillery strike.")
+ var/list/L = list()
+ for(var/turf/T in get_area_turfs(thearea.type))
+ L+=T
+ var/loc = pick(L)
+ explosion(loc, explosiondev, explosionmed, explosionlight)
+ reload = 0
diff --git a/code/modules/cargo/bounties/engineering.dm b/code/modules/cargo/bounties/engineering.dm
index 9da865ec7533..723d77074ae4 100644
--- a/code/modules/cargo/bounties/engineering.dm
+++ b/code/modules/cargo/bounties/engineering.dm
@@ -1,3 +1,58 @@
+/datum/bounty/item/engineering/gas
+ name = "Full Tank of Pluoxium"
+ description = "CentCom RnD is researching extra compact internals. Ship us a tank full of Pluoxium and you'll be compensated."
+ reward = CARGO_CRATE_VALUE * 15
+ wanted_types = list(/obj/item/tank)
+ var/moles_required = 20 // A full tank is 28 moles, but CentCom ignores that fact.
+ var/gas_type = /datum/gas/pluoxium
+
+/datum/bounty/item/engineering/gas/applies_to(obj/O)
+ if(!..())
+ return FALSE
+ var/obj/item/tank/T = O
+ var/datum/gas_mixture/our_mix = T.return_air()
+ if(!our_mix.gases[gas_type])
+ return FALSE
+ return our_mix.gases[gas_type][MOLES] >= moles_required
+
+/datum/bounty/item/engineering/gas/nitryl_tank
+ name = "Full Tank of Nitryl"
+ description = "The non-human staff of Station 88 has been volunteered to test performance enhancing drugs. Ship them a tank full of Nitryl so they can get started. (20 Moles)"
+ gas_type = /datum/gas/nitryl
+
+/datum/bounty/item/engineering/gas/freon_tank
+ name = "Full Tank of Freon"
+ description = "The Supermatter of station 33 has started the delamination process. Deliver a tank of Freon gas to help them stop it! (20 Moles)"
+ gas_type = /datum/gas/freon
+
+/datum/bounty/item/engineering/gas/tritium_tank
+ name = "Full Tank of Tritium"
+ description = "Station 49 is looking to kickstart their research program. Ship them a tank full of Tritium. (20 Moles)"
+ gas_type = /datum/gas/tritium
+
+/datum/bounty/item/engineering/gas/hydrogen_tank
+ name = "Full Tank of Hydrogen"
+ description = "Our R&D department is working on the development of more efficient electrical batteries using hydrogen as a catalyst. Ship us a tank full of it. (20 Moles)"
+ gas_type = /datum/gas/hydrogen
+
+/datum/bounty/item/engineering/gas/zauker_tank
+ name = "Full Tank of Zauker"
+ description = "The main planet of \[REDACTED] has been chosen as testing grounds for the new weapon that uses Zauker gas. Ship us a tank full of it. (20 Moles)"
+ reward = CARGO_CRATE_VALUE * 20
+ gas_type = /datum/gas/zauker
+
+/datum/bounty/item/engineering/energy_ball
+ name = "Contained Tesla Ball"
+ description = "Station 24 is being overrun by hordes of angry Mothpeople. They are requesting the ultimate bug zapper."
+ reward = CARGO_CRATE_VALUE * 375
+ wanted_types = list(/obj/singularity/energy_ball)
+
+/datum/bounty/item/engineering/energy_ball/applies_to(obj/O)
+ if(!..())
+ return FALSE
+ var/obj/singularity/energy_ball/T = O
+ return !T.miniball
+
/datum/bounty/item/engineering/emitter
name = "Emitter"
description = "We think there may be a defect in your station's emitter designs, based on the sheer number of delaminations your sector seems to see. Ship us one of yours."
diff --git a/code/modules/cargo/exports/large_objects.dm b/code/modules/cargo/exports/large_objects.dm
index 1871d69eb452..a340955a5431 100644
--- a/code/modules/cargo/exports/large_objects.dm
+++ b/code/modules/cargo/exports/large_objects.dm
@@ -81,6 +81,16 @@
unit_name = "tesla coil"
export_types = list(/obj/machinery/power/energy_accumulator/tesla_coil)
+/datum/export/large/pa
+ cost = CARGO_CRATE_VALUE * 2
+ unit_name = "particle accelerator part"
+ export_types = list(/obj/structure/particle_accelerator)
+
+/datum/export/large/pa/controls
+ cost = CARGO_CRATE_VALUE * 2.5
+ unit_name = "particle accelerator control console"
+ export_types = list(/obj/machinery/particle_accelerator/control_box)
+
/datum/export/large/supermatter
cost = CARGO_CRATE_VALUE * 16
unit_name = "supermatter shard"
@@ -91,6 +101,17 @@
unit_name = "grounding rod"
export_types = list(/obj/machinery/power/energy_accumulator/grounding_rod)
+/datum/export/large/tesla_gen
+ cost = CARGO_CRATE_VALUE * 16
+ unit_name = "energy ball generator"
+ export_types = list(/obj/machinery/the_singularitygen/tesla)
+
+/datum/export/large/singulo_gen
+ cost = CARGO_CRATE_VALUE * 16
+ unit_name = "gravitational singularity generator"
+ export_types = list(/obj/machinery/the_singularitygen)
+ include_subtypes = FALSE
+
/datum/export/large/iv
cost = CARGO_CRATE_VALUE * 0.25
unit_name = "iv drip"
diff --git a/code/modules/cargo/exports/tools.dm b/code/modules/cargo/exports/tools.dm
index 94e9c2643653..f22e141fca43 100644
--- a/code/modules/cargo/exports/tools.dm
+++ b/code/modules/cargo/exports/tools.dm
@@ -136,6 +136,26 @@
unit_name = "rapid pipe dispenser"
export_types = list(/obj/item/pipe_dispenser)
+/datum/export/singulo //failsafe in case someone decides to ship a live singularity to CentCom without the corresponding bounty
+ cost = 1
+ unit_name = "singularity"
+ export_types = list(/obj/singularity)
+ include_subtypes = FALSE
+
+/datum/export/singulo/total_printout(datum/export_report/ex, notes = TRUE)
+ . = ..()
+ if(. && notes)
+ . += " ERROR: Invalid object detected."
+
+/datum/export/singulo/tesla //see above
+ unit_name = "energy ball"
+ export_types = list(/obj/singularity/energy_ball)
+
+/datum/export/singulo/tesla/total_printout(datum/export_report/ex, notes = TRUE)
+ . = ..()
+ if(. && notes)
+ . += " ERROR: Unscheduled energy ball delivery detected."
+
//artisanal exports for the mom and pops
/datum/export/soap
cost = CARGO_CRATE_VALUE * 0.375
diff --git a/code/modules/power/port_gen.dm b/code/modules/power/port_gen.dm
index 5400a48422c4..2b6e998ac778 100644
--- a/code/modules/power/port_gen.dm
+++ b/code/modules/power/port_gen.dm
@@ -277,5 +277,20 @@
power_gen = 15000
sheet_path = /obj/item/stack/sheet/mineral/uranium
+/obj/machinery/power/port_gen/pacman/super/overheat()
+ explosion(src, devastation_range = 3, heavy_impact_range = 3, light_impact_range = 3, flash_range = -1)
+
+/obj/machinery/power/port_gen/pacman/mrs
+ name = "\improper M.R.S.P.A.C.M.A.N.-type portable generator"
+ base_icon = "portgen2"
+ icon_state = "portgen2_0"
+ circuit = /obj/item/circuitboard/machine/pacman/mrs
+ sheet_path = /obj/item/stack/sheet/mineral/diamond
+ power_gen = 40000
+ time_per_sheet = 80
+
+/obj/machinery/power/port_gen/pacman/mrs/overheat()
+ explosion(src.loc, 4, 4, 4, -1)
+
/obj/machinery/power/port_gen/pacman/pre_loaded
sheets = 15
diff --git a/code/modules/power/singularity/generator.dm b/code/modules/power/singularity/generator.dm
new file mode 100644
index 000000000000..f5e3bbc1417b
--- /dev/null
+++ b/code/modules/power/singularity/generator.dm
@@ -0,0 +1,35 @@
+/////SINGULARITY SPAWNER
+/obj/machinery/the_singularitygen
+ name = "Gravitational Singularity Generator"
+ desc = "An odd device which produces a Gravitational Singularity when set up."
+ icon = 'icons/obj/singularity.dmi'
+ icon_state = "TheSingGen"
+ anchored = FALSE
+ density = TRUE
+ use_power = NO_POWER_USE
+ resistance_flags = FIRE_PROOF
+
+ // You can buckle someone to the singularity generator, then start the engine. Fun!
+ can_buckle = TRUE
+ buckle_lying = FALSE
+ buckle_requires_restraints = TRUE
+
+ var/energy = 0
+ var/creation_type = /obj/singularity
+
+/obj/machinery/the_singularitygen/attackby(obj/item/W, mob/user, params)
+ if(W.tool_behaviour == TOOL_WRENCH)
+ default_unfasten_wrench(user, W, 0)
+ else
+ return ..()
+
+/obj/machinery/the_singularitygen/process()
+ if(energy > 0)
+ if(energy >= 200)
+ var/turf/T = get_turf(src)
+ SSblackbox.record_feedback("tally", "engine_started", 1, type)
+ var/obj/singularity/S = new creation_type(T, 50)
+ transfer_fingerprints_to(S)
+ qdel(src)
+ else
+ energy -= 1
diff --git a/code/modules/power/singularity/particle_accelerator/particle.dm b/code/modules/power/singularity/particle_accelerator/particle.dm
new file mode 100644
index 000000000000..6dcc0a6aae2a
--- /dev/null
+++ b/code/modules/power/singularity/particle_accelerator/particle.dm
@@ -0,0 +1,63 @@
+/obj/effect/accelerated_particle
+ name = "Accelerated Particles"
+ desc = "Small things moving very fast."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "particle"
+ anchored = TRUE
+ density = FALSE
+ var/movement_range = 20
+ var/energy = 10
+ var/speed = 1
+
+/obj/effect/accelerated_particle/weak
+ movement_range = 15
+ energy = 5
+
+/obj/effect/accelerated_particle/strong
+ movement_range = 25
+ energy = 15
+
+/obj/effect/accelerated_particle/powerful
+ movement_range = 30
+ energy = 50
+
+
+/obj/effect/accelerated_particle/New(loc)
+ ..()
+
+ addtimer(CALLBACK(src, .proc/move), 1)
+
+
+/obj/effect/accelerated_particle/Bump(atom/A)
+ if(A)
+ if(isliving(A))
+ toxmob(A)
+ else if(istype(A, /obj/machinery/the_singularitygen))
+ var/obj/machinery/the_singularitygen/S = A
+ S.energy += energy
+ else if(istype(A, /obj/singularity))
+ var/obj/singularity/S = A
+ S.energy += energy
+ else if(istype(A, /obj/structure/blob))
+ var/obj/structure/blob/B = A
+ B.take_damage(energy*0.6)
+ movement_range = 0
+
+/obj/effect/accelerated_particle/ex_act(severity, target)
+ qdel(src)
+
+/obj/effect/accelerated_particle/singularity_pull()
+ return
+
+/obj/effect/accelerated_particle/proc/toxmob(mob/living/M)
+ M.rad_act(energy*6)
+
+/obj/effect/accelerated_particle/proc/move()
+ if(!step(src,dir))
+ forceMove(get_step(src,dir))
+ movement_range--
+ if(movement_range == 0)
+ qdel(src)
+ else
+ sleep(speed)
+ move()
diff --git a/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
new file mode 100644
index 000000000000..4518f4654307
--- /dev/null
+++ b/code/modules/power/singularity/particle_accelerator/particle_accelerator.dm
@@ -0,0 +1,166 @@
+#define PA_CONSTRUCTION_UNSECURED 0
+#define PA_CONSTRUCTION_UNWIRED 1
+#define PA_CONSTRUCTION_PANEL_OPEN 2
+#define PA_CONSTRUCTION_COMPLETE 3
+
+/obj/structure/particle_accelerator
+ name = "Particle Accelerator"
+ desc = "Part of a Particle Accelerator."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "none"
+ anchored = FALSE
+ density = TRUE
+ max_integrity = 500
+ armor = list("melee" = 30, "bullet" = 20, "laser" = 20, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 90, "acid" = 80)
+
+ var/obj/machinery/particle_accelerator/control_box/master = null
+ var/construction_state = PA_CONSTRUCTION_UNSECURED
+ var/reference = null
+ var/powered = 0
+ var/strength = null
+
+/obj/structure/particle_accelerator/examine(mob/user)
+ . = ..()
+
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED)
+ . += "Looks like it's not attached to the flooring."
+ if(PA_CONSTRUCTION_UNWIRED)
+ . += "It is missing some cables."
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ . += "The panel is open."
+
+/obj/structure/particle_accelerator/Destroy()
+ construction_state = PA_CONSTRUCTION_UNSECURED
+ if(master)
+ master.connected_parts -= src
+ master.assembled = 0
+ master = null
+ return ..()
+
+/obj/structure/particle_accelerator/ComponentInitialize()
+ . = ..()
+ AddComponent(/datum/component/simple_rotation,ROTATION_ALTCLICK | ROTATION_CLOCKWISE | ROTATION_COUNTERCLOCKWISE | ROTATION_VERBS )
+
+
+/obj/structure/particle_accelerator/set_anchored(anchorvalue)
+ . = ..()
+ if(isnull(.))
+ return
+ construction_state = anchorvalue ? PA_CONSTRUCTION_UNWIRED : PA_CONSTRUCTION_UNSECURED
+ update_state()
+ update_icon()
+
+/obj/structure/particle_accelerator/attackby(obj/item/W, mob/user, params)
+ var/did_something = FALSE
+
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED)
+ if(W.tool_behaviour == TOOL_WRENCH && !isinspace())
+ W.play_tool_sound(src, 75)
+ set_anchored(TRUE)
+ user.visible_message("[user.name] secures the [name] to the floor.", \
+ "You secure the external bolts.")
+ user.changeNext_move(CLICK_CD_MELEE)
+ return //set_anchored handles the rest of the stuff we need to do.
+ if(PA_CONSTRUCTION_UNWIRED)
+ if(W.tool_behaviour == TOOL_WRENCH)
+ W.play_tool_sound(src, 75)
+ set_anchored(FALSE)
+ user.visible_message("[user.name] detaches the [name] from the floor.", \
+ "You remove the external bolts.")
+ user.changeNext_move(CLICK_CD_MELEE)
+ return //set_anchored handles the rest of the stuff we need to do.
+ else if(istype(W, /obj/item/stack/cable_coil))
+ var/obj/item/stack/cable_coil/CC = W
+ if(CC.use(1))
+ user.visible_message("[user.name] adds wires to the [name].", \
+ "You add some wires.")
+ construction_state = PA_CONSTRUCTION_PANEL_OPEN
+ did_something = TRUE
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ if(W.tool_behaviour == TOOL_WIRECUTTER)//TODO:Shock user if its on?
+ user.visible_message("[user.name] removes some wires from the [name].", \
+ "You remove some wires.")
+ construction_state = PA_CONSTRUCTION_UNWIRED
+ did_something = TRUE
+ else if(W.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message("[user.name] closes the [name]'s access panel.", \
+ "You close the access panel.")
+ construction_state = PA_CONSTRUCTION_COMPLETE
+ did_something = TRUE
+ if(PA_CONSTRUCTION_COMPLETE)
+ if(W.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message("[user.name] opens the [name]'s access panel.", \
+ "You open the access panel.")
+ construction_state = PA_CONSTRUCTION_PANEL_OPEN
+ did_something = TRUE
+
+ if(did_something)
+ user.changeNext_move(CLICK_CD_MELEE)
+ update_state()
+ update_icon()
+ return
+
+ return ..()
+
+
+/obj/structure/particle_accelerator/deconstruct(disassembled = TRUE)
+ if(!(flags_1 & NODECONSTRUCT_1))
+ new /obj/item/stack/sheet/iron (loc, 5)
+ qdel(src)
+
+/obj/structure/particle_accelerator/Move()
+ . = ..()
+ if(master && master.active)
+ master.toggle_power()
+ investigate_log("was moved whilst active; it powered down.", INVESTIGATE_SINGULO)
+
+
+/obj/structure/particle_accelerator/update_icon_state()
+ . = ..()
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED,PA_CONSTRUCTION_UNWIRED)
+ icon_state="[reference]"
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ icon_state="[reference]w"
+ if(PA_CONSTRUCTION_COMPLETE)
+ if(powered)
+ icon_state="[reference]p[strength]"
+ else
+ icon_state="[reference]c"
+
+/obj/structure/particle_accelerator/proc/update_state()
+ if(master)
+ master.update_state()
+
+/obj/structure/particle_accelerator/proc/connect_master(obj/O)
+ if(O.dir == dir)
+ master = O
+ return 1
+ return 0
+
+///////////
+// PARTS //
+///////////
+
+
+/obj/structure/particle_accelerator/end_cap
+ name = "Alpha Particle Generation Array"
+ desc = "This is where Alpha particles are generated from \[REDACTED\]."
+ icon_state = "end_cap"
+ reference = "end_cap"
+
+/obj/structure/particle_accelerator/power_box
+ name = "Particle Focusing EM Lens"
+ desc = "This uses electromagnetic waves to focus the Alpha particles."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "power_box"
+ reference = "power_box"
+
+/obj/structure/particle_accelerator/fuel_chamber
+ name = "EM Acceleration Chamber"
+ desc = "This is where the Alpha particles are accelerated to radical speeds."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "fuel_chamber"
+ reference = "fuel_chamber"
diff --git a/code/modules/power/singularity/particle_accelerator/particle_control.dm b/code/modules/power/singularity/particle_accelerator/particle_control.dm
new file mode 100644
index 000000000000..dd2be56d282d
--- /dev/null
+++ b/code/modules/power/singularity/particle_accelerator/particle_control.dm
@@ -0,0 +1,329 @@
+/obj/machinery/particle_accelerator/control_box
+ name = "Particle Accelerator Control Console"
+ desc = "This controls the density of the particles."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "control_box"
+ anchored = FALSE
+ density = TRUE
+ use_power = NO_POWER_USE
+ idle_power_usage = 500
+ active_power_usage = 10000
+ dir = NORTH
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+ var/strength_upper_limit = 2
+ var/interface_control = TRUE
+ var/list/obj/structure/particle_accelerator/connected_parts
+ var/assembled = FALSE
+ var/construction_state = PA_CONSTRUCTION_UNSECURED
+ var/active = FALSE
+ var/strength = 0
+ var/powered = FALSE
+
+/obj/machinery/particle_accelerator/control_box/Initialize()
+ . = ..()
+ wires = new /datum/wires/particle_accelerator/control_box(src)
+ connected_parts = list()
+
+/obj/machinery/particle_accelerator/control_box/Destroy()
+ if(active)
+ toggle_power()
+ for(var/CP in connected_parts)
+ var/obj/structure/particle_accelerator/part = CP
+ part.master = null
+ connected_parts.Cut()
+ QDEL_NULL(wires)
+ return ..()
+
+/obj/machinery/particle_accelerator/control_box/multitool_act(mob/living/user, obj/item/I)
+ . = ..()
+ if(construction_state == PA_CONSTRUCTION_PANEL_OPEN)
+ wires.interact(user)
+ return TRUE
+
+/obj/machinery/particle_accelerator/control_box/proc/update_state()
+ if(construction_state < PA_CONSTRUCTION_COMPLETE)
+ use_power = NO_POWER_USE
+ assembled = FALSE
+ active = FALSE
+ for(var/CP in connected_parts)
+ var/obj/structure/particle_accelerator/part = CP
+ part.strength = null
+ part.powered = FALSE
+ part.update_icon()
+ connected_parts.Cut()
+ return
+ if(!part_scan())
+ use_power = IDLE_POWER_USE
+ active = FALSE
+ connected_parts.Cut()
+
+/obj/machinery/particle_accelerator/control_box/update_icon_state()
+ . = ..()
+ if(active)
+ icon_state = "control_boxp1"
+ else
+ if(use_power)
+ if(assembled)
+ icon_state = "control_boxp"
+ else
+ icon_state = "ucontrol_boxp"
+ else
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED, PA_CONSTRUCTION_UNWIRED)
+ icon_state = "control_box"
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ icon_state = "control_boxw"
+ else
+ icon_state = "control_boxc"
+
+/obj/machinery/particle_accelerator/control_box/proc/strength_change()
+ for(var/CP in connected_parts)
+ var/obj/structure/particle_accelerator/part = CP
+ part.strength = strength
+ part.update_icon()
+
+/obj/machinery/particle_accelerator/control_box/proc/add_strength(s)
+ if(assembled && (strength < strength_upper_limit))
+ strength++
+ strength_change()
+
+ message_admins("PA Control Computer increased to [strength] by [ADMIN_LOOKUPFLW(usr)] in [ADMIN_VERBOSEJMP(src)]")
+ log_game("PA Control Computer increased to [strength] by [key_name(usr)] in [AREACOORD(src)]")
+ investigate_log("increased to [strength] by [key_name(usr)] at [AREACOORD(src)]", INVESTIGATE_SINGULO)
+
+/obj/machinery/particle_accelerator/control_box/proc/remove_strength(s)
+ if(assembled && (strength > 0))
+ strength--
+ strength_change()
+
+ message_admins("PA Control Computer decreased to [strength] by [ADMIN_LOOKUPFLW(usr)] in [ADMIN_VERBOSEJMP(src)]")
+ log_game("PA Control Computer decreased to [strength] by [key_name(usr)] in [AREACOORD(src)]")
+ investigate_log("decreased to [strength] by [key_name(usr)] at [AREACOORD(src)]", INVESTIGATE_SINGULO)
+
+/obj/machinery/particle_accelerator/control_box/power_change()
+ . = ..()
+ if(machine_stat & NOPOWER)
+ active = FALSE
+ use_power = NO_POWER_USE
+ else if(!machine_stat && construction_state == PA_CONSTRUCTION_COMPLETE)
+ use_power = IDLE_POWER_USE
+
+/obj/machinery/particle_accelerator/control_box/process()
+ if(active)
+ //a part is missing!
+ if(connected_parts.len < 6)
+ investigate_log("lost a connected part; It powered down.", INVESTIGATE_SINGULO)
+ toggle_power()
+ update_icon()
+ return
+ //emit some particles
+ for(var/obj/structure/particle_accelerator/particle_emitter/PE in connected_parts)
+ PE.emit_particle(strength)
+
+/obj/machinery/particle_accelerator/control_box/proc/part_scan()
+ var/ldir = turn(dir,-90)
+ var/rdir = turn(dir,90)
+ var/odir = turn(dir,180)
+ var/turf/T = loc
+
+ assembled = FALSE
+ critical_machine = FALSE
+
+ var/obj/structure/particle_accelerator/fuel_chamber/F = locate() in orange(1,src)
+ if(!F)
+ return FALSE
+
+ setDir(F.dir)
+ connected_parts.Cut()
+
+ T = get_step(T,rdir)
+ if(!check_part(T, /obj/structure/particle_accelerator/fuel_chamber))
+ return FALSE
+ T = get_step(T,odir)
+ if(!check_part(T, /obj/structure/particle_accelerator/end_cap))
+ return FALSE
+ T = get_step(T,dir)
+ T = get_step(T,dir)
+ if(!check_part(T, /obj/structure/particle_accelerator/power_box))
+ return FALSE
+ T = get_step(T,dir)
+ if(!check_part(T, /obj/structure/particle_accelerator/particle_emitter/center))
+ return FALSE
+ T = get_step(T,ldir)
+ if(!check_part(T, /obj/structure/particle_accelerator/particle_emitter/left))
+ return FALSE
+ T = get_step(T,rdir)
+ T = get_step(T,rdir)
+ if(!check_part(T, /obj/structure/particle_accelerator/particle_emitter/right))
+ return FALSE
+
+ assembled = TRUE
+ critical_machine = TRUE //Only counts if the PA is actually assembled.
+ return TRUE
+
+/obj/machinery/particle_accelerator/control_box/proc/check_part(turf/T, type)
+ var/obj/structure/particle_accelerator/PA = locate(/obj/structure/particle_accelerator) in T
+ if(istype(PA, type) && (PA.construction_state == PA_CONSTRUCTION_COMPLETE))
+ if(PA.connect_master(src))
+ connected_parts.Add(PA)
+ return TRUE
+ return FALSE
+
+/obj/machinery/particle_accelerator/control_box/proc/toggle_power()
+ active = !active
+ investigate_log("turned [active?"ON":"OFF"] by [usr ? key_name(usr) : "outside forces"] at [AREACOORD(src)]", INVESTIGATE_SINGULO)
+ message_admins("PA Control Computer turned [active ?"ON":"OFF"] by [usr ? ADMIN_LOOKUPFLW(usr) : "outside forces"] in [ADMIN_VERBOSEJMP(src)]")
+ log_game("PA Control Computer turned [active ?"ON":"OFF"] by [usr ? "[key_name(usr)]" : "outside forces"] at [AREACOORD(src)]")
+ if(active)
+ use_power = ACTIVE_POWER_USE
+ for(var/CP in connected_parts)
+ var/obj/structure/particle_accelerator/part = CP
+ part.strength = strength
+ part.powered = TRUE
+ part.update_icon()
+ else
+ use_power = IDLE_POWER_USE
+ for(var/CP in connected_parts)
+ var/obj/structure/particle_accelerator/part = CP
+ part.strength = null
+ part.powered = FALSE
+ part.update_icon()
+ return TRUE
+
+/obj/machinery/particle_accelerator/control_box/examine(mob/user)
+ . = ..()
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED)
+ . += "Looks like it's not attached to the flooring."
+ if(PA_CONSTRUCTION_UNWIRED)
+ . += "It is missing some cables."
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ . += "The panel is open."
+
+/obj/machinery/particle_accelerator/control_box/set_anchored(anchorvalue)
+ . = ..()
+ if(isnull(.))
+ return
+ construction_state = anchorvalue ? PA_CONSTRUCTION_UNWIRED : PA_CONSTRUCTION_UNSECURED
+ update_state()
+ update_icon()
+
+/obj/machinery/particle_accelerator/control_box/attackby(obj/item/W, mob/user, params)
+ var/did_something = FALSE
+
+ switch(construction_state)
+ if(PA_CONSTRUCTION_UNSECURED)
+ if(W.tool_behaviour == TOOL_WRENCH && !isinspace())
+ W.play_tool_sound(src, 75)
+ set_anchored(TRUE)
+ user.visible_message("[user.name] secures the [name] to the floor.", \
+ "You secure the external bolts.")
+ user.changeNext_move(CLICK_CD_MELEE)
+ return //set_anchored handles the rest of the stuff we need to do.
+ if(PA_CONSTRUCTION_UNWIRED)
+ if(W.tool_behaviour == TOOL_WRENCH)
+ W.play_tool_sound(src, 75)
+ set_anchored(FALSE)
+ user.visible_message("[user.name] detaches the [name] from the floor.", \
+ "You remove the external bolts.")
+ user.changeNext_move(CLICK_CD_MELEE)
+ return //set_anchored handles the rest of the stuff we need to do.
+ else if(istype(W, /obj/item/stack/cable_coil))
+ var/obj/item/stack/cable_coil/CC = W
+ if(CC.use(1))
+ user.visible_message("[user.name] adds wires to the [name].", \
+ "You add some wires.")
+ construction_state = PA_CONSTRUCTION_PANEL_OPEN
+ did_something = TRUE
+ if(PA_CONSTRUCTION_PANEL_OPEN)
+ if(W.tool_behaviour == TOOL_WIRECUTTER)//TODO:Shock user if its on?
+ user.visible_message("[user.name] removes some wires from the [name].", \
+ "You remove some wires.")
+ construction_state = PA_CONSTRUCTION_UNWIRED
+ did_something = TRUE
+ else if(W.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message("[user.name] closes the [name]'s access panel.", \
+ "You close the access panel.")
+ construction_state = PA_CONSTRUCTION_COMPLETE
+ did_something = TRUE
+ if(PA_CONSTRUCTION_COMPLETE)
+ if(W.tool_behaviour == TOOL_SCREWDRIVER)
+ user.visible_message("[user.name] opens the [name]'s access panel.", \
+ "You open the access panel.")
+ construction_state = PA_CONSTRUCTION_PANEL_OPEN
+ did_something = TRUE
+
+ if(did_something)
+ user.changeNext_move(CLICK_CD_MELEE)
+ update_state()
+ update_icon()
+ return
+
+ return ..()
+
+/obj/machinery/particle_accelerator/control_box/blob_act(obj/structure/blob/B)
+ if(prob(50))
+ qdel(src)
+
+/obj/machinery/particle_accelerator/control_box/interact(mob/user)
+ if(construction_state == PA_CONSTRUCTION_PANEL_OPEN)
+ wires.interact(user)
+ else
+ ..()
+
+/obj/machinery/particle_accelerator/control_box/proc/is_interactive(mob/user)
+ if(!interface_control)
+ to_chat(user, "ERROR: Request timed out. Check wire contacts.")
+ return FALSE
+ if(construction_state != PA_CONSTRUCTION_COMPLETE)
+ return FALSE
+ return TRUE
+
+/obj/machinery/particle_accelerator/control_box/ui_status(mob/user)
+ if(is_interactive(user))
+ return ..()
+ return UI_CLOSE
+
+/obj/machinery/particle_accelerator/control_box/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ParticleAccelerator", name)
+ ui.open()
+
+/obj/machinery/particle_accelerator/control_box/ui_data(mob/user)
+ var/list/data = list()
+ data["assembled"] = assembled
+ data["power"] = active
+ data["strength"] = strength
+ return data
+
+/obj/machinery/particle_accelerator/control_box/ui_act(action, params)
+ if(..())
+ return
+
+ switch(action)
+ if("power")
+ if(wires.is_cut(WIRE_POWER))
+ return
+ toggle_power()
+ . = TRUE
+ if("scan")
+ part_scan()
+ . = TRUE
+ if("add_strength")
+ if(wires.is_cut(WIRE_STRENGTH))
+ return
+ add_strength()
+ . = TRUE
+ if("remove_strength")
+ if(wires.is_cut(WIRE_STRENGTH))
+ return
+ remove_strength()
+ . = TRUE
+
+ update_icon()
+
+#undef PA_CONSTRUCTION_UNSECURED
+#undef PA_CONSTRUCTION_UNWIRED
+#undef PA_CONSTRUCTION_PANEL_OPEN
+#undef PA_CONSTRUCTION_COMPLETE
diff --git a/code/modules/power/singularity/particle_accelerator/particle_emitter.dm b/code/modules/power/singularity/particle_accelerator/particle_emitter.dm
new file mode 100644
index 000000000000..d754b7e62d31
--- /dev/null
+++ b/code/modules/power/singularity/particle_accelerator/particle_emitter.dm
@@ -0,0 +1,43 @@
+/obj/structure/particle_accelerator/particle_emitter
+ name = "EM Containment Grid"
+ desc = "This launches the Alpha particles, might not want to stand near this end."
+ icon = 'icons/obj/particle_accelerator.dmi'
+ icon_state = "none"
+ var/fire_delay = 50
+ var/last_shot = 0
+
+/obj/structure/particle_accelerator/particle_emitter/center
+ icon_state = "emitter_center"
+ reference = "emitter_center"
+
+/obj/structure/particle_accelerator/particle_emitter/left
+ icon_state = "emitter_left"
+ reference = "emitter_left"
+
+/obj/structure/particle_accelerator/particle_emitter/right
+ icon_state = "emitter_right"
+ reference = "emitter_right"
+
+/obj/structure/particle_accelerator/particle_emitter/proc/set_delay(delay)
+ if(delay >= 0)
+ fire_delay = delay
+ return 1
+ return 0
+
+/obj/structure/particle_accelerator/particle_emitter/proc/emit_particle(strength = 0)
+ if((last_shot + fire_delay) <= world.time)
+ last_shot = world.time
+ var/turf/T = get_turf(src)
+ var/obj/effect/accelerated_particle/P
+ switch(strength)
+ if(0)
+ P = new/obj/effect/accelerated_particle/weak(T)
+ if(1)
+ P = new/obj/effect/accelerated_particle(T)
+ if(2)
+ P = new/obj/effect/accelerated_particle/strong(T)
+ if(3)
+ P = new/obj/effect/accelerated_particle/powerful(T)
+ P.setDir(dir)
+ return 1
+ return 0
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index de6a36e43c0c..dab0054c316e 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -7,49 +7,44 @@
anchored = TRUE
density = TRUE
move_resist = INFINITY
- plane = MASSIVE_OBJ_PLANE
- plane = ABOVE_LIGHTING_PLANE
+ layer = MASSIVE_OBJ_PLANE
light_range = 6
- appearance_flags = LONG_GLIDE
-
- /// The singularity component itself.
- /// A weak ref in case an admin removes the component to preserve the functionality.
- var/datum/weakref/singularity_component
+ appearance_flags = 0
///Current singularity size, from 1 to 6
var/current_size = 1
///Current allowed size for the singulo
var/allowed_size = 1
- ///How strong are we?
- var/energy = 100
- ///Do we lose energy over time?
- var/dissipate = TRUE
- /// How long should it take for us to dissipate in seconds?
- var/dissipate_delay = 20
- /// How much energy do we lose every dissipate_delay?
- var/dissipate_strength = 1
- /// How long its been (in seconds) since the last dissipation
- var/time_since_last_dissipiation = 0
- ///Prob for event each tick
- var/event_chance = 10
- ///Can i move by myself?
- var/move_self = TRUE
- ///If the singularity has eaten a supermatter shard and can go to stage six
- var/consumed_supermatter = FALSE
- /// How long it's been since the singulo last acted, in seconds
- var/time_since_act = 0
-
- flags_1 = SUPERMATTER_IGNORES_1
+ var/contained = 1 //Is it gonna move around?
+ var/energy = 100 //How strong are we?
+ var/dissipate = 1 //Do we lose energy over time?
+ var/dissipate_delay = 10 //How long does it take to dissipate in seconds?
+ var/dissipate_track = 0 //Tracks the time before dissipation starts
+ var/dissipate_strength = 1 //How much energy do we lose?
+ var/move_self = 1 //Do we move on our own?
+ var/grav_pull = 4 //How many tiles out do we pull?
+ var/consume_range = 0 //How many tiles out do we eat
+ var/event_chance = 10 //Prob for event each tick
+ var/target = null //its target. moves towards the target if it has one
+ var/last_failed_movement = 0//Will not move in the same dir if it couldnt before, will help with the getting stuck on fields thing
+ var/last_warning
+ var/consumedSupermatter = 0 //If the singularity has eaten a supermatter shard and can go to stage six
+ var/drifting_dir = 0 // Chosen direction to drift in
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
/obj/singularity/Initialize(mapload, starting_energy = 50)
+ //In case Urist McShitler is feeling quirky for once.
+ admin_investigate_setup()
+
+ src.energy = starting_energy
. = ..()
energy = starting_energy
START_PROCESSING(SSsinguloprocess, src)
SSpoints_of_interest.make_point_of_interest(src)
+ GLOB.poi_list |= src
var/datum/component/singularity/new_component = AddComponent(
/datum/component/singularity, \
@@ -77,10 +72,57 @@
)
+ AddElement(/datum/element/bsa_blocker)
+ RegisterSignal(src, COMSIG_ATOM_BSA_BEAM, .proc/bluespace_reaction)
+
/obj/singularity/Destroy()
STOP_PROCESSING(SSsinguloprocess, src)
+ GLOB.poi_list.Remove(src)
+ GLOB.singularities.Remove(src)
return ..()
+/obj/singularity/Move(atom/newloc, direct)
+ var/turf/T = get_turf(src)
+ for(var/dir in GLOB.cardinals)
+ if(direct & dir)
+ T = get_step(T, dir)
+ if(!T)
+ break
+ // eat the stuff if we're going to move into it so it doesn't mess up our movement
+ for(var/atom/A in T.contents)
+ consume(A)
+ consume(T)
+
+ if(current_size >= STAGE_FIVE || check_turfs_in(direct))
+ last_failed_movement = 0//Reset this because we moved
+ return ..()
+ else
+ last_failed_movement = direct
+ return 0
+
+/obj/singularity/attack_hand(mob/user)
+ consume(user)
+ return TRUE
+
+/obj/singularity/attack_paw(mob/user)
+ consume(user)
+
+/obj/singularity/attack_alien(mob/user)
+ consume(user)
+
+/obj/singularity/attack_animal(mob/user)
+ consume(user)
+
+/obj/singularity/attackby(obj/item/W, mob/user, params)
+ consume(user)
+ return 1
+
+/obj/singularity/Process_Spacemove() //The singularity stops drifting for no man!
+ return 0
+
+/obj/singularity/blob_act(obj/structure/blob/B)
+ return
+
/obj/singularity/attack_tk(mob/user)
if(!iscarbon(user))
return
@@ -154,13 +196,53 @@
time_since_act = 0
if(current_size >= STAGE_TWO)
if(prob(event_chance))
+
+/obj/singularity/bullet_act(obj/projectile/P)
+ qdel(P)
+ return BULLET_ACT_HIT //Will there be an impact? Who knows. Will we see it? No.
+
+
+/obj/singularity/Bump(atom/A)
+ consume(A)
+ if(QDELETED(A)) // don't keep moving into objects that weren't destroyed infinitely
+ step(src, drifting_dir)
+ return
+
+
+/obj/singularity/Bumped(atom/movable/AM)
+ consume(AM)
+
+
+/obj/singularity/process()
+ if(current_size >= STAGE_TWO)
+ move()
+ radiation_pulse(src, min(5000, (energy*4.5)+1000), RAD_DISTANCE_COEFFICIENT*0.5)
+ if(prob(event_chance))//Chance for it to run a special event TODO:Come up with one or two more that fit
event()
- dissipate(delta_time)
+ eat()
+ dissipate()
check_energy()
+ return
-/obj/singularity/proc/dissipate(delta_time)
- if (!dissipate)
+/obj/singularity/attack_ai() //to prevent ais from gibbing themselves when they click on one.
+ return
+
+/obj/singularity/proc/admin_investigate_setup()
+ var/turf/T = get_turf(src)
+ last_warning = world.time
+ var/count = locate(/obj/machinery/field/containment) in urange(30, src, 1)
+ if(!count)
+ message_admins("A singularity has been created without containment fields active at [ADMIN_VERBOSEJMP(T)].")
+ investigate_log("was created at [AREACOORD(T)]. [count?"":"No containment fields were active"]", INVESTIGATE_SINGULO)
+
+/obj/singularity/proc/dissipate()
+ if(!dissipate)
return
+ if(dissipate_track >= dissipate_delay)
+ src.energy -= dissipate_strength
+ dissipate_track = 0
+ else
+ dissipate_track++
time_since_last_dissipiation += delta_time
@@ -174,13 +256,9 @@
if(force_size)
temp_allowed_size = force_size
-
- if(temp_allowed_size >= STAGE_SIX && !consumed_supermatter)
+ if(temp_allowed_size >= STAGE_SIX && !consumedSupermatter)
temp_allowed_size = STAGE_FIVE
- var/new_grav_pull
- var/new_consume_range
-
switch(temp_allowed_size)
if(STAGE_ONE)
current_size = STAGE_ONE
@@ -188,10 +266,10 @@
icon_state = "singularity_s1"
pixel_x = 0
pixel_y = 0
- new_grav_pull = 4
- new_consume_range = 0
+ grav_pull = 4
+ consume_range = 0
dissipate_delay = 10
- time_since_last_dissipiation = 0
+ dissipate_track = 0
dissipate_strength = 1
if(STAGE_TWO)
if(check_cardinals_range(1, TRUE))
@@ -200,10 +278,10 @@
icon_state = "singularity_s3"
pixel_x = -32
pixel_y = -32
- new_grav_pull = 6
- new_consume_range = 1
+ grav_pull = 6
+ consume_range = 1
dissipate_delay = 5
- time_since_last_dissipiation = 0
+ dissipate_track = 0
dissipate_strength = 5
if(STAGE_THREE)
if(check_cardinals_range(2, TRUE))
@@ -212,10 +290,10 @@
icon_state = "singularity_s5"
pixel_x = -64
pixel_y = -64
- new_grav_pull = 8
- new_consume_range = 2
+ grav_pull = 8
+ consume_range = 2
dissipate_delay = 4
- time_since_last_dissipiation = 0
+ dissipate_track = 0
dissipate_strength = 20
if(STAGE_FOUR)
if(check_cardinals_range(3, TRUE))
@@ -224,10 +302,10 @@
icon_state = "singularity_s7"
pixel_x = -96
pixel_y = -96
- new_grav_pull = 10
- new_consume_range = 3
+ grav_pull = 10
+ consume_range = 3
dissipate_delay = 10
- time_since_last_dissipiation = 0
+ dissipate_track = 0
dissipate_strength = 10
if(STAGE_FIVE)//this one also lacks a check for gens because it eats everything
current_size = STAGE_FIVE
@@ -235,26 +313,18 @@
icon_state = "singularity_s9"
pixel_x = -128
pixel_y = -128
- new_grav_pull = 10
- new_consume_range = 4
- dissipate = FALSE //It cant go smaller due to e loss
+ grav_pull = 10
+ consume_range = 4
+ dissipate = 0 //It cant go smaller due to e loss
if(STAGE_SIX) //This only happens if a stage 5 singulo consumes a supermatter shard.
current_size = STAGE_SIX
icon = 'icons/effects/352x352.dmi'
icon_state = "singularity_s11"
pixel_x = -160
pixel_y = -160
- new_grav_pull = 15
- new_consume_range = 5
- dissipate = FALSE
-
- var/datum/component/singularity/resolved_singularity = singularity_component.resolve()
- if (!isnull(resolved_singularity))
- resolved_singularity.consume_range = new_consume_range
- resolved_singularity.grav_pull = new_grav_pull
- resolved_singularity.disregard_failed_movements = current_size >= STAGE_FIVE
- resolved_singularity.roaming = move_self && current_size >= STAGE_TWO
- resolved_singularity.singularity_size = current_size
+ grav_pull = 15
+ consume_range = 5
+ dissipate = 0
if(current_size == allowed_size)
investigate_log("grew to size [current_size].", INVESTIGATE_ENGINE)
@@ -262,13 +332,13 @@
else if(current_size < (--temp_allowed_size))
expand(temp_allowed_size)
else
- return FALSE
+ return 0
/obj/singularity/proc/check_energy()
if(energy <= 0)
investigate_log("collapsed.", INVESTIGATE_ENGINE)
qdel(src)
- return FALSE
+ return 0
switch(energy)//Some of these numbers might need to be changed up later -Mport
if(1 to 199)
allowed_size = STAGE_ONE
@@ -279,22 +349,57 @@
if(1000 to 1999)
allowed_size = STAGE_FOUR
if(2000 to INFINITY)
- if(energy >= 3000 && consumed_supermatter)
+ if(energy >= 3000 && consumedSupermatter)
allowed_size = STAGE_SIX
else
allowed_size = STAGE_FIVE
if(current_size != allowed_size)
expand()
- return TRUE
+ return 1
+
+/obj/singularity/proc/eat()
+ for(var/tile in spiral_range_turfs(grav_pull, src))
+ var/turf/T = tile
+ if(!T || !isturf(loc))
+ continue
+ if(get_dist(T, src) > consume_range)
+ T.singularity_pull(src, current_size)
+ else
+ consume(T)
+ for(var/thing in T)
+ if(isturf(loc) && thing != src)
+ var/atom/movable/X = thing
+ if(get_dist(X, src) > consume_range)
+ X.singularity_pull(src, current_size)
+ else
+ consume(X)
+ CHECK_TICK
+ return
+
-/obj/singularity/proc/consume(atom/thing)
- var/gain = thing.singularity_act(current_size, src)
- energy += gain
- if(istype(thing, /obj/machinery/power/supermatter_crystal) && !consumed_supermatter)
+/obj/singularity/proc/consume(atom/A)
+ var/gain = A.singularity_act(current_size, src)
+ src.energy += gain
+ if(istype(A, /obj/machinery/power/supermatter_crystal) && !consumedSupermatter)
desc = "[initial(desc)] It glows fiercely with inner fire."
name = "supermatter-charged [initial(name)]"
- consumed_supermatter = TRUE
+ consumedSupermatter = 1
set_light(10)
+ return
+
+/obj/singularity/proc/move(force_move = 0)
+ if(!move_self)
+ return 0
+
+ var/drifting_dir = pick(GLOB.alldirs - last_failed_movement)
+
+ if(force_move)
+ drifting_dir = force_move
+
+ if(target && prob(60))
+ drifting_dir = get_dir(src,target) //moves to a singulo beacon, if there is one
+
+ step(src, drifting_dir)
/obj/singularity/proc/check_cardinals_range(steps, retry_with_move = FALSE)
. = length(GLOB.cardinals) //Should be 4.
@@ -305,11 +410,11 @@
if(step(src, i)) //Move in each direction.
if(check_cardinals_range(steps, FALSE)) //New location passes, return true.
return TRUE
- return !.
+ . = !.
/obj/singularity/proc/check_turfs_in(direction = 0, step = 0)
if(!direction)
- return FALSE
+ return 0
var/steps = 0
if(!step)
switch(current_size)
@@ -426,6 +531,7 @@
/obj/singularity/proc/emp_area()
empulse(src, 8, 10)
+ return
/obj/singularity/singularity_act()
var/gain = (energy/2)
@@ -454,3 +560,7 @@
/obj/singularity/deadchat_controlled/Initialize(mapload, starting_energy)
. = ..()
deadchat_plays(mode = DEMOCRACY_MODE)
+
+/obj/singularity/proc/bluespace_reaction()
+ investigate_log("has been shot by bluespace artillery and destroyed.", INVESTIGATE_SINGULO)
+ qdel(src)
diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm
index 34c6b551344d..30e5ba8675cc 100644
--- a/code/modules/power/tesla/energy_ball.dm
+++ b/code/modules/power/tesla/energy_ball.dm
@@ -10,6 +10,176 @@
#define BLOB (STRUCTURE + 1)
#define STRUCTURE (1)
+/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/Initialize(mapload, starting_energy = 50, is_miniball = FALSE)
+ miniball = is_miniball
+ . = ..()
+ if(!is_miniball)
+ set_light(10, 7, "#EEEEFF")
+
+/obj/singularity/energy_ball/ex_act(severity, target)
+ return
+
+/obj/singularity/energy_ball/consume(severity, target)
+ return
+
+/obj/singularity/energy_ball/Destroy()
+ if(orbiting && istype(orbiting.parent, /obj/singularity/energy_ball))
+ var/obj/singularity/energy_ball/EB = orbiting.parent
+ EB.orbiting_balls -= src
+
+ for(var/ball in orbiting_balls)
+ var/obj/singularity/energy_ball/EB = ball
+ QDEL_NULL(EB)
+
+ . = ..()
+
+/obj/singularity/energy_ball/admin_investigate_setup()
+ if(miniball)
+ return //don't annnounce miniballs
+ ..()
+
+
+/obj/singularity/energy_ball/process()
+ if(!orbiting)
+ handle_energy()
+
+ move_the_basket_ball(4 + orbiting_balls.len * 1.5)
+
+ playsound(src.loc, 'sound/magic/lightningbolt.ogg', 100, TRUE, extrarange = 30)
+
+ pixel_x = 0
+ pixel_y = 0
+
+ tesla_zap(src, 7, TESLA_DEFAULT_POWER)
+
+ pixel_x = -32
+ pixel_y = -32
+ 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)
+ else
+ energy = 0 // ensure we dont have miniballs of miniballs
+
+/obj/singularity/energy_ball/examine(mob/user)
+ . = ..()
+ if(orbiting_balls.len)
+ . += "There are [orbiting_balls.len] mini-balls orbiting it."
+
+
+/obj/singularity/energy_ball/proc/move_the_basket_ball(move_amount)
+ //we face the last thing we zapped, so this lets us favor that direction a bit
+ var/move_bias = pick(GLOB.alldirs)
+ for(var/i in 0 to move_amount)
+ var/move_dir = pick(GLOB.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)
+ setDir(move_dir)
+ for(var/mob/living/carbon/C in loc)
+ dust_mobs(C)
+
+
+/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/magic/lightning_chargeup.ogg', 100, TRUE, extrarange = 30)
+ addtimer(CALLBACK(src, .proc/new_mini_ball), 100)
+
+ 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/attack_tk(mob/user)
+ if(iscarbon(user))
+ var/mob/living/carbon/C = user
+ to_chat(C, "That was a shockingly dumb idea.")
+ var/obj/item/organ/brain/rip_u = locate(/obj/item/organ/brain) in C.internal_organs
+ C.ghostize(0)
+ qdel(rip_u)
+ C.death()
+
+/obj/singularity/energy_ball/orbit(obj/singularity/energy_ball/target)
+ if (istype(target))
+ target.orbiting_balls += src
+ GLOB.poi_list -= src
+ target.dissipate_strength = target.orbiting_balls.len
+ . = ..()
+
+/obj/singularity/energy_ball/stop_orbit()
+ if (orbiting && istype(orbiting.parent, /obj/singularity/energy_ball))
+ var/obj/singularity/energy_ball/orbitingball = orbiting.parent
+ orbitingball.orbiting_balls -= src
+ orbitingball.dissipate_strength = orbitingball.orbiting_balls.len
+ . = ..()
+ if (!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 || L.status_flags & GODMODE)
+ 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()
+
/// The Tesla engine
/obj/energy_ball
name = "energy ball"
diff --git a/code/modules/power/tesla/generator.dm b/code/modules/power/tesla/generator.dm
new file mode 100644
index 000000000000..a63e11b4914a
--- /dev/null
+++ b/code/modules/power/tesla/generator.dm
@@ -0,0 +1,12 @@
+/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/zap_act(power, zap_flags)
+ if(zap_flags & ZAP_MACHINE_EXPLOSIVE)
+ energy += power
+ zap_flags &= ~(ZAP_MACHINE_EXPLOSIVE | ZAP_OBJ_DAMAGE) // Don't blow yourself up yeah?
+ return ..()
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index f5a7099cfbea..a49c6e5965ca 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -21,6 +21,22 @@
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+/datum/design/board/circulator
+ name = "Machine Design (Circulator Board)"
+ desc = "The circuit board for a circulator."
+ id = "circulator"
+ build_path = /obj/item/circuitboard/machine/circulator
+ category = list ("Engineering Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/board/teg
+ name = "Machine Design (TEG Board)"
+ desc = "The circuit board for a TEG."
+ id = "teg"
+ build_path = /obj/item/circuitboard/machine/generator
+ category = list ("Engineering Machinery")
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
+
/datum/design/board/announcement_system
name = "Automated Announcement System Board"
desc = "The circuit board for an automated announcement system."
diff --git a/code/modules/research/designs/power_designs.dm b/code/modules/research/designs/power_designs.dm
index 89377a8e1d4e..3f4e99afd794 100644
--- a/code/modules/research/designs/power_designs.dm
+++ b/code/modules/research/designs/power_designs.dm
@@ -127,3 +127,16 @@
RND_CATEGORY_STOCK_PARTS + RND_SUBCATEGORY_STOCK_PARTS_TURBINE
)
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING
+/datum/design/board/pacman/super
+ name = "Machine Design (SUPERPACMAN-type Generator Board)"
+ desc = "The circuit board that for a SUPERPACMAN-type portable generator."
+ id = "superpacman"
+ build_path = /obj/item/circuitboard/machine/pacman/super
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
+
+/datum/design/board/pacman/mrs
+ name = "Machine Design (MRSPACMAN-type Generator Board)"
+ desc = "The circuit board that for a MRSPACMAN-type portable generator."
+ id = "mrspacman"
+ build_path = /obj/item/circuitboard/machine/pacman/mrs
+ departmental_flags = DEPARTMENTAL_FLAG_ENGINEERING
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index c33156361018..577c86a50a08 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -662,6 +662,10 @@
"turbine_compressor",
"turbine_rotor",
"turbine_stator",
+ "circulator",
+ "teg",
+ "superpacman",
+ "mrspacman",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3500)
discount_experiments = list(/datum/experiment/scanning/points/machinery_pinpoint_scan/tier2_capacitors = 2500)
diff --git a/code/modules/station_goals/bsa.dm b/code/modules/station_goals/bsa.dm
index 8f87770b4d33..b5e1d125b73f 100644
--- a/code/modules/station_goals/bsa.dm
+++ b/code/modules/station_goals/bsa.dm
@@ -32,7 +32,7 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE)
return FALSE
/obj/machinery/bsa
- icon = 'icons/obj/machines/particle_accelerator.dmi'
+ icon = 'icons/obj/particle_accelerator.dmi'
density = TRUE
anchored = TRUE
@@ -260,7 +260,7 @@ GLOBAL_VAR_INIT(bsa_unlock, FALSE)
name = "bluespace artillery control"
use_power = NO_POWER_USE
circuit = /obj/item/circuitboard/computer/bsa_control
- icon = 'icons/obj/machines/particle_accelerator.dmi'
+ icon = 'icons/obj/particle_accelerator.dmi'
icon_state = "control_boxp"
icon_keyboard = null
icon_screen = null
diff --git a/icons/obj/engine/singularity.dmi b/icons/obj/engine/singularity.dmi
index e231032f5768..9d11f886accf 100644
Binary files a/icons/obj/engine/singularity.dmi and b/icons/obj/engine/singularity.dmi differ
diff --git a/icons/obj/particle_accelerator.dmi b/icons/obj/particle_accelerator.dmi
new file mode 100644
index 000000000000..c601b8d1b6ad
Binary files /dev/null and b/icons/obj/particle_accelerator.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index a3de34d3c63e..6e8e50a4fe45 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1422,6 +1422,7 @@
#include "code\datums\wires\microwave.dm"
#include "code\datums\wires\mod.dm"
#include "code\datums\wires\mulebot.dm"
+#include "code\datums\wires\particle_accelerator.dm"
#include "code\datums\wires\r_n_d.dm"
#include "code\datums\wires\radio.dm"
#include "code\datums\wires\robot.dm"
@@ -4322,8 +4323,13 @@
#include "code\modules\power\singularity\containment_field.dm"
#include "code\modules\power\singularity\emitter.dm"
#include "code\modules\power\singularity\field_generator.dm"
+#include "code\modules\power\singularity\generator.dm"
#include "code\modules\power\singularity\narsie.dm"
#include "code\modules\power\singularity\singularity.dm"
+#include "code\modules\power\singularity\particle_accelerator\particle.dm"
+#include "code\modules\power\singularity\particle_accelerator\particle_accelerator.dm"
+#include "code\modules\power\singularity\particle_accelerator\particle_control.dm"
+#include "code\modules\power\singularity\particle_accelerator\particle_emitter.dm"
#include "code\modules\power\supermatter\supermatter.dm"
#include "code\modules\power\supermatter\supermatter_extra_effects.dm"
#include "code\modules\power\supermatter\supermatter_gas.dm"
@@ -4336,6 +4342,7 @@
#include "code\modules\power\supermatter\supermatter_delamination\delamination_effects.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\turbine\turbine.dm"
#include "code\modules\power\turbine\turbine_computer.dm"
#include "code\modules\power\turbine\turbine_parts.dm"
diff --git a/tgui/packages/tgui/interfaces/ParticleAccelerator.js b/tgui/packages/tgui/interfaces/ParticleAccelerator.js
new file mode 100644
index 000000000000..04f8ba48104e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ParticleAccelerator.js
@@ -0,0 +1,64 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { Box, Button, LabeledList, Section } from '../components';
+import { Window } from '../layouts';
+
+export const ParticleAccelerator = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ assembled,
+ power,
+ strength,
+ } = data;
+ return (
+
+
+
+
+ act('scan')} />
+ )}>
+
+ {assembled
+ ? "Ready - All parts in place"
+ : "Unable to detect all parts"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file