Files
Batrachophreno 1c8c0c2221 Bugfix pile (#22823)
Fixes several runtimes from the past few weeks, some additional material
repath regressions, and the fact that if I shoot you with a stream of
ionized deuterium particles, it doesn't riddle you with cancer.

changes:
- bugfix: "Fixes emergency shields runtiming when hit by thrown
objects."
  - bugfix: "Fixes several stale global list runtimes."
- bugfix: "Fixes several health percentage checks that could use unset
initial health values."
  - bugfix: "Fixes additional materials regressions in INDRA code."
  - bugfix: "Fixes invalid fuel injector depletion math."
- bugfix: "Makes accelerated particles apply radiation damage to living
mobs they pass through."
2026-07-12 18:06:28 +00:00

591 lines
18 KiB
Plaintext

/**
* BUILDABLE SMES (Superconducting Magnetic Energy Storage) UNIT
* AKA PSUs- both 'SMES unit' and 'PSU' will be used interchangeably in documentation and in-game.
*
* PSUs are linked by the RCON System, allowing for remote management of all linked
* It also supports RCON System which allows you to operate it remotely, if properly set.
*
* This file contains the main code and definition information for SMES units.
* For instantiation of buildable SMES units (the ones you'll see 99% of the time),
* please refer to 'code/modules/power/smes_construction.dm'
*/
#define SMESRATE 0.05
#define SMESMAXCHARGELEVEL 250000
#define SMESMAXOUTPUT 250000
//Cache defines
#define SMES_CLEVEL_1 1
#define SMES_CLEVEL_2 2
#define SMES_CLEVEL_3 3
#define SMES_CLEVEL_4 4
#define SMES_CLEVEL_5 5
#define SMES_OUTPUTTING 6
#define SMES_OUTPUT_ATTEMPT 7
#define SMES_NOT_OUTPUTTING 8
#define SMES_INPUTTING 9
#define SMES_INPUT_ATTEMPT 10
#define SMES_INPUT_MAX 11
/obj/structure/machinery/power/smes
name = "power storage unit"
desc = "A high-capacity superconducting magnetic energy storage (SMES) unit."
icon_state = "smes"
density = 1
anchored = 1
use_power = POWER_USE_OFF
clicksound = SFX_SWITCH
maxhealth = OBJECT_HEALTH_MEDIUM
/// this it to prevent the damage text from playing repeatedly
var/busted = FALSE
/// maximum charge
var/capacity = 5e6
var/charge = 1e6 /// actual charge
var/max_coils = 0
/// 1 = attempting to charge, 0 = not attempting to charge
var/input_attempt = 0
/// 1 = actually inputting, 0 = not inputting
var/inputting = 0
/// amount of power the SMES attempts to charge by
var/input_level = 250000
/// cap on input_level
var/input_level_max = 2000000
/// amount that we received from powernet last tick
var/input_taken = 0
/// 1 = attempting to output, 0 = not attempting to output
var/output_attempt = 0
/// 1 = actually outputting, 0 = not outputting
var/outputting = 0
/// amount of power the SMES attempts to output
var/output_level = 250000
/// cap on output_level
var/output_level_max = 2000000
/// amount of power actually outputted. may be less than output_level if the powernet returns excess power
var/output_used = 0
///Holders for powerout event.
///var/last_output_attempt = 0
///var/last_input_attempt = 0
///var/last_charge = 0
///For icon overlay updates
var/last_disp
var/last_chrg
var/last_onln
var/input_cut = 0
var/input_pulsed = 0
var/output_cut = 0
var/output_pulsed = 0
/// Use by gridcheck event, if set to true we do not disable it
var/is_critical = FALSE
/// Set by gridcheck event, temporarily disables the SMES.
var/failure_timer = 0
var/target_load = 0
var/open_hatch = 0
var/name_tag = null
///Suggestions about how to avoid clickspam building several terminals accepted!
var/building_terminal = 0
var/obj/structure/machinery/power/terminal/terminal = null
/// If this is set to 0 it will send out warning on New()
var/should_be_mapped = 0
var/datum/effect_system/sparks/big_spark
var/datum/effect_system/sparks/small_spark
var/datum/looping_sound/electrical_humming/electrical_humming
var/time = 0
var/charge_mode = 0
var/last_time = 1
/obj/structure/machinery/power/smes/assembly_hints(mob/user, distance, is_adjacent)
. += ..()
if(health < maxhealth)
. += "It can be repaired with a <b>welding tool</b>."
/obj/structure/machinery/power/smes/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
if(is_badly_damaged())
. += SPAN_DANGER("\The [src] is damaged to the point of non-function!")
if(open_hatch)
. += "The maintenance hatch is open."
if (max_coils > 1 && Adjacent(user))
var/list/coils = list()
for(var/obj/item/smes_coil/C in component_parts)
coils += C
. += "The [max_coils] coil slots contain: [counting_english_list(coils)]."
/obj/structure/machinery/power/smes/drain_power(var/drain_check, var/surge, var/amount = 0)
if(drain_check)
return 1
var/smes_amt = min((amount * SMESRATE), charge)
charge -= smes_amt
return smes_amt / SMESRATE
/obj/structure/machinery/power/smes/proc/drain_power_simple(var/amount = 0)
var/power_drawn = between(0, amount, charge)
charge -= power_drawn
return power_drawn
/obj/structure/machinery/power/smes/Destroy()
QDEL_NULL(big_spark)
QDEL_NULL(small_spark)
SSmachinery.smes_units -= src
QDEL_NULL(terminal)
return ..() // TODO: Properly clean up terminal.
/obj/structure/machinery/power/smes/Initialize()
. = ..()
SSmachinery.smes_units += src
big_spark = bind_spark(src, 5, GLOB.alldirs)
small_spark = bind_spark(src, 3)
electrical_humming = new(src)
if(!powernet)
connect_to_network()
dir_loop:
for(var/d in GLOB.cardinals)
var/turf/T = get_step(src, d)
for(var/obj/structure/machinery/power/terminal/term in T)
if(term && term.dir == turn(d, 180))
terminal = term
break dir_loop
if(!terminal)
stat |= BROKEN
return
terminal.master = src
if(!terminal.powernet)
terminal.connect_to_network()
update_icon()
if(!should_be_mapped)
warning("Non-buildable or Non-magical SMES at [src.x]X [src.y]Y [src.z]Z")
/obj/structure/machinery/power/smes/proc/can_function()
if(is_badly_damaged())
return FALSE
if(stat & BROKEN)
return FALSE
return TRUE
/obj/structure/machinery/power/smes/proc/is_badly_damaged()
if(health < maxhealth / 5)
return TRUE
return FALSE
/obj/structure/machinery/power/smes/disconnect_terminal()
if(terminal)
terminal.master = null
terminal = null
return 1
return 0
/obj/structure/machinery/power/smes/update_icon()
ClearOverlays()
var/mutable_appearance/oc
var/mutable_appearance/oc_emis
var/mutable_appearance/og
var/mutable_appearance/og_emis
var/mutable_appearance/op
var/mutable_appearance/op_emis
if(!can_function())
return
if(inputting == (1 || 2))
oc = overlay_image(icon, "[icon_state]-oc[inputting]")
oc_emis = emissive_appearance(icon, "[icon_state]-oc[inputting]")
else if (input_attempt)
oc = overlay_image(icon, "[icon_state]-oc0")
oc_emis = emissive_appearance(icon, "[icon_state]-oc0")
var/clevel = chargedisplay()
if(clevel)
og = overlay_image(icon, "[icon_state]-og[clevel]")
og_emis = emissive_appearance(icon, "[icon_state]-og[clevel]")
if(outputting == 2 || 1)
op = overlay_image(icon, "[icon_state]-op[outputting]")
op_emis = emissive_appearance(icon, "[icon_state]-op[outputting]")
else
op = overlay_image(icon, "[icon_state]-op0")
op_emis = emissive_appearance(icon, "[icon_state]-op0")
AddOverlays(list(
oc,
oc_emis,
og,
og_emis,
op,
op_emis
))
/obj/structure/machinery/power/smes/proc/chargedisplay()
return round(5.5*charge/(capacity ? capacity : 5e6))
/obj/structure/machinery/power/smes/proc/input_power(var/percentage)
var/inputted_power = target_load * (percentage/100)
inputted_power = between(0, inputted_power, target_load)
if(terminal && terminal.powernet)
inputted_power = TERMINAL_POWER_DRAW(inputted_power)
TERMINAL_DRAW_POWER(inputted_power)
charge += inputted_power * SMESRATE
input_taken = inputted_power
if(percentage == 100)
inputting = 2
else if(percentage)
inputting = 1
// else inputting = 0, as set in process()
/obj/structure/machinery/power/smes/proc/update_time()
var/delta_power = input_taken - output_used
delta_power *= SMESRATE
var/goal = (delta_power < 0) ? (charge) : (capacity - charge)
time = world.time + (delta_power ? ((goal / abs(delta_power)) * (world.time - last_time)) : 0)
if(input_cut) // Cannot charge if input wire cut
charge_mode = 0
else if(delta_power < 0) // If we are negative - we are discharging
charge_mode = 0
else if(delta_power != 0)
charge_mode = 1
else
charge_mode = 2
last_time = world.time
/obj/structure/machinery/power/smes/process()
if(!can_function())
if(electrical_humming && electrical_humming.loop_started)
electrical_humming.stop()
return
else
if(electrical_humming && !electrical_humming.loop_started)
electrical_humming.start()
if(failure_timer) // Disabled by gridcheck.
failure_timer--
return
// only update icon if state changed
if(last_disp != chargedisplay() || last_chrg != inputting || last_onln != outputting)
update_icon()
//store machine state to see if we need to update the icon overlays
last_disp = chargedisplay()
last_chrg = inputting
last_onln = outputting
update_time()
//inputting
if(input_attempt && (!input_pulsed && !input_cut))
target_load = min((capacity-charge)/SMESRATE, input_level) // Amount we will request from the powernet.
if(terminal && terminal.powernet)
terminal.powernet.smes_demand += target_load
terminal.powernet.inputting.Add(src)
else
target_load = 0 // We won't input any power without powernet connection.
inputting = 0
//outputting
if(output_attempt && (!output_pulsed && !output_cut) && powernet && charge)
output_used = min( charge/SMESRATE, output_level) //limit output to that stored
charge -= output_used*SMESRATE // reduce the storage (may be recovered in /restore() if excessive)
SMES_ADD_TO_POWERNET(src, output_used) // add output to powernet (smes side)
outputting = 2
else if(!powernet || !charge)
outputting = 1
else
outputting = 0
// called after all power processes are finished
// restores charge level to smes if there was excess this ptick
/obj/structure/machinery/power/smes/proc/restore(var/percent_load)
if(!can_function())
return
if(!outputting)
output_used = 0
return
var/total_restore = output_used * (percent_load / 100) // First calculate amount of power used from our output
total_restore = between(0, total_restore, output_used) // Now clamp the value between 0 and actual output, just for clarity.
total_restore = output_used - total_restore // And, at last, subtract used power from outputted power, to get amount of power we will give back to the SMES.
// now recharge this amount
var/clev = chargedisplay()
charge += total_restore * SMESRATE // restore unused power
powernet.netexcess -= total_restore // remove the excess from the powernet, so later SMESes don't try to use it
output_used -= total_restore
if(clev != chargedisplay() ) //if needed updates the icons overlay
update_icon()
return
//Will return 1 on failure
/obj/structure/machinery/power/smes/proc/make_terminal(const/mob/user)
if (user.loc == loc)
to_chat(user, SPAN_WARNING("You must not be on the same tile as the [src]."))
return 1
//Direction the terminal will face to
var/tempDir = get_dir(user, src)
switch(tempDir)
if (NORTHEAST, SOUTHEAST)
tempDir = EAST
if (NORTHWEST, SOUTHWEST)
tempDir = WEST
var/turf/tempLoc = get_step(src, REVERSE_DIR(tempDir))
if (istype(tempLoc, /turf/space))
to_chat(user, SPAN_WARNING("You can't build a terminal on space."))
return 1
else if (istype(tempLoc))
if(!tempLoc.is_plating())
to_chat(user, SPAN_WARNING("You must remove the floor plating first."))
return 1
to_chat(user, SPAN_NOTICE("You start adding cable to the [src]."))
if(do_after(user, 5 SECONDS, src, DO_REPAIR_CONSTRUCT))
terminal = new /obj/structure/machinery/power/terminal(tempLoc)
terminal.set_dir(tempDir)
terminal.master = src
return 0
return 1
/obj/structure/machinery/power/smes/attack_ai(mob/user)
if(!ai_can_interact(user))
return
add_hiddenprint(user)
ui_interact(user)
/obj/structure/machinery/power/smes/attack_hand(mob/user)
add_fingerprint(user)
ui_interact(user)
/obj/structure/machinery/power/smes/attackby(obj/item/attacking_item, mob/user)
if(attacking_item.tool_behaviour == TOOL_SCREWDRIVER)
if(!open_hatch)
if(is_badly_damaged())
to_chat(user, SPAN_WARNING("\The [src]'s maintenance panel is broken open!"))
return
open_hatch = 1
user.visible_message(\
SPAN_NOTICE("\The [user] opens the maintenance hatch of \the [src]."),\
SPAN_NOTICE("You open the maintenance hatch of \the [src]."),\
range = 4)
return 0
else
open_hatch = 0
user.visible_message(\
SPAN_NOTICE("\The [user] closes the maintenance hatch of \the [src]."),\
SPAN_NOTICE("You close the maintenance hatch of \the [src]."),\
range = 4)
return 0
if (!open_hatch)
to_chat(user, SPAN_WARNING("You need to open access hatch on [src] first!"))
return 0
if(attacking_item.tool_behaviour == TOOL_CABLECOIL && !terminal && !building_terminal)
building_terminal = 1
var/obj/item/stack/cable_coil/CC = attacking_item
if (CC.get_amount() <= 10)
to_chat(user, SPAN_WARNING("You need more cables."))
building_terminal = 0
return 0
if (make_terminal(user))
building_terminal = 0
return 0
building_terminal = 0
CC.use(10)
user.visible_message(\
SPAN_NOTICE("[user.name] has added cables to the [src]."),\
SPAN_NOTICE("You added cables to the [src]."))
terminal.connect_to_network()
stat = 0
return 0
else if(attacking_item.tool_behaviour == TOOL_WIRECUTTER && terminal && !building_terminal)
building_terminal = 1
var/turf/tempTDir = terminal.loc
if (istype(tempTDir))
if(!tempTDir.is_plating())
to_chat(user, SPAN_WARNING("You must remove the floor plating first."))
else
to_chat(user, SPAN_NOTICE("You begin to cut the cables..."))
if(attacking_item.use_tool(src, user, 50, volume = 50))
if (prob(50) && electrocute_mob(usr, terminal.powernet, terminal))
big_spark.queue()
building_terminal = 0
if(usr.stunned)
return 0
new /obj/item/stack/cable_coil(loc,10)
user.visible_message(\
SPAN_NOTICE("[user.name] cut the cables and dismantled the power terminal."),\
SPAN_NOTICE("You cut the cables and dismantle the power terminal."))
qdel(terminal)
building_terminal = 0
return 0
return 1
/obj/structure/machinery/power/smes/ui_data(mob/user)
var/list/data = list()
data["name_tag"] = name_tag
data["charge_taken"] = round(input_taken)
data["charging"] = inputting
data["charge_attempt"] = input_attempt
data["charge_level"] = input_level
data["charge_max"] = input_level_max
data["output_load"] = round(output_used)
data["outputting"] = outputting
data["output_attempt"] = output_attempt
data["output_level"] = output_level
data["output_max"] = output_level_max
data["time"] = time
data["wtime"] = world.time
data["charge_mode"] = charge_mode
data["stored_capacity"] = 0
if(capacity)
data["stored_capacity"] = round(100.0*charge/capacity, 0.1)
data["fail_time"] = failure_timer * 2
return data
/obj/structure/machinery/power/smes/ui_interact(mob/user, var/datum/tgui/ui)
if(!can_function())
if(!terminal)
to_chat(user, SPAN_WARNING("\The [src] is lacking a terminal!"))
return
if(is_badly_damaged())
to_chat(user, SPAN_WARNING("\The [src] is too damaged to function!"))
return
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "SMES", name, 540, 420)
ui.open()
/obj/structure/machinery/power/smes/proc/Percentage()
return round(100.0*charge/capacity, 0.1)
/obj/structure/machinery/power/smes/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("cmode")
inputting(!input_attempt)
update_icon()
. = TRUE
if("online")
outputting(!output_attempt)
update_icon()
. = TRUE
if("reboot")
failure_timer = 0
update_icon()
. = TRUE
if("input")
input_level = clamp(params["input"], 0, input_level_max) // clamp to range
. = TRUE
if("output")
output_level = clamp(params["output"], 0, output_level_max) // clamp to range
. = TRUE
/obj/structure/machinery/power/smes/proc/energy_fail(var/duration)
failure_timer = max(failure_timer, duration)
/obj/structure/machinery/power/smes/proc/ion_act()
if(is_station_level(src.z))
if(prob(1)) //explosion
for(var/mob/M in viewers(src))
M.show_message(SPAN_WARNING("The [src.name] is making strange noises!"), 3, SPAN_WARNING("You hear sizzling electronics."), 2)
sleep(10*pick(4,5,6,7,10,14))
var/datum/effect/effect/system/smoke_spread/smoke = new /datum/effect/effect/system/smoke_spread()
smoke.set_up(3, 0, src.loc)
smoke.attach(src)
smoke.start()
explosion(src.loc, -1, 0, 1, 3, 1, 0)
qdel(src)
return
else if(prob(15)) //Power drain
small_spark.queue()
if(prob(50))
emp_act(EMP_HEAVY)
else
emp_act(EMP_LIGHT)
else if(prob(5)) //smoke only
var/datum/effect/effect/system/smoke_spread/smoke = new /datum/effect/effect/system/smoke_spread()
smoke.set_up(3, 0, src.loc)
smoke.attach(src)
smoke.start()
else
energy_fail(rand(0, 30))
/obj/structure/machinery/power/smes/proc/inputting(var/do_input)
input_attempt = do_input
if(!input_attempt)
inputting = 0
/obj/structure/machinery/power/smes/proc/outputting(var/do_output)
output_attempt = do_output
if(!output_attempt)
outputting = 0
/obj/structure/machinery/power/smes/emp_act(severity)
. = ..()
if(prob(50))
inputting(rand(0,1))
outputting(rand(0,1))
if(prob(50))
output_level = rand(0, output_level_max)
input_level = rand(0, input_level_max)
if(prob(50))
charge -= 1e6/severity
if (charge < 0)
charge = 0
if(prob(50))
energy_fail(rand(0 + (severity * 30),30 + (severity * 30)))
update_icon()
/obj/structure/machinery/power/smes/magical
name = "quantum power storage unit"
desc = "A high-capacity superconducting magnetic energy storage (SMES) unit. Gains energy from quantum entanglement link."
capacity = 5000000
output_level = 250000
should_be_mapped = 1
/obj/structure/machinery/power/smes/magical/process()
charge = 5000000
..()
/obj/structure/machinery/power/smes/buildable/superconducting
name = "superconducting cryogenic capacitor"
desc = "An experimental, extremely high-capacity type of SMES. It uses integrated cryogenic cooling and superconducting cables to break conventional limits on power transfer."
icon_state = "cannon_smes"
charge = 0
max_coils = 12
cur_coils = 12
#undef SMES_CLEVEL_1
#undef SMES_CLEVEL_2
#undef SMES_CLEVEL_3
#undef SMES_CLEVEL_4
#undef SMES_CLEVEL_5
#undef SMES_OUTPUTTING
#undef SMES_OUTPUT_ATTEMPT
#undef SMES_NOT_OUTPUTTING
#undef SMES_INPUTTING
#undef SMES_INPUT_ATTEMPT
#undef SMES_INPUT_MAX