Files
Bubberstation/code/modules/mod/modules/modules_maint.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

350 lines
13 KiB
Plaintext

//Maint modules for MODsuits
///Springlock Mechanism - allows your modsuit to activate faster, but reagents are very dangerous.
/obj/item/mod/module/springlock
name = "MOD springlock module"
desc = "A module that spans the entire size of the MOD unit, sitting under the outer shell. \
This mechanical exoskeleton pushes out of the way when the user enters and it helps in booting \
up, but was taken out of modern suits because of the springlock's tendency to \"snap\" back \
into place when exposed to humidity. You know what it's like to have an entire exoskeleton enter you?"
icon_state = "springlock"
complexity = 3 // it is inside every part of your suit, so
incompatible_modules = list(/obj/item/mod/module/springlock)
/obj/item/mod/module/springlock/on_install()
mod.activation_step_time *= 0.5
/obj/item/mod/module/springlock/on_uninstall(deleting = FALSE)
mod.activation_step_time *= 2
/obj/item/mod/module/springlock/on_suit_activation()
RegisterSignal(mod.wearer, COMSIG_ATOM_EXPOSE_REAGENTS, PROC_REF(on_wearer_exposed))
/obj/item/mod/module/springlock/on_suit_deactivation(deleting = FALSE)
UnregisterSignal(mod.wearer, COMSIG_ATOM_EXPOSE_REAGENTS)
///Signal fired when wearer is exposed to reagents
/obj/item/mod/module/springlock/proc/on_wearer_exposed(atom/source, list/reagents, datum/reagents/source_reagents, methods, volume_modifier, show_message)
SIGNAL_HANDLER
if(!(methods & (VAPOR|PATCH|TOUCH)))
return //remove non-touch reagent exposure
to_chat(mod.wearer, span_danger("[src] makes an ominous click sound..."))
playsound(src, 'sound/items/modsuit/springlock.ogg', 75, TRUE)
addtimer(CALLBACK(src, PROC_REF(snap_shut)), rand(3 SECONDS, 5 SECONDS))
RegisterSignal(mod, COMSIG_MOD_ACTIVATE, PROC_REF(on_activate_spring_block))
///Signal fired when wearer attempts to activate/deactivate suits
/obj/item/mod/module/springlock/proc/on_activate_spring_block(datum/source, user)
SIGNAL_HANDLER
balloon_alert(user, "springlocks aren't responding...?")
return MOD_CANCEL_ACTIVATE
///Delayed death proc of the suit after the wearer is exposed to reagents
/obj/item/mod/module/springlock/proc/snap_shut()
UnregisterSignal(mod, COMSIG_MOD_ACTIVATE)
if(!mod.wearer) //while there is a guaranteed user when on_wearer_exposed() fires, that isn't the same case for this proc
return
mod.wearer.visible_message("[src] inside [mod.wearer]'s [mod.name] snaps shut, mutilating the user inside!", span_userdanger("*SNAP*"))
mod.wearer.emote("scream")
playsound(mod.wearer, 'sound/effects/snap.ogg', 75, TRUE, frequency = 0.5)
playsound(mod.wearer, 'sound/effects/splat.ogg', 50, TRUE, frequency = 0.5)
mod.wearer.client?.give_award(/datum/award/achievement/misc/springlock, mod.wearer)
mod.wearer.apply_damage(500, BRUTE, forced = TRUE, spread_damage = TRUE, sharpness = SHARP_POINTY) //boggers, bogchamp, etc
if(!HAS_TRAIT(mod.wearer, TRAIT_NODEATH))
mod.wearer.investigate_log("has been killed by [src].", INVESTIGATE_DEATHS)
mod.wearer.death() //just in case, for some reason, they're still alive
flash_color(mod.wearer, flash_color = "#FF0000", flash_time = 10 SECONDS)
///Rave Visor - Gives you a rainbow visor and plays jukebox music to you.
/obj/item/mod/module/visor/rave
name = "MOD rave visor module"
desc = "A Super Cool Awesome Visor (SCAV), intended for modular suits."
icon_state = "rave_visor"
complexity = 1
overlay_state_inactive = "module_rave"
/// The client colors applied to the wearer.
var/datum/client_colour/rave_screen
/// The current element in the rainbow_order list we are on.
var/rave_number = 1
/// A list of the colors the module can take.
var/static/list/rainbow_order = list(
list(1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(1,0,0,0, 0,0.5,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(1,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(0,0,0,0, 0,1,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0),
list(0,0,0,0, 0,0.5,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0),
list(1,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1, 0,0,0,0),
)
/// What actually plays music to us
var/datum/jukebox/single_mob/music_player
/obj/item/mod/module/visor/rave/Initialize(mapload)
. = ..()
music_player = new(src)
music_player.sound_loops = TRUE
/obj/item/mod/module/visor/rave/Destroy()
QDEL_NULL(music_player)
QDEL_NULL(rave_screen)
return ..()
/obj/item/mod/module/visor/rave/on_activation()
. = ..()
if(!.)
return
rave_screen = mod.wearer.add_client_colour(/datum/client_colour/rave)
rave_screen.update_colour(rainbow_order[rave_number])
music_player.start_music(mod.wearer)
/obj/item/mod/module/visor/rave/on_deactivation(display_message = TRUE, deleting = FALSE)
. = ..()
if(!.)
return
QDEL_NULL(rave_screen)
if(isnull(music_player.active_song_sound))
return
music_player.unlisten_all()
QDEL_NULL(music_player)
if(deleting)
return
SEND_SOUND(mod.wearer, sound('sound/machines/terminal_off.ogg', volume = 50, channel = CHANNEL_JUKEBOX))
/obj/item/mod/module/visor/rave/generate_worn_overlay(mutable_appearance/standing)
. = ..()
for(var/mutable_appearance/appearance as anything in .)
appearance.color = isnull(music_player.active_song_sound) ? null : rainbow_order[rave_number]
/obj/item/mod/module/visor/rave/on_active_process(seconds_per_tick)
rave_number++
if(rave_number > length(rainbow_order))
rave_number = 1
mod.wearer.update_clothing(mod.slot_flags)
rave_screen.update_colour(rainbow_order[rave_number])
/obj/item/mod/module/visor/rave/get_configuration()
. = ..()
if(length(music_player.songs))
.["selection"] = add_ui_configuration("Song", "list", music_player.selection.song_name, music_player.songs)
/obj/item/mod/module/visor/rave/configure_edit(key, value)
switch(key)
if("selection")
if(!isnull(music_player.active_song_sound))
return
var/datum/track/new_song = music_player.songs[value]
if(QDELETED(src) || !istype(new_song, /datum/track))
return
music_player.selection = new_song
///Tanner - Tans you with spraytan.
/obj/item/mod/module/tanner
name = "MOD tanning module"
desc = "A tanning module for modular suits. Skin cancer functionality has not been ever proven, \
although who knows with the rumors..."
icon_state = "tanning"
module_type = MODULE_USABLE
complexity = 1
use_energy_cost = DEFAULT_CHARGE_DRAIN * 5
incompatible_modules = list(/obj/item/mod/module/tanner)
cooldown_time = 30 SECONDS
/obj/item/mod/module/tanner/on_use()
. = ..()
if(!.)
return
playsound(src, 'sound/machines/microwave/microwave-end.ogg', 50, TRUE)
var/datum/reagents/holder = new()
holder.add_reagent(/datum/reagent/spraytan, 10)
holder.trans_to(mod.wearer, 10, methods = VAPOR)
if(prob(5))
SSradiation.irradiate(mod.wearer)
drain_power(use_energy_cost)
///Balloon Blower - Blows a balloon.
/obj/item/mod/module/balloon
name = "MOD balloon blower module"
desc = "A strange module invented years ago by some ingenious mimes. It blows balloons."
icon_state = "bloon"
module_type = MODULE_USABLE
complexity = 1
use_energy_cost = DEFAULT_CHARGE_DRAIN * 0.5
incompatible_modules = list(/obj/item/mod/module/balloon)
cooldown_time = 15 SECONDS
/obj/item/mod/module/balloon/on_use()
. = ..()
if(!.)
return
if(!do_after(mod.wearer, 10 SECONDS, target = mod))
return FALSE
mod.wearer.adjustOxyLoss(20)
playsound(src, 'sound/items/modsuit/inflate_bloon.ogg', 50, TRUE)
var/obj/item/toy/balloon/balloon = new(get_turf(src))
mod.wearer.put_in_hands(balloon)
drain_power(use_energy_cost)
///Paper Dispenser - Dispenses (sometimes burning) paper sheets.
/obj/item/mod/module/paper_dispenser
name = "MOD paper dispenser module"
desc = "A simple module designed by the bureaucrats of Torch Bay. \
It dispenses 'warm, clean, and crisp sheets of paper' onto a nearby table. Usually."
icon_state = "paper_maker"
module_type = MODULE_USABLE
complexity = 1
use_energy_cost = DEFAULT_CHARGE_DRAIN * 0.5
incompatible_modules = list(/obj/item/mod/module/paper_dispenser)
cooldown_time = 5 SECONDS
/// The total number of sheets created by this MOD. The more sheets, them more likely they set on fire.
var/num_sheets_dispensed = 0
/obj/item/mod/module/paper_dispenser/on_use()
. = ..()
if(!.)
return
if(!do_after(mod.wearer, 1 SECONDS, target = mod))
return FALSE
var/obj/item/paper/crisp_paper = new(get_turf(src))
crisp_paper.desc = "It's crisp and warm to the touch. Must be fresh."
var/obj/structure/table/nearby_table = locate() in range(1, mod.wearer)
playsound(get_turf(src), 'sound/machines/click.ogg', 50, TRUE)
balloon_alert(mod.wearer, "dispensed paper[nearby_table ? " onto table":""]")
mod.wearer.put_in_hands(crisp_paper)
if(nearby_table)
mod.wearer.transferItemToLoc(crisp_paper, nearby_table.drop_location(), silent = FALSE)
// Up to a 30% chance to set the sheet on fire, +2% per sheet made
if(prob(min(num_sheets_dispensed * 2, 30)))
if(crisp_paper in mod.wearer.held_items)
mod.wearer.dropItemToGround(crisp_paper, force = TRUE)
crisp_paper.balloon_alert(mod.wearer, UNLINT("PC LOAD LETTER!"))
crisp_paper.visible_message(span_warning("[crisp_paper] bursts into flames, it's too crisp!"))
crisp_paper.fire_act(1000, 100)
drain_power(use_energy_cost)
num_sheets_dispensed++
///Stamper - Extends a stamp that can switch between accept/deny modes.
/obj/item/mod/module/stamp
name = "MOD stamper module"
desc = "A module installed into the wrist of the suit, this functions as a high-power stamp, \
able to switch between accept and deny modes."
icon_state = "stamp"
module_type = MODULE_ACTIVE
complexity = 1
active_power_cost = DEFAULT_CHARGE_DRAIN * 0.3
device = /obj/item/stamp/mod
incompatible_modules = list(/obj/item/mod/module/stamp)
cooldown_time = 0.5 SECONDS
/obj/item/stamp/mod
name = "MOD electronic stamp"
desc = "A high-power stamp, able to switch between accept and deny mode when used."
/obj/item/stamp/mod/attack_self(mob/user, modifiers)
. = ..()
if(icon_state == "stamp-ok")
icon_state = "stamp-deny"
else
icon_state = "stamp-ok"
balloon_alert(user, "switched mode")
///Atrocinator - Flips your gravity.
/obj/item/mod/module/atrocinator
name = "MOD atrocinator module"
desc = "A mysterious orb that has mysterious effects when inserted in a MODsuit."
icon_state = "atrocinator"
module_type = MODULE_TOGGLE
complexity = 2
active_power_cost = DEFAULT_CHARGE_DRAIN
incompatible_modules = list(/obj/item/mod/module/atrocinator, /obj/item/mod/module/magboot, /obj/item/mod/module/anomaly_locked/antigrav)
cooldown_time = 0.5 SECONDS
overlay_state_inactive = "module_atrocinator"
/// How many steps the user has taken since turning the suit on, used for footsteps.
var/step_count = 0
/// If you use the module on a planetary turf, you fly up. To the sky.
var/you_fucked_up = FALSE
/obj/item/mod/module/atrocinator/on_activation()
. = ..()
if(!.)
return
playsound(src, 'sound/effects/curseattack.ogg', 50)
mod.wearer.AddElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY)
RegisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED, PROC_REF(check_upstairs))
RegisterSignal(mod.wearer, COMSIG_MOB_SAY, PROC_REF(on_talk))
ADD_TRAIT(mod.wearer, TRAIT_SILENT_FOOTSTEPS, MOD_TRAIT)
passtable_on(mod.wearer, MOD_TRAIT)
check_upstairs() //todo at some point flip your screen around
/obj/item/mod/module/atrocinator/on_deactivation(display_message = TRUE, deleting = FALSE)
if(you_fucked_up && !deleting)
to_chat(mod.wearer, span_danger("It's too late."))
return FALSE
. = ..()
if(!.)
return
if(deleting)
playsound(src, 'sound/effects/curseattack.ogg', 50)
qdel(mod.wearer.RemoveElement(/datum/element/forced_gravity, NEGATIVE_GRAVITY))
UnregisterSignal(mod.wearer, COMSIG_MOVABLE_MOVED)
UnregisterSignal(mod.wearer, COMSIG_MOB_SAY)
step_count = 0
REMOVE_TRAIT(mod.wearer, TRAIT_SILENT_FOOTSTEPS, MOD_TRAIT)
passtable_off(mod.wearer, MOD_TRAIT)
var/turf/open/openspace/current_turf = get_turf(mod.wearer)
if(istype(current_turf))
current_turf.zFall(mod.wearer, falling_from_move = TRUE)
/obj/item/mod/module/atrocinator/proc/check_upstairs()
SIGNAL_HANDLER
if(you_fucked_up || mod.wearer.has_gravity() > NEGATIVE_GRAVITY)
return
var/turf/open/current_turf = get_turf(mod.wearer)
var/turf/open/openspace/turf_above = get_step_multiz(mod.wearer, UP)
if(current_turf && istype(turf_above))
current_turf.zFall(mod.wearer)
else if(!turf_above && istype(current_turf) && current_turf.planetary_atmos) //nothing holding you down
INVOKE_ASYNC(src, PROC_REF(fly_away))
else if(!(step_count % 2))
playsound(current_turf, 'sound/items/modsuit/atrocinator_step.ogg', 50)
step_count++
#define FLY_TIME (5 SECONDS)
/obj/item/mod/module/atrocinator/proc/fly_away()
you_fucked_up = TRUE
playsound(src, 'sound/effects/whirthunk.ogg', 75)
to_chat(mod.wearer, span_userdanger("That was stupid."))
investigate_log("has flown off into space due to the [src].", INVESTIGATE_DEATHS)
mod.wearer.Stun(FLY_TIME, ignore_canstun = TRUE)
animate(mod.wearer, FLY_TIME, pixel_z = 256, alpha = 0)
QDEL_IN(mod.wearer, FLY_TIME)
#undef FLY_TIME
/obj/item/mod/module/atrocinator/proc/on_talk(datum/source, list/speech_args)
SIGNAL_HANDLER
speech_args[SPEECH_SPANS] |= "upside_down"
/obj/item/mod/module/recycler/donk/safe
name = "MOD foam dart recycler module"
desc = "A mod module that collects and repackages fired foam darts into half-sized ammo boxes. \
Activate on a nearby turf or storage to unload stored ammo boxes."
icon_state = "donk_safe_recycler"
overlay_state_inactive = "module_donk_safe_recycler"
overlay_state_active = "module_donk_safe_recycler"
complexity = 1
efficiency = 1
allowed_item_types = list(/obj/item/ammo_casing/foam_dart)
ammobox_type = /obj/item/ammo_box/foambox/mini
required_amount = SMALL_MATERIAL_AMOUNT*2.5