Files
Aurora.3/code/modules/power/power.dm
T
b3b4f4b9c6 INDRA, general nuclear fusion update (#21285)
Fixes https://github.com/Aurorastation/Aurora.3/issues/18951
Fixes https://github.com/Aurorastation/Aurora.3/issues/19447
Fixes https://github.com/Aurorastation/Aurora.3/issues/20283
Fixes https://github.com/Aurorastation/Aurora.3/issues/21395

Overhauls a wide variety of INDRA behaviors and related gas/material
properties.
changes:
- balance: "Fusion reaction modifications. Higher radiation and
temperature output, comparable power output."
- balance: "Increased rad resistance modifier of Borosilicate Windows
from 1 -> 4 and (closed) Blast Doors from 1 -> 6."
- balance: "INDRA Field Strength minima/maxima changed from 1-100 ->
20-120. Power costs increased significantly."
- balance: "INDRA instability increase per tick now limited, similar to
Supermatter (prevents sudden instability spike from instantly nuking
it)."
  - balance: "APC cell charge rate increased 10x."
  - balance: "Rebalanced contents of INDRA hard storage compartment."
  - balance: "SMES coils capacities halved, throughputs doubled."
  - balance: "SMES unit maximum coil capacity increased from 6 -> 8."
- balance: "TEG maximum power output increased from 500000 -> 2500000."
- balance: "Power wasted from exceeding TEG maximum power output
decreased from 50% to 33%."
- balance: "Portable generators of all types have had their power
generation capacity doubled."
- rscadd: "New 'Fusion Codex' app added for all crew with ACCESS_ENGINE
that details all available nuclear fusion chains."
- rscadd: "Boron added as a singleton/reagent. Poisonous to vaurca,
still usable in nuclear fusion as fuel assembly rods."
- rscadd: "INDRA Field Strength now scales plasma temperature entropy,
power output from temperature multiplier, and instability increase per
tick."
- refactor: "'Boron' gas renamed to 'Helium-3'. Boron is not a gas. Deal
with it."
  - refactor: "'Steam' gas renamed to 'Water Vapor'."
  - code_imp: "Descriptions added to gas singletons for future use."
  - qol: "Minor remap of INDRA control room for usability."
  - qol: "Standardized gas canister naming conventions."
- qol: "INDRA and APC TGUI interfaces now use use kW, MW abbreviations
for high wattages."
- bugfix: "INDRA Kinetic Harvester no longer rapidly deselects materials
for harvest when too many are being generated at once."
- bugfix: "INDRA EM Field effect no longer considers the camera within
its reactor chamber a destabilizing influence."
  - bugfix: "INDRA reactor core now produces radiation as intended."
- bugfix: "Deuterium and Tritium canisters no longer contain 50%
standard hydrogen (inheritance issue)."

---------

Signed-off-by: Batrachophreno <Batrochophreno@gmail.com>
Co-authored-by: Matt Atlas <mattiathebest2000@hotmail.it>
Co-authored-by: Geeves <22774890+Geevies@users.noreply.github.com>
2025-10-05 20:13:06 +00:00

377 lines
11 KiB
Plaintext

//////////////////////////////
// POWER MACHINERY BASE CLASS
//////////////////////////////
/////////////////////////////
// Definitions
/////////////////////////////
/obj/machinery/power
name = null
icon = 'icons/obj/power.dmi'
anchored = 1.0
var/datum/powernet/powernet = null
use_power = POWER_USE_OFF
idle_power_usage = 0
active_power_usage = 0
/obj/machinery/power/Destroy()
disconnect_from_network()
disconnect_terminal()
return ..()
///////////////////////////////
// General procedures
//////////////////////////////
// common helper procs for all power machines
/obj/machinery/power/drain_power(var/drain_check, var/surge, var/amount = 0)
if(drain_check)
return 1
if(powernet && powernet.avail)
powernet.trigger_warning()
return powernet.draw_power(amount)
/obj/machinery/power/proc/add_avail(var/amount)
if(powernet)
powernet.newavail += amount
return 1
return 0
/obj/machinery/power/proc/draw_power(var/amount)
if(powernet)
return powernet.draw_power(amount)
return 0
/obj/machinery/power/proc/surplus()
if(powernet)
return powernet.avail-powernet.load
else
return 0
/obj/machinery/power/proc/avail()
if(powernet)
return powernet.avail
else
return 0
// Proc: power_wattage_readable()
// Parameters: 1 (amount - Power in Watts to be converted to W, kW or MW)
// Description: Helper proc that converts reading in Watts to kW or MW (returns string version of amount parameter)
/obj/machinery/proc/power_wattage_readable(var/amount = 0)
var/units = ""
// 10kW and less - Watts
if(amount < 10000)
units = "W"
// 10MW and less - KiloWatts
else if(amount < 10000000)
units = "kW"
amount = (round(amount/100) / 10)
// More than 10MW - MegaWatts
else
units = "MW"
amount = (round(amount/10000) / 100)
if (units == "W")
return "[amount] W"
else
return "[amount] [units]"
/obj/machinery/power/proc/disconnect_terminal() // machines without a terminal will just return, no harm no fowl.
return
// connect the machine to a powernet if a node cable is present on the turf
/obj/machinery/power/proc/connect_to_network()
var/turf/T = src.loc
if(!T || !istype(T))
return 0
var/obj/structure/cable/C = T.get_cable_node() //check if we have a node cable on the machine turf, the first found is picked
if(!C || !C.powernet)
return 0
C.powernet.add_machine(src)
return 1
// remove and disconnect the machine from its current powernet
/obj/machinery/power/proc/disconnect_from_network()
if(!powernet)
return 0
powernet.remove_machine(src)
return 1
// attach a wire to a power machine - leads from the turf you are standing on
//almost never called, overwritten by all power machines but terminal and generator
/obj/machinery/power/attackby(obj/item/attacking_item, mob/user)
if(attacking_item.iscoil())
var/obj/item/stack/cable_coil/coil = attacking_item
var/turf/T = user.loc
if(!T.is_plating() || !istype(T, /turf/simulated/floor))
return
if(get_dist(src, user) > 1)
return
coil.turf_place(T, user)
return
else
..()
return
///////////////////////////////////////////
// Powernet handling helpers
//////////////////////////////////////////
//returns all the cables WITHOUT a powernet in neighbors turfs,
//pointing towards the turf the machine is located at
/obj/machinery/power/proc/get_connections()
. = list()
var/cdir
var/turf/T
for(var/card in GLOB.cardinals)
T = get_step(loc,card)
cdir = get_dir(T,loc)
for(var/obj/structure/cable/C in T)
if(C.powernet) continue
if(C.d1 == cdir || C.d2 == cdir)
. += C
return .
//returns all the cables in neighbors turfs,
//pointing towards the turf the machine is located at
/obj/machinery/power/proc/get_marked_connections()
. = list()
var/cdir
var/turf/T
for(var/card in GLOB.cardinals)
T = get_step(loc,card)
cdir = get_dir(T,loc)
for(var/obj/structure/cable/C in T)
if(C.d1 == cdir || C.d2 == cdir)
. += C
return .
//returns all the NODES (O-X) cables WITHOUT a powernet in the turf the machine is located at
/obj/machinery/power/proc/get_indirect_connections()
. = list()
for(var/obj/structure/cable/C in loc)
if(C.powernet) continue
if(C.d1 == 0) // the cable is a node cable
. += C
return .
///////////////////////////////////////////
// GLOBAL PROCS for powernets handling
//////////////////////////////////////////
// returns a list of all power-related objects (nodes, cable, junctions) in turf,
// excluding source, that match the direction d
// if unmarked==1, only return those with no powernet
/proc/power_list(var/turf/T, var/source, var/d, var/unmarked=0, var/cable_only = 0)
. = list()
var/fdir = (!d)? 0 : turn(d, 180) // the opposite direction to d (or 0 if d==0)
///// Z-Level Stuff
var/Zdir
if(d==11)
Zdir = 11
else if (d==12)
Zdir = 12
else
Zdir = 999
///// Z-Level Stuff
for(var/AM in T)
if(AM == source) continue //we don't want to return source
if(!cable_only && istype(AM,/obj/machinery/power))
var/obj/machinery/power/P = AM
if(P.powernet == 0) continue // exclude APCs which have powernet=0
if(!unmarked || !P.powernet) //if unmarked=1 we only return things with no powernet
if(d == 0)
. += P
else if(istype(AM,/obj/structure/cable))
var/obj/structure/cable/C = AM
if(!unmarked || !C.powernet)
///// Z-Level Stuff
if(C.d1 == fdir || C.d2 == fdir || C.d1 == Zdir || C.d2 == Zdir)
///// Z-Level Stuff
. += C
else if(C.d1 == d || C.d2 == d)
. += C
return .
//remove the old powernet and replace it with a new one throughout the network.
/proc/propagate_network(var/obj/O, var/datum/powernet/PN)
//world.log << "propagating new network"
var/list/worklist = list()
var/list/found_machines = list()
var/index = 1
var/obj/P = null
worklist+=O //start propagating from the passed object
while(index<=worklist.len) //until we've exhausted all power objects
P = worklist[index] //get the next power object found
index++
if( istype(P,/obj/structure/cable))
var/obj/structure/cable/C = P
if(C.powernet != PN) //add it to the powernet, if it isn't already there
PN.add_cable(C)
worklist |= C.get_connections() //get adjacents power objects, with or without a powernet
else if(P.anchored && istype(P,/obj/machinery/power))
var/obj/machinery/power/M = P
found_machines |= M //we wait until the powernet is fully propagates to connect the machines
else
continue
//now that the powernet is set, connect found machines to it
for(var/obj/machinery/power/PM in found_machines)
if(!PM.connect_to_network()) //couldn't find a node on its turf...
PM.disconnect_from_network() //... so disconnect if already on a powernet
//Merge two powernets, the bigger (in cable length term) absorbing the other
/proc/merge_powernets(var/datum/powernet/net1, var/datum/powernet/net2)
if(!net1 || !net2) //if one of the powernet doesn't exist, return
return
if(net1 == net2) //don't merge same powernets
return
//We assume net1 is larger. If net2 is in fact larger we are just going to make them switch places to reduce on code.
if(net1.cables.len < net2.cables.len) //net2 is larger than net1. Let's switch them around
var/temp = net1
net1 = net2
net2 = temp
//merge net2 into net1
for(var/obj/structure/cable/Cable in net2.cables) //merge cables
net1.add_cable(Cable)
if(!net2) return net1
for(var/obj/machinery/power/Node in net2.nodes) //merge power machines
if(!Node.connect_to_network())
Node.disconnect_from_network() //if somehow we can't connect the machine to the new powernet, disconnect it from the old nonetheless
return net1
/**
* Determines how strong could be shock, deals damage to mob, uses power
*
* No animations will be performed by this proc
*
* * victim - The mob who has to be shocked
* * power_source - is a source of electricity, can be power cell, area, apc, cable, powernet or null
* * source - An object caused electrocuting (airlock, grille, etc)
* * siemens_coeff - Layman's terms, conductivity
* * contact_zone - Where the electrocution is happening, see BP_* defines in `code\__DEFINES\mobs.dm`
*
* Returns the amount of energy that was used to electrocute the mob, or `null` / `FALSE` if the electrocution didn't happen
*/
/proc/electrocute_mob(mob/living/carbon/victim, power_source, obj/source, siemens_coeff = 1.0, contact_zone = "hand")
if (!victim)
return 0
var/area/source_area
if(istype(power_source,/area))
source_area = power_source
power_source = source_area.get_apc()
if(istype(power_source,/obj/structure/cable))
var/obj/structure/cable/Cable = power_source
power_source = Cable.powernet
var/datum/powernet/PN
var/obj/item/cell/cell
if(istype(power_source,/datum/powernet))
PN = power_source
else if(istype(power_source,/obj/item/cell))
cell = power_source
else if(istype(power_source,/obj/machinery/power/apc))
var/obj/machinery/power/apc/apc = power_source
cell = apc.cell
if (apc.terminal)
PN = apc.terminal.powernet
else if (!power_source)
return 0
else
log_admin("ERROR: /proc/electrocute_mob([victim], [power_source], [source]): wrong power_source")
return 0
//Triggers powernet warning, but only for 5 ticks (if applicable)
//If following checks determine user is protected we won't alarm for long.
if(PN)
PN.trigger_warning(5)
var/mob/living/carbon/human/H
if(ishuman(victim))
H = victim
if(H)
if(H.species.siemens_coefficient == 0)
return
if(H.species.is_naturally_insulated())
return
if(H.gloves && contact_zone == "hand")
var/obj/item/clothing/gloves/G = H.gloves
if(G.siemens_coefficient == 0) return 0 //to avoid spamming with insulated glvoes on
//Checks again. If we are still here subject will be shocked, trigger standard 20 tick warning
//Since this one is longer it will override the original one.
if(PN)
PN.trigger_warning()
if (!cell && !PN)
return 0
var/PN_damage = 0
var/cell_damage = 0
if (PN)
PN_damage = PN.get_electrocute_damage()
if (cell)
cell_damage = cell.get_electrocute_damage()
var/shock_damage = 0
if (PN_damage>=cell_damage)
power_source = PN
shock_damage = PN_damage
else
power_source = cell
shock_damage = cell_damage
var/drained_hp
var/touchy_hand
if(contact_zone == "hand")
if(victim.hand)
touchy_hand = BP_R_HAND
else
touchy_hand = BP_L_HAND
if(!touchy_hand)
drained_hp = victim.electrocute_act(shock_damage, source, siemens_coeff, ground_zero = contact_zone) //zzzzzzap!
else
drained_hp = victim.electrocute_act(shock_damage, source, siemens_coeff, ground_zero = touchy_hand) //zzzzzzap!
var/drained_energy = drained_hp*20
if (source_area)
source_area.use_power_oneoff(drained_energy/CELLRATE)
else if (istype(power_source,/datum/powernet))
var/drained_power = drained_energy/CELLRATE
drained_power = PN.draw_power(drained_power)
else if (istype(power_source, /obj/item/cell))
cell.use(drained_energy)
return drained_energy