Files
Bubberstation/code/modules/power/solar.dm
Pickle-Coding c1f11f26ce Converts arbitrary energy units to the joule. Fixes conservation of energy issues relating to charging cells. (#81579)
## About The Pull Request
Removes all arbitrary energy and power units in the codebase. Everything
is replaced with the joule and watt, with 1 = 1 joule, or 1 watt if you
are going to multiply by time. This is a visible change, where all
arbitrary energy units you see in the game will get proper prefixed
units of energy.

With power cells being converted to the joule, charging one joule of a
power cell will require one joule of energy.

The grid will now store energy, instead of power. When an energy usage
is described as using the watt, a power to energy conversion based on
the relevant subsystem's timing (usually multiplying by seconds_per_tick
or applying power_to_energy()) is needed before adding or removing from
the grid. Power usages that are described as the watt is really anything
you would scale by time before applying the load. If it's described as a
joule, no time conversion is needed. Players will still read the grid as
power, having no visible change.

Machines that dynamically use power with the use_power() proc will
directly drain from the grid (and apc cell if there isn't enough)
instead of just tallying it up on the dynamic power usages for the area.
This should be more robust at conserving energy as the surplus is
updated on the go, preventing charging cells from nothing.

APCs no longer consume power for the dynamic power usage channels. APCs
will consume power for static power usages. Because static power usages
are added up without checking surplus, static power consumption will be
applied before any machine processes. This will give a more truthful
surplus for dynamic power consumers.

APCs will display how much power it is using for charging the cell. APC
cell charging applies power in its own channel, which gets added up to
the total. This will prevent invisible power usage you see when looking
at the power monitoring console.

After testing in MetaStation, I found roundstart power consumption to be
around 406kW after all APCs get fully charged. During the roundstart APC
charge rush, the power consumption can get as high as over 2MW (up to
25kW per roundstart APC charging) as long as there's that much
available.

Because of the absurd potential power consumption of charging APCs near
roundstart, I have changed how APCs decide to charge. APCs will now
charge only after all other machines have processed in the machines
processing subsystem. This will make sure APC charging won't disrupt
machines taking from the grid, and should stop APCs getting their power
drained due to others demanding too much power while charging. I have
removed the delays for APC charging too, so they start charging
immediately whenever there's excess power. It also stops them turning
red when a small amount of cell gets drained (airlocks opening and shit
during APC charge rush), as they immediately become fully charged
(unless too much energy got drained somehow) before changing icon.

Engineering SMES now start at 100% charge instead of 75%. I noticed
cells were draining earlier than usual after these changes, so I am
making them start maxed to try and combat that.

These changes will fix all conservation of energy issues relating to
charging powercells.
## Why It's Good For The Game
Closes #73438
Closes #75789
Closes #80634
Closes #82031

Makes it much easier to interface with the power system in the codebase.
It's more intuitive. Removes a bunch of conservation of energy issues,
making energy and power much more meaningful. It will help the
simulation remain immersive as players won't encounter energy
duplication so easily. Arbitrary energy units getting replaced with the
joule will also tell people more meaningful information when reading it.
APC charging will feel more snappy.
## Changelog
🆑
fix: Fixes conservation of energy issues relating to charging
powercells.
qol: APCs will display how much power they are using to charge their
cell. This is accounted for in the power monitoring console.
qol: All arbitrary power cell energy units you see are replaced with
prefixed joules.
balance: As a consequence of the conservation of energy issues getting
fixed, the power consumption for charging cells is now very significant.
balance: APCs only use surplus power from the grid after every machine
processes when charging, preventing APCs from causing others to
discharge while charging.
balance: Engineering SMES start at max charge to combat the increased
energy loss due to conservation of energy fixes.
/🆑

---------

Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2024-03-23 16:58:56 +01:00

608 lines
21 KiB
Plaintext

#define SOLAR_GEN_RATE 1500
#define OCCLUSION_DISTANCE 20
#define PANEL_Z_OFFSET 13
#define PANEL_EDGE_Z_OFFSET (PANEL_Z_OFFSET - 2)
/obj/machinery/power/solar
name = "solar panel"
desc = "A solar panel. Generates electricity when in contact with sunlight."
icon = 'icons/obj/machines/solar.dmi'
icon_state = "sp_base"
density = TRUE
use_power = NO_POWER_USE
idle_power_usage = 0
active_power_usage = 0
max_integrity = 150
integrity_failure = 0.33
var/id
var/obscured = FALSE
///`[0-1]` measure of obscuration -- multipllier against power generation
var/sunfrac = 0
///`[0-360)` degrees, which direction are we facing?
var/azimuth_current = 0
var/azimuth_target = 0 //same but what way we're going to face next time we turn
var/obj/machinery/power/solar_control/control
///do we need to turn next tick?
var/needs_to_turn = TRUE
///do we need to call update_solar_exposure() next tick?
var/needs_to_update_solar_exposure = TRUE
var/obj/effect/overlay/panel
var/obj/effect/overlay/panel_edge
/obj/machinery/power/solar/Initialize(mapload, obj/item/solar_assembly/S)
. = ..()
panel_edge = add_panel_overlay("solar_panel_edge", PANEL_EDGE_Z_OFFSET)
panel = add_panel_overlay("solar_panel", PANEL_Z_OFFSET)
Make(S)
connect_to_network()
RegisterSignal(SSsun, COMSIG_SUN_MOVED, PROC_REF(queue_update_solar_exposure))
/obj/machinery/power/solar/Destroy()
unset_control() //remove from control computer
return ..()
/obj/machinery/power/solar/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
. = ..()
if(same_z_layer)
return
SET_PLANE(panel_edge, PLANE_TO_TRUE(panel_edge.plane), new_turf)
SET_PLANE(panel, PLANE_TO_TRUE(panel.plane), new_turf)
/obj/effect/overlay/solar_panel
vis_flags = VIS_INHERIT_ID | VIS_INHERIT_ICON
appearance_flags = TILE_BOUND
blocks_emissive = EMISSIVE_BLOCK_UNIQUE
/obj/machinery/power/solar/proc/add_panel_overlay(icon_state, z_offset)
var/obj/effect/overlay/solar_panel/overlay = new(src)
overlay.icon_state = icon_state
SET_PLANE_EXPLICIT(overlay, ABOVE_GAME_PLANE, src)
overlay.pixel_z = z_offset
vis_contents += overlay
return overlay
/obj/machinery/power/solar/should_have_node()
return TRUE
//set the control of the panel to a given computer
/obj/machinery/power/solar/proc/set_control(obj/machinery/power/solar_control/SC)
unset_control()
control = SC
SC.connected_panels += src
queue_turn(SC.azimuth_target)
//set the control of the panel to null and removes it from the control list of the previous control computer if needed
/obj/machinery/power/solar/proc/unset_control()
if(control)
control.connected_panels -= src
control = null
/obj/machinery/power/solar/proc/Make(obj/item/solar_assembly/S)
if(!S)
S = new /obj/item/solar_assembly(src)
S.glass_type = /obj/item/stack/sheet/glass
S.set_anchored(TRUE)
else
S.forceMove(src)
if(S.glass_type == /obj/item/stack/sheet/rglass) //if the panel is in reinforced glass
max_integrity *= 2 //this need to be placed here, because panels already on the map don't have an assembly linked to
atom_integrity = max_integrity
/obj/machinery/power/solar/crowbar_act(mob/user, obj/item/I)
playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE)
user.visible_message(span_notice("[user] begins to take the glass off [src]."), span_notice("You begin to take the glass off [src]..."))
if(I.use_tool(src, user, 50))
playsound(src.loc, 'sound/items/deconstruct.ogg', 50, TRUE)
user.visible_message(span_notice("[user] takes the glass off [src]."), span_notice("You take the glass off [src]."))
deconstruct(TRUE)
return TRUE
/obj/machinery/power/solar/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
if(BRUTE)
if(machine_stat & BROKEN)
playsound(loc, 'sound/effects/hit_on_shattered_glass.ogg', 60, TRUE)
else
playsound(loc, 'sound/effects/glasshit.ogg', 90, TRUE)
if(BURN)
playsound(loc, 'sound/items/welder.ogg', 100, TRUE)
/obj/machinery/power/solar/atom_break(damage_flag)
. = ..()
if(.)
playsound(loc, 'sound/effects/glassbr3.ogg', 100, TRUE)
unset_control()
// Make sure user can see it's broken
var/new_angle = rand(160, 200)
visually_turn(new_angle)
azimuth_current = new_angle
/obj/machinery/power/solar/on_deconstruction(disassembled)
if(disassembled)
var/obj/item/solar_assembly/S = locate() in src
if(S)
S.forceMove(loc)
S.give_glass(machine_stat & BROKEN)
else
playsound(src, SFX_SHATTER, 70, TRUE)
new /obj/item/shard(src.loc)
new /obj/item/shard(src.loc)
/obj/machinery/power/solar/update_overlays()
. = ..()
panel.icon_state = "solar_panel[(machine_stat & BROKEN) ? "-b" : null]"
panel_edge.icon_state = "solar_panel[(machine_stat & BROKEN) ? "-b" : "_edge"]"
/obj/machinery/power/solar/proc/queue_turn(azimuth)
needs_to_turn = TRUE
azimuth_target = azimuth
/obj/machinery/power/solar/proc/queue_update_solar_exposure()
SIGNAL_HANDLER
needs_to_update_solar_exposure = TRUE //updating right away would be wasteful if we're also turning later
/**
* Get the 2.5D transform for the panel, given an angle
* Arguments:
* * angle - the angle the panel is facing
*/
/obj/machinery/power/solar/proc/get_panel_transform(angle)
// 2.5D solar panel works by using a magic combination of transforms
var/matrix/turner = matrix()
// Rotate towards sun
turner.Turn(angle)
// "Tilt" the panel in 3D towards East and West
turner.Shear(0, -0.6 * sin(angle))
// Make it skinny when facing north (away), fat south
turner.Scale(1, 0.85 * (cos(angle) * -0.5 + 0.5) + 0.15)
return turner
/obj/machinery/power/solar/proc/visually_turn_part(part, angle)
var/mid_azimuth = (azimuth_current + angle) / 2
// actually flip to other direction?
if(abs(angle - azimuth_current) > 180)
mid_azimuth = (mid_azimuth + 180) % 360
// Split into 2 parts so it doesn't distort on large changes
animate(part,
transform = get_panel_transform(mid_azimuth),
time = 2.5 SECONDS, easing = CUBIC_EASING|EASE_IN
)
animate(
transform = get_panel_transform(angle),
time = 2.5 SECONDS, easing = CUBIC_EASING|EASE_OUT
)
/obj/machinery/power/solar/proc/visually_turn(angle)
visually_turn_part(panel, angle)
visually_turn_part(panel_edge, angle)
/obj/machinery/power/solar/proc/update_turn()
needs_to_turn = FALSE
if(azimuth_current != azimuth_target)
visually_turn(azimuth_target)
azimuth_current = azimuth_target
occlusion_setup()
needs_to_update_solar_exposure = TRUE
///trace towards sun to see if we're in shadow
/obj/machinery/power/solar/proc/occlusion_setup()
obscured = TRUE
var/distance = OCCLUSION_DISTANCE
var/target_x = round(sin(SSsun.azimuth), 0.01)
var/target_y = round(cos(SSsun.azimuth), 0.01)
var/x_hit = x
var/y_hit = y
var/turf/hit
for(var/run in 1 to distance)
x_hit += target_x
y_hit += target_y
hit = locate(round(x_hit, 1), round(y_hit, 1), z)
if(IS_OPAQUE_TURF(hit))
return
if(hit.x == 1 || hit.x == world.maxx || hit.y == 1 || hit.y == world.maxy) //edge of the map
break
obscured = FALSE
///calculates the fraction of the sunlight that the panel receives
/obj/machinery/power/solar/proc/update_solar_exposure()
needs_to_update_solar_exposure = FALSE
sunfrac = 0
if(obscured)
return 0
var/sun_azimuth = SSsun.azimuth
if(azimuth_current == sun_azimuth) //just a quick optimization for the most frequent case
. = 1
else
//dot product of sun and panel -- Lambert's Cosine Law
. = cos(azimuth_current - sun_azimuth)
. = clamp(round(., 0.01), 0, 1)
sunfrac = .
/obj/machinery/power/solar/process()
if(machine_stat & BROKEN)
return
// space vines block out sunlight
var/obj/structure/spacevine/vine = locate(/obj/structure/spacevine) in loc
if(istype(vine) && !(/datum/spacevine_mutation/transparency in vine.mutations))
unset_control()
return
if(control && (!powernet || control.powernet != powernet))
unset_control()
if(needs_to_turn)
update_turn()
if(needs_to_update_solar_exposure)
update_solar_exposure()
if(sunfrac <= 0)
return
var/sgen = SOLAR_GEN_RATE * sunfrac
add_avail(power_to_energy(sgen))
if(control)
control.gen += sgen
//Bit of a hack but this whole type is a hack
/obj/machinery/power/solar/fake/Initialize(mapload, obj/item/solar_assembly/S)
. = ..()
UnregisterSignal(SSsun, COMSIG_SUN_MOVED)
/obj/machinery/power/solar/fake/process()
return PROCESS_KILL
//
// Solar Assembly - For construction of solar arrays.
//
/obj/item/solar_assembly
name = "solar panel assembly"
desc = "A solar panel assembly kit, allows constructions of a solar panel, or with a tracking circuit board, a solar tracker."
icon = 'icons/obj/machines/solar.dmi'
icon_state = "sp_base"
inhand_icon_state = "electropack"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
w_class = WEIGHT_CLASS_BULKY // Pretty big!
anchored = FALSE
var/tracker = 0
var/glass_type = null
var/random_offset = 6 //amount in pixels an unanchored assembly may be offset by
/obj/item/solar_assembly/Initialize(mapload)
. = ..()
if(!anchored && !pixel_x && !pixel_y)
randomise_offset(random_offset)
/obj/item/solar_assembly/update_icon_state()
. = ..()
icon_state = tracker ? "tracker_base" : "sp_base"
/obj/item/solar_assembly/proc/randomise_offset(amount)
pixel_x = base_pixel_x + rand(-amount, amount)
pixel_y = base_pixel_y + rand(-amount, amount)
// Give back the glass type we were supplied with
/obj/item/solar_assembly/proc/give_glass(device_broken)
var/atom/Tsec = drop_location()
if(device_broken)
new /obj/item/shard(Tsec)
new /obj/item/shard(Tsec)
else if(glass_type)
new glass_type(Tsec, 2)
glass_type = null
/obj/item/solar_assembly/set_anchored(anchorvalue)
. = ..()
if(isnull(.))
return
randomise_offset(anchored ? 0 : random_offset)
/obj/item/solar_assembly/attackby(obj/item/W, mob/user, params)
if(W.tool_behaviour == TOOL_WRENCH && isturf(loc))
if(isinspace())
to_chat(user, span_warning("You can't secure [src] here."))
return
set_anchored(!anchored)
user.visible_message(
span_notice("[user] [anchored ? null : "un"]wrenches the solar assembly [anchored ? "into place" : null]."),
span_notice("You [anchored ? null : "un"]wrench the solar assembly [anchored ? "into place" : null]."),
)
W.play_tool_sound(src, 75)
return TRUE
if(istype(W, /obj/item/stack/sheet/glass) || istype(W, /obj/item/stack/sheet/rglass))
if(!anchored)
to_chat(user, span_warning("You need to secure the assembly before you can add glass."))
return
var/turf/solarturf = get_turf(src)
if(locate(/obj/machinery/power/solar) in solarturf)
to_chat(user, span_warning("A solar panel is already assembled here."))
return
var/obj/item/stack/sheet/S = W
if(S.use(2))
glass_type = W.type
playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE)
user.visible_message(span_notice("[user] places the glass on the solar assembly."), span_notice("You place the glass on the solar assembly."))
if(tracker)
new /obj/machinery/power/tracker(get_turf(src), src)
else
new /obj/machinery/power/solar(get_turf(src), src)
else
to_chat(user, span_warning("You need two sheets of glass to put them into a solar panel!"))
return
return TRUE
if(!tracker)
if(istype(W, /obj/item/electronics/tracker))
if(!user.temporarilyRemoveItemFromInventory(W))
return
tracker = TRUE
update_appearance()
qdel(W)
user.visible_message(span_notice("[user] inserts the electronics into the solar assembly."), span_notice("You insert the electronics into the solar assembly."))
return TRUE
else
if(W.tool_behaviour == TOOL_CROWBAR)
new /obj/item/electronics/tracker(src.loc)
tracker = FALSE
update_appearance()
user.visible_message(span_notice("[user] takes out the electronics from the solar assembly."), span_notice("You take out the electronics from the solar assembly."))
return TRUE
return ..()
//
// Solar Control Computer
//
/obj/machinery/power/solar_control
name = "solar panel control"
desc = "A controller for solar panel arrays."
icon = 'icons/obj/machines/computer.dmi'
icon_state = "computer"
density = TRUE
use_power = IDLE_POWER_USE
idle_power_usage = BASE_MACHINE_IDLE_CONSUMPTION
max_integrity = 200
integrity_failure = 0.5
var/icon_screen = "solar"
var/icon_keyboard = "power_key"
var/id = 0
var/gen = 0
var/lastgen = 0
var/azimuth_target = 0
var/azimuth_rate = 1 ///degree change per minute
var/track = SOLAR_TRACK_OFF ///SOLAR_TRACK_OFF, SOLAR_TRACK_TIMED, SOLAR_TRACK_AUTO
var/obj/machinery/power/tracker/connected_tracker = null
var/list/connected_panels = list()
///History of power supply
var/list/history = list()
///Size of history, should be equal or bigger than the solar cycle
var/record_size = 0
///Interval between records
var/record_interval = 60 SECONDS
///History record timer
var/next_record = 0
/obj/machinery/power/solar_control/Initialize(mapload)
. = ..()
RegisterSignal(SSsun, COMSIG_SUN_MOVED, PROC_REF(timed_track))
connect_to_network()
if(powernet)
set_panels(azimuth_target)
azimuth_rate = SSsun.base_rotation
record_interval = SSsun.wait
history["supply"] = list()
history["capacity"] = list()
/obj/machinery/power/solar_control/Destroy()
for(var/obj/machinery/power/solar/M in connected_panels)
M.unset_control()
if(connected_tracker)
connected_tracker.unset_control()
return ..()
//search for unconnected panels and trackers in the computer powernet and connect them
/obj/machinery/power/solar_control/proc/search_for_connected()
if(powernet)
for(var/obj/machinery/power/M in powernet.nodes)
if(istype(M, /obj/machinery/power/solar))
// space vines block out sunlight
var/obj/structure/spacevine/vine = locate(/obj/structure/spacevine) in loc
if(istype(vine) && !(/datum/spacevine_mutation/transparency in vine.mutations))
continue
var/obj/machinery/power/solar/S = M
if(!S.control) //i.e unconnected
S.set_control(src)
else if(istype(M, /obj/machinery/power/tracker))
if(!connected_tracker) //if there's already a tracker connected to the computer don't add another
var/obj/machinery/power/tracker/T = M
if(!T.control) //i.e unconnected
T.set_control(src)
///Record the generated power supply and capacity for history
/obj/machinery/power/solar_control/proc/record()
if(record_size == 0)
record_size = 1 + ROUND_UP(360 / (azimuth_rate * abs(SSsun.azimuth_mod))) //History contains full sun cycle
if(world.time >= next_record)
next_record = world.time + record_interval
var/list/supply = history["supply"]
if(powernet)
supply += round(lastgen)
if(supply.len > record_size)
supply.Cut(1, 2)
var/list/capacity = history["capacity"]
if(powernet)
capacity += round(max(connected_panels.len, 1) * SOLAR_GEN_RATE)
if(capacity.len > record_size)
capacity.Cut(1, 2)
/obj/machinery/power/solar_control/update_overlays()
. = ..()
if(machine_stat & NOPOWER)
. += mutable_appearance(icon, "[icon_keyboard]_off")
return
. += mutable_appearance(icon, icon_keyboard)
if(machine_stat & BROKEN)
. += mutable_appearance(icon, "[icon_state]_broken")
return
. += mutable_appearance(icon, icon_screen)
/obj/machinery/power/solar_control/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "SolarControl", name)
ui.open()
/obj/machinery/power/solar_control/ui_data()
var/data = list()
data["supply"] = round(lastgen)
data["capacity"] = connected_panels.len * SOLAR_GEN_RATE
data["azimuth_current"] = azimuth_target
data["azimuth_rate"] = azimuth_rate
data["max_rotation_rate"] = SSsun.base_rotation * 2
data["tracking_state"] = track
data["connected_panels"] = connected_panels.len
data["connected_tracker"] = (connected_tracker ? TRUE : FALSE)
data["history"] = history
return data
/obj/machinery/power/solar_control/ui_act(action, params)
. = ..()
if(.)
return
if(action == "azimuth")
var/adjust = text2num(params["adjust"])
var/value = text2num(params["value"])
if(adjust)
value = azimuth_target + adjust
if(value != null)
set_panels(value)
return TRUE
return FALSE
if(action == "azimuth_rate")
var/adjust = text2num(params["adjust"])
var/value = text2num(params["value"])
if(adjust)
value = azimuth_rate + adjust
if(value != null)
azimuth_rate = round(clamp(value, -2 * SSsun.base_rotation, 2 * SSsun.base_rotation), 0.01)
return TRUE
return FALSE
if(action == "tracking")
var/mode = text2num(params["mode"])
track = mode
if(mode == SOLAR_TRACK_AUTO)
if(connected_tracker)
connected_tracker.sun_update(SSsun, SSsun.azimuth)
else
track = SOLAR_TRACK_OFF
return TRUE
if(action == "refresh")
search_for_connected()
return TRUE
return FALSE
/obj/machinery/power/solar_control/attackby(obj/item/I, mob/living/user, params)
if(I.tool_behaviour == TOOL_SCREWDRIVER)
if(I.use_tool(src, user, 20, volume=50))
if (src.machine_stat & BROKEN)
to_chat(user, span_notice("The broken glass falls out."))
var/obj/structure/frame/computer/A = new /obj/structure/frame/computer( src.loc )
new /obj/item/shard( src.loc )
var/obj/item/circuitboard/computer/solar_control/M = new /obj/item/circuitboard/computer/solar_control( A )
for (var/obj/C in src)
C.forceMove(drop_location())
A.circuit = M
A.state = 3
A.icon_state = "3"
A.set_anchored(TRUE)
qdel(src)
else
to_chat(user, span_notice("You disconnect the monitor."))
var/obj/structure/frame/computer/A = new /obj/structure/frame/computer( src.loc )
var/obj/item/circuitboard/computer/solar_control/M = new /obj/item/circuitboard/computer/solar_control( A )
for (var/obj/C in src)
C.forceMove(drop_location())
A.circuit = M
A.state = 4
A.icon_state = "4"
A.set_anchored(TRUE)
qdel(src)
else if(!user.combat_mode && !(I.item_flags & NOBLUDGEON))
attack_hand(user)
else
return ..()
/obj/machinery/power/solar_control/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
switch(damage_type)
if(BRUTE)
if(machine_stat & BROKEN)
playsound(src.loc, 'sound/effects/hit_on_shattered_glass.ogg', 70, TRUE)
else
playsound(src.loc, 'sound/effects/glasshit.ogg', 75, TRUE)
if(BURN)
playsound(src.loc, 'sound/items/welder.ogg', 100, TRUE)
/obj/machinery/power/solar_control/atom_break(damage_flag)
. = ..()
if(.)
playsound(loc, 'sound/effects/glassbr3.ogg', 100, TRUE)
/obj/machinery/power/solar_control/process()
lastgen = gen
gen = 0
if(connected_tracker && (!powernet || connected_tracker.powernet != powernet))
connected_tracker.unset_control()
record()
///Ran every time the sun updates.
/obj/machinery/power/solar_control/proc/timed_track()
SIGNAL_HANDLER
if(track == SOLAR_TRACK_TIMED)
azimuth_target += azimuth_rate
set_panels(azimuth_target)
///Rotates the panel to the passed angles
/obj/machinery/power/solar_control/proc/set_panels(azimuth)
azimuth = clamp(round(azimuth, 0.01), -360, 719.99)
if(azimuth >= 360)
azimuth -= 360
if(azimuth < 0)
azimuth += 360
azimuth_target = azimuth
for(var/obj/machinery/power/solar/S in connected_panels)
S.queue_turn(azimuth)
//
// MISC
//
/obj/item/paper/guides/jobs/engi/solars
name = "paper- 'Going green! Setup your own solar array instructions.'"
default_raw_text = "<h1>Welcome</h1><p>At greencorps we love the environment, and space. With this package you are able to help mother nature and produce energy without any usage of fossil fuel or plasma! Singularity energy is dangerous while solar energy is safe, which is why it's better. Now here is how you setup your own solar array.</p><p>You can make a solar panel by wrenching the solar assembly onto a cable node. Adding a glass panel, reinforced or regular glass will do, will finish the construction of your solar panel. It is that easy!</p><p>Now after setting up 19 more of these solar panels you will want to create a solar tracker to keep track of our mother nature's gift, the sun. These are the same steps as before except you insert the tracker equipment circuit into the assembly before performing the final step of adding the glass. You now have a tracker! Now the last step is to add a computer to calculate the sun's movements and to send commands to the solar panels to change direction with the sun. Setting up the solar computer is the same as setting up any computer, so you should have no trouble in doing that. You do need to put a wire node under the computer, and the wire needs to be connected to the tracker.</p><p>Congratulations, you should have a working solar array. If you are having trouble, here are some tips. Make sure all solar equipment are on a cable node, even the computer. You can always deconstruct your creations if you make a mistake.</p><p>That's all to it, be safe, be green!</p>"
#undef SOLAR_GEN_RATE
#undef OCCLUSION_DISTANCE
#undef PANEL_Z_OFFSET
#undef PANEL_EDGE_Z_OFFSET