Files
Bubberstation/code/modules/mod/mod_control.dm
Useroth 59d74624b1 Upstream power stuff combined and shit (#27284)
* Converts arbitrary energy units to the joule. Fixes conservation of energy issues relating to charging cells. (#81579)

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.

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.

🆑
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>

* Corrects Suit Storage Unit charge rate  (#82192)

## About The Pull Request

Adjusts SSU charge rate according to the new conversion ratio. 

Betcha didn't know SSUs recharge suit and MOD cells? 

This number is actually supposed to be equal to the rate a recharger
station does it.
I don't know if we have some macro for it. 

## Changelog

🆑 Melbert
fix: Fixed Suit Storage Unit cell charging rate
/🆑

* Corrects EVA thermal regulator cell cost (#82195)

## About The Pull Request

Another unit not converted to watts / joules

## Changelog

🆑 Melbert
fix: Fixed space suit thermal regulators cell usage.
/🆑

* Fixing cell power usage (Part 1) (#82197)

## About The Pull Request
Yeah i am not about to create 30 different PR's to address 1 issue at a
time. The changes are small enough to be grouped together in bulk.

This fixes the following issues specified in #82196
- Borg & exosuit RCD (Fixes #82193)
- Motorized wheelchair
- Canister shielding
- Electrolyser
- Potato cell
- Space heater
- Microwave

## Changelog
🆑
fix: Fixed cell energy usage for a bunch of stuff(Part 1). See PR 82197
for details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* Fixing cell power usage (Part 2) (#82198)

## About The Pull Request
Continuation of #82197.

Fixes these issues in #82196
- Cyborg self repair
- Cyborg lollipop dispenser
- Mauna mug
- Plasma cutter (Initial charge not the number of laser shots so partial
fix)

## Changelog
🆑
fix: Fixed more energy usages for cells(Part 2). See PR 82198 for
details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* Fixing cell power usage (Part 3) (#82204)

## About The Pull Request
Continuation of #82198

Fixes these issues in #82196
- Borg hypo spray
- Borg projectile dampen
- Borg chameleon
- Firelance
- MODlink scryer
- Emergency light usage

## Changelog
🆑
fix: Fixed more energy usages for cells(Part 3). See PR 82204 for
details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* [NO GBP]Fixes static power usage not always drawing the remaining energy of an APC cell. (#82205)

## About The Pull Request
Makes APC static power draw consume the remaining energy of the cell if
there's not enough energy.
## Why It's Good For The Game
Prevents a niche issue where an area composed entirely of static power
users with no dynamic users from running forever with no power supply.
## Changelog
🆑
fix: Fixes static power usage from being able to not draw power.
/🆑

* Fixes recharge stations charge rates (#82191)

## About The Pull Request
- Fixes #82190

Have to now use the assigned constants and not magic number `10000`.
Also stuff will take the exact charge needed without any wastage.

## Changelog
🆑
fix: recharge stations draw the same amount of power as before but
directly from grid(without using apc cell power) and won't waste any
excess power
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>

* Fixes space heater power usage (#82208)

## About The Pull Request

Related to https://github.com/tgstation/tgstation/issues/82196 fixes the
space heater power cell usage relating to power per tile heated.

Space heater calculates the amount of power required to heat a tile, but
only uses power at the end of the processing loop. Fixes so the power
consumption matches the calculated usage per tile.

Reverts changes to space heater power efficiency in
https://github.com/tgstation/tgstation/pull/82197 that causes the heater
to instantly drain the cell.

Fixes https://github.com/tgstation/tgstation/issues/82228

## Changelog

🆑 LT3
fix: Fixes Schrödinger's space heater, where a space heater both
consumes a power cell instantly while also not consuming power when
heating adjacent tiles
/🆑

* General maintenance for reagent grinder (#82161)

## About The Pull Request

**1. Qol**
  - Adds examines & screentips for screwdriver, crowbar & wrench acts.
- Adds examines & screentips for inserting, replacing & removing beaker,
Also for inserting items from bags or directly
- Adds an off icon overlay for when the reagent grinder is either
screwed open or loses power,
 
**2. Code Improvements**
- Replaced `attackby()` with `item_interaction()` so we can end the
attack chain early for non combat operations like inserting beakers/
ingredients for grinding etc.
- Removed custom shake animations & replaced it with the global
`Shake()` proc cause it did the same thing
- Removed constructed version of reagent grinder. We instead just check
`mapload` to see if we need the beaker to be created or not for round
start reagent grinders
- Grinding & Juicing use the same `operate_for()` rather than having
seperate procs for each operation
- Removed trait `TRAIT_MAY_CONTAIN_BLENDED_DUST`. Why do we have this?
Its just used to change the grinder description to warn it may contain
dust. It's a waste.
  
**3. Fixes**
- You cannot insert hologram items into the grinder. Rather than
destroying that item & making it vanish you simply won't be allowed to
put it inside the grinder so you can save that item
- You can hit the grinder with items like screwdriver, wrench, crowbar,
beaker & even with stuff you would normally put in the grinder when in
combat mode
  - Adds `can_interact()` checks for using the UI & other stuff 
- Fixes #46356. All items of type `obj/item/grown` can be put from any
bag into the grinder
- The item "and its contents" are now grinded/juiced recursively to get
all the reagents it has to offer just like a real grinder would
- An AI/Human with AI access examining the reagent grinder now actually
works.
 
**4. Refactors**
- The grinder now measures its available capacity based on the "total
weight" of all items present & not its number. This is more realistic
because the grinder has limited space inside & so inserting huge items
should have greater impact rather than deciding on an arbitrary number
like 10(The grinder having the capacity for 10 items of any size inside
its small compartment makes no sense). Examines are displayed to show
how much capacity of the grinder has been filled. Upgrading the grinder
with better matter bins will allow for higher storage capacities.
- Total power consumed is measured based on the duration & weight of all
items grinded cause you know its realistic.


🆑
qol: adds examines & screentips for tool acts & other operations for
reagent grinder
qol: adds an off icon for when the grinder panel is open/not powered
code: auto docs vars & procs. Shared common proc for grinding or juicing
code: removed trait for blended dust, changed some item interactions to
end the attack chain early & save time
fix: no inserting hologram items into the reagent grinder
fix: you can hit the reagent grinder tools like screwdriver, wrench,
crowbar & even beakers/ingredients etc when in combat mode
fix; adds sanity checks for when & how mobs interact with the reagent
grinder
fix: examining a reagent grinder by an AI/Human with AI access now
actually works.
fix: you can insert Nova flowers & other food items from any bag type
fix: reagent grinder now grinds all the contents of an item recursively
to produce maximum reagents like a real grinder would
refactor: reagent grinder now measures available capacity to store items
as total weight of stored items & not number. Capacity can be increased
with upgraded matter bin
refactor: reagent grinder power usage is now a function of duration &
total weight of items blended, meaining blending more number of
items/larger items will consume more power
refactor: reagent grinder code has been optimized overall. Report bugs
on github
/🆑

---------

Co-authored-by: Timberpoes <silent_insomnia_pp@hotmail.co.uk>

* Suit Storage Units / Inducers can charge MODsuits without necessitating them be screwdrivered opened  (#82194)

## About The Pull Request

So MODsuits do this thing here with `get_cell` in that they don't return
anything when they're closed


![image](https://github.com/tgstation/tgstation/assets/51863163/416f8ef5-3bfc-4d2c-a12f-029f051d6692)

And I... can't tell why they do this. 

I looked through every use of `get_cell` and the only things affected by
this are
A. Suit Storage Units, which I believe have always been intended to
charge MODsuits?
and
B. Inducers

So I removed the `open` check. Allowing both Inducers and Suit Storage
Units to charge mods without needing you screwdriver their panel open
first.

I also took the opportunity to allow SSUs to charge multiple items at
once (divvying charge accross all items)

## Why It's Good For The Game

I asked Fikou and they said it was "probably not" intended that you need
to screwdriver them open so yeah.

I think I remember charging my MODs during the original test merges
years back but I can't remember if I opened the suit first when I did or
not.

Either way, it's not super intuitive. Though it's already not very
intuitive that SSUs charge things.

## Changelog

🆑 Melbert
qol: Suit Storage Units charge MODsuits while their cell panel is closed
or open, rather than only when screwed open
qol: Inducers can charge MODsuits while their cell panel is closed or
open, rather than only when screwed open
qol: Suit Storage Units will charge all items within simultaneously (if
possible)
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>

* Fixes modular computer boot-up (#82254)

## About The Pull Request

Fixes a bug where modular computers (specifically PDAs) will fail to
start up if there is zero required application power draw.

PDA will now consume base active power usage during startup.

Related https://github.com/tgstation/tgstation/issues/82196
Fixes https://github.com/tgstation/tgstation/issues/82245
Fixes https://github.com/tgstation/tgstation/issues/82229

## Changelog

🆑 LT3
fix: Fixed modular computers failing to boot up using cell power (eg:
contractor tablet)
/🆑

* Fixing cell power usage (Part 4) (#82227)

## About The Pull Request
Continuation of #82204

Fixes these issues in #82196
- Cyborg Electroadaptive Pseudocircuit
- Defib EMP
- Cell EMP
- `/datum/action/cooldown/mob_cooldown/charge_apc` stuff
- Mecha movement, melee, light ,weapon & tool energy drains
- Ninja drain

## Changelog
🆑
fix: Fixed more energy usages for cells(Part 4). See PR 82227 for
details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* Fixing cell power usage (Part 5) (#82296)

## About The Pull Request
Continuation of #82227

Fixes these issues in
https://github.com/tgstation/tgstation/issues/82196 and others that
weren't noticed.
- Batton emp protection
- Cyborg stun arm
- Cyborg energy sword
- Cyborg hug attack
- Mechanical god religious sect charge check
- Mecha fixes
  - Phasing energy drain
  - Short circuit energy drain
  - Durand shield damage energy drain
  - Plasma engine recharge rate
- Mechbay recharge power rate
- Recharge station charge rate

Stuff that was already working & didn't require fixing.
- Plasma cutter energy shots
- Botany cell charging

## Changelog
🆑
fix: Fixed cell energy usage for a bunch of stuff(Part 5). See PR 82296
for details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* Ties power limit of anchored circuits to 20 * standard cell charge to make it consistent with power changes. (#82287)

## About The Pull Request
It just makes the power requirement 20 * standard cell charge instead of
20000
## Why It's Good For The Game
This is too restrictive to make anything with.



https://github.com/tgstation/tgstation/assets/62126254/e39dcf27-8793-42b0-84a0-7f747e95efcc
## Changelog
🆑
fix: anchored circuits no longer blow up after 2 components are used.
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* Space heater power and heating tweaks (#82344)

## About The Pull Request
- Fixes #82342
- Space heater computes total power for heating adjacent turfs and uses
cell energy once rather multiple times per turf
- Improvised space heater actually works & uses beaker heat capacity and
not a constant of 200 for heating beaker contents

## Changelog 
🆑 SyncIt21,Pickle-Coding
fix: space heater(including improvised) turns off when cell is drained
fix: optimized power usage for both improvised and main space heater.
Improvised heater now works & uses beaker heat capacity
/🆑

* Improved lathe error message (#82260)

## About The Pull Request

Improves the auto/protolathe low charge error message. Instead of simply
saying low power, it will tell you how long until it has enough charge
to print.


![image](https://github.com/tgstation/tgstation/assets/83487515/a2aebd3e-b7bf-4a13-ae7a-6c1cc14c9057)

## Why It's Good For The Game

Less mashing the lathe over and over with no idea how much APC charge it
needs to start printing again

## Changelog

🆑 LT3
code: APCs can now calculate time-to-charge
qol: Overloaded lathes will now tell you the wait time until they're
ready to print again
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>

* Fixes issues with multitools on power objects (#82389)

## About The Pull Request

So at some point the power object's `multitool_act(...)` proc was set to
_always_ block, for what I could find to be no discernable reason.

### The Main Thing


d38f9385b8/code/modules/power/power.dm (L62-L74)
Now, of course, it shouldn't, because this cuts the entire chain short
and thus blocks any other multitool interactions. Like opening the wires
panel with a multitool, in this case. Even if `can_change_cable_layer`
were to be false and thus the object would never actually care about
having this interaction, it'd _still_ block it.

So we don't do that. But what _do_ we do?
I decided to just split off the actual cable changing part into its own
proc, `cable_layer_act(...)`.
```dm
/obj/machinery/power/proc/cable_layer_act(mob/living/user, obj/item/tool)
	var/choice = tgui_input_list(user, "Select Power Line For Operation", "Select Cable Layer", GLOB.cable_name_to_layer)
	if(isnull(choice))
		return ITEM_INTERACT_BLOCKING

	cable_layer = GLOB.cable_name_to_layer[choice]
	balloon_alert(user, "now operating on the [choice]")
	return ITEM_INTERACT_SUCCESS
```
Which is then called on `multitool_act(...)`, if
`can_change_cable_layer` is true.
```dm
/obj/machinery/power/multitool_act(mob/living/user, obj/item/tool)
	if(can_change_cable_layer)
		return cable_layer_act(user, tool)
```
Which continues with the chain if we can't change layers by default, and
otherwise lets `cable_layer_act(...)` work out whether we should block
or continue.
Notably, we've removed the `cable_layer_change_checks(...)` proc from
the equation, and just let inheritors override it to add their own
preconditions and what flags they should return.
On its own this fixes the APC wire panel interactions, but also lets us
just return `NONE` when we need to.
```dm
/obj/machinery/power/emitter/cable_layer_act(mob/living/user, obj/item/tool)
	if(panel_open)
		return NONE
	if(welded)
		balloon_alert(user, "unweld first!")
		return ITEM_INTERACT_BLOCKING
	return ..()
```

### The OTHER Things

While doing this I noticed there's actually very little sanity checks
after we close our input list.
```dm
var/choice = tgui_input_list(user, "Select Power Line For Operation", "Select Cable Layer", GLOB.cable_name_to_layer)
if(isnull(choice))
	return ITEM_INTERACT_BLOCKING
```
We only care about whether we made a choice!
Testing this, lo and behold, this can cause runtimes if the power object
gets qdeleted before you close the menu.
As a funny side, it _also_ doesn't care about whether you're on the
other side of the station, while your multitool is on a different
z-level, or just doesn't exist anymore.
So we just add a few basic sanity checks while we're at it.
```dm
var/choice = tgui_input_list(user, "Select Power Line For Operation", "Select Cable Layer", GLOB.cable_name_to_layer)
if(isnull(choice) || QDELETED(src) || QDELETED(user) || QDELETED(tool) || !user.Adjacent(src) || !user.is_holding(tool))
	return ITEM_INTERACT_BLOCKING
```
That's all. Having done some basic testing, I believe the behaviour is
otherwise unaffected.
## Why It's Good For The Game

It's annoying to need to swap to an empty hand or wirecutters to
interact with APC, emitter, or tesla coil wires.
This fixes that. (Fixes #81745.)
...and then a few other tidbits I realized existed.
## Changelog
🆑
fix: Fix using a multitool on a power object with wires not actually
opening the wires menu when it should.
fix: Fix a runtime from a power object being deleted before selecting
what cable layer to put it at.
fix: Fix power object cable changing not caring about whether you were
still adjacent, still holding your multitool, or whether it even still
existed after the selection menu was closed.
/🆑

* Updates cyborg cells created from borgifier to the SI standard (#82437)

## About The Pull Request

Unchanged value in transformer.dm resulted in borg charge draining to
zero immediately after forced conversion in the borgifer. Changing the
value of robot cell charge to 5 MJs to fix this.
## Why It's Good For The Game

Fixes #82426
## Changelog
🆑
fix: changed value of cell charge from 5000 to 5 megajoules
/🆑

* cell chargers now bypass APCs (#82309)

## About The Pull Request
This makes cell chargers and suit storage units draw from the grid
before the local apc

## Why It's Good For The Game
Upgraded Cell chargers have a charging power of 1MW leading to them
instantly draining the apc of any room they are in,
this Pr makes them draw from the grid preventing immediate blackout. 
This is a stopgap until someone smarter than me changes power values so
a pocket-sized battery won't require the same power to charge then over
600 average suburban homes.

## Changelog
🆑
balance: suit and cell chargers should draw from grid preventing instant
apc blackouts in most cases.
/🆑

* Let ethereals starve again (#82308)

## About The Pull Request

Ethereals use energy as 'food', so of course #81579 had to touch them.
To bring them in line with the new standard, the Ethereal charge levels
were updated to be in megajoules.

466b3df048/code/__DEFINES/mobs.dm (L292-L299)

7fa8daad63/code/__DEFINES/mobs.dm (L285-L292)
However! This forgot to update the rate at which Ethereals passively
discharge.

7fa8daad63/code/modules/surgery/organs/internal/stomach/stomach_ethereal.dm (L11-L14)

7fa8daad63/code/__DEFINES/mobs.dm (L437)
Meaning it's effectively a thousand times less with the new charge
levels.

So we simply update this define to be in kilowatts.
```dm
#define ETHEREAL_CHARGE_FACTOR (0.8 KILO WATTS) //factor at which ethereal's charge decreases per second
```
## Why It's Good For The Game

Fixes issue with ethereal hunger caused by #81579.
## Changelog
🆑
fix: Ethereal starvation has been updated to the new joules/watts
standard. Congratulations Ethereals! You can starve again!
/🆑

* [NO GBP]Fixes ethereal charging and recharge station charge speed. (#82483)

## About The Pull Request
Fixes many instances of things not charging ethereals properly. Scales
all things that are meant for charging/taking from the ethereal stomach
by STANDARD_CELL_CHARGE, so we never run into this issue again. Ethereal
stomachs now store a cell inside them, and uses that for the charge
instead of tracking a variable. Fixes recharging stations not being able
to charge ethereal stomachs. The ethereal signal proc attempted to feed
a callback datum to adjust_charge(), which caused a runtime. Changes
that by invoking the charge_cell callback instead.

Also fixes recharge station charging speed. They weren't converted
correctly. Also formats their charging speed in their description, and
displays power rather than referencing cycles.
## Why It's Good For The Game
So ethereals charge properly.

Closes #82470
## Changelog
🆑
fix: Fixes many instances of energy sources for ethereals supplying a
thousand times less energy than intended.
fix: Fixes recharging stations not being able to charge ethereals.
fix: Fixes recharge stations charging too fast.
qol: Recharge stations display their recharging speed in formatted
power, rather than unformatted energy per cycle.
/🆑

---------

Co-authored-by: san7890 <the@san7890.com>

* Update lead acid batteries charge values (#82510)

## About The Pull Request

So during the whole power consistency update thing, it seems lead acid
batteries were entirely forgotten about.
Which, well, is easy, because they never actually used
`STANDARD_CELL_CHARGE`.

c34d56a45b/code/game/objects/items/maintenance_loot.dm (L32-L33)
Looking into it, the previous value for `STANDARD_CELL_CHARGE` seemed to
have been 1000, so we convert it directly:
```dm
maxcharge = STANDARD_CELL_CHARGE * 20
chargerate = STANDARD_CELL_CHARGE * 1.4
```
But, comparing this to the normal power cells, it seems their charge
rates got _halved_ during the update.
So, we do that too.
```dm
maxcharge = STANDARD_CELL_CHARGE * 20
chargerate = STANDARD_CELL_CHARGE * 0.7
```
And that fixes it.
## Why It's Good For The Game

Fixes lead acid batteries still using the old power amounts, and not
being relative to `STANDARD_CELL_CHARGE`.
## Changelog
🆑
fix: Lead acid batteries have had their power values fixed.
/🆑

* This should do for modular code, for now.

* Fixing cell power usage (Part 5) (#82296)

Continuation of #82227

Fixes these issues in
https://github.com/tgstation/tgstation/issues/82196 and others that
weren't noticed.
- Batton emp protection
- Cyborg stun arm
- Cyborg energy sword
- Cyborg hug attack
- Mechanical god religious sect charge check
- Mecha fixes
  - Phasing energy drain
  - Short circuit energy drain
  - Durand shield damage energy drain
  - Plasma engine recharge rate
- Mechbay recharge power rate
- Recharge station charge rate

Stuff that was already working & didn't require fixing.
- Plasma cutter energy shots
- Botany cell charging

🆑
fix: Fixed cell energy usage for a bunch of stuff(Part 5). See PR 82296
for details
/🆑

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>

* [NO GBP]Fixes some mapped SMES starting with low energy. (#82203)

## About The Pull Request
Scales them all by up to 20 to account for removing the dumb SMESRATE
define. This isn't a 100% conversion for every SMES because it would go
beyond their capacity (old SMES use to duplicate cell energy, so they
had a higher capacity than their cell parts imply). Also removes
instances of varediting their capacity to fucking 1e+600 and replaces
them with magical SMES.
## Why It's Good For The Game
So lavaland and crap don't instantly run out of power. Also I don't
think we should be varediting anything to 1e+600.
## Changelog
🆑
fix: Fixes lavaland SMES and other crap from not starting with enough
energy.
fix: The pirate SMES are now magical instead of secretly infinite.
/🆑

* Mechbay & modsuit recharger tweaks (#82337)

## About The Pull Request
- Both mechbay & modsuit rechargers now waste a small amount of energy
as heat like it did before
- Fixes #82332. Mechbay recharger displays the energy of the mech in
joules & charges the cell with the exact energy required directly from
the grid thus not causing the room to blackout

## Changelog
🆑
fix: Mechbay & modsuit rechargers waste a small amount of energy as heat
fix: Mechbay recharger console displays mech charge as joules & charges
directly from the grid thus sparing the rooms apc cell from huge loads
/🆑

* Refactor APCs interaction chain from attackby to item_interaction (#82390)

## About The Pull Request

For how many lines this is, there's not a lot to really say.
In general, we simply move all item interactions from `attackby(...)` to
`item_interaction(...)`, split each item interaction off into a separate
proc, and make them all return the proper item interaction flags.
We _do_ kill some probably dead code, and remove a call to
`attackby(...)` elsewhere. Then, for clarity, we move the cell check
below the ID check so it can be next to the other item type checks, as
the priority between cell and ID is unlikely to matter anyway.
Other than what's described above and detailed below, each section's
functionality should be the same.

Now, for the parts that _do_ need to be explained more.

### Killing Probably Dead Code

Alright, so, the first part that does not have the cleanest transition.

d38f9385b8/code/modules/power/apc/apc_attack.dm (L22-L23)
Whatever the fuck this is.

Asking around, this seems to just be dead code.
For sanity's sake removing it and testing, silicon interactions with it
seem to work just fine.
So we kill it. We just kill it. We Just Kill It.

Closest we could find requires the distance check there to be false, so
it wouldn't apply. But it _does_ bring us to the second bit of weird
code.

### Calling APC Attackby Elsewhere?
So wallframes let you screwdriver them to put them up, which from a
comment seems to be because of cyborgs.
APC wallframes of course override this with their own implementation,
that allows you to also replace a damaged cover or frame like that!

d38f9385b8/code/game/objects/items/apc_frame.dm (L29-L39)
...By just calling the wholeass `attackby(...)` proc on the APC and
calling it a day.

But hey, this is where our previous splitting up comes in handy, because
we just have a `wallframe_act(...)` proc!
So we just call that instead.
```dm
var/obj/machinery/power/apc/mounted_apc = locate(/obj/machinery/power/apc) in get_turf(user)
mounted_apc.wallframe_act(user, src)
return ITEM_INTERACT_SUCCESS
```
...And not use single letter variables, while we're at it.

That should be all.
Remember to get snacks and drinks.
## Why It's Good For The Game

Split off 178 line `attackby(...)` item interaction chain into separate
procs called in `item_interaction(...)`.
Screwdrivering APC wallframes no longer calls the wholeass
`attackby(...)` on the APC, but just call the new sub-proc for the
specific interaction it cares about.
## Changelog
🆑
refactor: APCs have had their item interaction chain refactored. This
should functionally be the same, but please report any issues.
/🆑

* [NO GBP]Cells will only consider 0.1% of their charge when shocking a user. (#82456)

## About The Pull Request
Makes cells only consider 0.1% of their charge when calculating the
damage for shocking someone. This makes the minimum damage 20, and goes
up to 22 (previous behaviour, even though that's a shockingly small
difference) with a 50 MJ cell, which is the highest capacity crew can
get. This makes it inversely scale with the standard cell charge define,
so if that gets changed, cells will use a different composition of their
charge to consider.

## Why It's Good For The Game

Airlocks instantly critting people when shocked regardless of what's in
the grid wasn't previous behaviour.

* Oops. There we go.

* Oh, these two too.

---------

Co-authored-by: Pickle-Coding <58013024+Pickle-Coding@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: san7890 <the@san7890.com>
Co-authored-by: lessthanthree <83487515+lessthnthree@users.noreply.github.com>
Co-authored-by: Timberpoes <silent_insomnia_pp@hotmail.co.uk>
Co-authored-by: moocowswag <62126254+moocowswag@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: Archie700 <archie712@hotmail.com>
Co-authored-by: Blacklist897 <149209377+Blacklist897@users.noreply.github.com>
2024-04-12 06:33:40 +02:00

787 lines
28 KiB
Plaintext

/// MODsuits, trade-off between armor and utility
/obj/item/mod
name = "Base MOD"
desc = "You should not see this, yell at a coder!"
icon = 'icons/obj/clothing/modsuit/mod_clothing.dmi'
worn_icon = 'icons/mob/clothing/modsuit/mod_clothing.dmi'
/obj/item/mod/control
name = "MOD control unit"
desc = "The control unit of a Modular Outerwear Device, a powered suit that protects against various environments."
icon_state = "standard-control"
inhand_icon_state = "mod_control"
base_icon_state = "control"
w_class = WEIGHT_CLASS_BULKY
slot_flags = ITEM_SLOT_BACK
strip_delay = 10 SECONDS
armor_type = /datum/armor/none
actions_types = list(
/datum/action/item_action/mod/deploy,
/datum/action/item_action/mod/activate,
/datum/action/item_action/mod/sprite_accessories, // SKYRAT EDIT - Hide mutant parts action
/datum/action/item_action/mod/panel,
/datum/action/item_action/mod/module,
/datum/action/item_action/mod/deploy/ai,
/datum/action/item_action/mod/activate/ai,
/datum/action/item_action/mod/panel/ai,
/datum/action/item_action/mod/module/ai,
)
resistance_flags = NONE
max_heat_protection_temperature = SPACE_SUIT_MAX_TEMP_PROTECT
min_cold_protection_temperature = SPACE_SUIT_MIN_TEMP_PROTECT
siemens_coefficient = 0.5
alternate_worn_layer = HANDS_LAYER+0.1 //we want it to go above generally everything, but not hands
/// The MOD's theme, decides on some stuff like armor and statistics.
var/datum/mod_theme/theme = /datum/mod_theme
/// Looks of the MOD.
var/skin = "standard"
/// Theme of the MOD TGUI
var/ui_theme = "ntos"
/// If the suit is deployed and turned on.
var/active = FALSE
/// If the suit wire/module hatch is open.
var/open = FALSE
/// If the suit is ID locked.
var/locked = FALSE
/// If the suit is malfunctioning.
var/malfunctioning = FALSE
/// If the suit is currently activating/deactivating.
var/activating = FALSE
/// How long the MOD is electrified for.
var/seconds_electrified = MACHINE_NOT_ELECTRIFIED
/// If the suit interface is broken.
var/interface_break = FALSE
/// How much module complexity can this MOD carry.
var/complexity_max = DEFAULT_MAX_COMPLEXITY
/// How much module complexity this MOD is carrying.
var/complexity = 0
/// Power usage of the MOD.
var/charge_drain = DEFAULT_CHARGE_DRAIN
/// Slowdown of the MOD when not active.
var/slowdown_inactive = 1.25
/// Slowdown of the MOD when active.
var/slowdown_active = 0.75
/// How long this MOD takes each part to seal.
var/activation_step_time = MOD_ACTIVATION_STEP_TIME
/// Extended description of the theme.
var/extended_desc
/// MOD helmet.
var/obj/item/clothing/head/mod/helmet
/// MOD chestplate.
var/obj/item/clothing/suit/mod/chestplate
/// MOD gauntlets.
var/obj/item/clothing/gloves/mod/gauntlets
/// MOD boots.
var/obj/item/clothing/shoes/mod/boots
/// MOD core.
var/obj/item/mod/core/core
/// Associated list of parts (helmet, chestplate, gauntlets, boots) to their unsealed worn layer.
var/list/mod_parts = list()
/// Associated list of parts that can overslot to their overslot (overslot means the part can cover another layer of clothing).
var/list/overslotting_parts = list()
/// Modules the MOD currently possesses.
var/list/modules = list()
/// Currently used module.
var/obj/item/mod/module/selected_module
/// AI or pAI mob inhabiting the MOD.
var/mob/living/silicon/ai_assistant
/// The MODlink datum, letting us call people from the suit.
var/datum/mod_link/mod_link
/// The starting MODlink frequency, overridden on subtypes that want it to be something.
var/starting_frequency = null
/// Delay between moves as AI.
var/static/movedelay = 0
/// Cooldown for AI moves.
COOLDOWN_DECLARE(cooldown_mod_move)
/// Person wearing the MODsuit.
var/mob/living/carbon/human/wearer
/obj/item/mod/control/Initialize(mapload, datum/mod_theme/new_theme, new_skin, obj/item/mod/core/new_core)
. = ..()
if(!movedelay)
movedelay = CONFIG_GET(number/movedelay/run_delay)
if(new_theme)
theme = new_theme
theme = GLOB.mod_themes[theme]
slot_flags = theme.slot_flags
extended_desc = theme.extended_desc
slowdown_inactive = theme.slowdown_inactive
slowdown_active = theme.slowdown_active
activation_step_time = theme.activation_step_time
complexity_max = theme.complexity_max
ui_theme = theme.ui_theme
charge_drain = theme.charge_drain
set_wires(new /datum/wires/mod(src))
if(length(req_access))
locked = TRUE
new_core?.install(src)
helmet = new /obj/item/clothing/head/mod(src)
mod_parts += helmet
chestplate = new /obj/item/clothing/suit/mod(src)
chestplate.allowed += theme.allowed_suit_storage
mod_parts += chestplate
gauntlets = new /obj/item/clothing/gloves/mod(src)
mod_parts += gauntlets
boots = new /obj/item/clothing/shoes/mod(src)
mod_parts += boots
var/list/all_parts = mod_parts + src
for(var/obj/item/part as anything in all_parts)
part.name = "[theme.name] [part.name]"
part.desc = "[part.desc] [theme.desc]"
part.set_armor(theme.armor_type)
part.resistance_flags = theme.resistance_flags
part.flags_1 |= theme.atom_flags //flags like initialization or admin spawning are here, so we cant set, have to add
part.heat_protection = NONE
part.cold_protection = NONE
part.max_heat_protection_temperature = theme.max_heat_protection_temperature
part.min_cold_protection_temperature = theme.min_cold_protection_temperature
part.siemens_coefficient = theme.siemens_coefficient
for(var/obj/item/part as anything in mod_parts)
RegisterSignal(part, COMSIG_ATOM_DESTRUCTION, PROC_REF(on_part_destruction))
RegisterSignal(part, COMSIG_QDELETING, PROC_REF(on_part_deletion))
set_mod_skin(new_skin || theme.default_skin)
update_speed()
RegisterSignal(src, COMSIG_ATOM_EXITED, PROC_REF(on_exit))
RegisterSignal(src, COMSIG_SPEED_POTION_APPLIED, PROC_REF(on_potion))
for(var/obj/item/mod/module/module as anything in theme.inbuilt_modules)
module = new module(src)
install(module)
START_PROCESSING(SSobj, src)
/obj/item/mod/control/Destroy()
STOP_PROCESSING(SSobj, src)
for(var/obj/item/mod/module/module as anything in modules)
uninstall(module, deleting = TRUE)
for(var/obj/item/part as anything in mod_parts)
overslotting_parts -= part
var/atom/deleting_atom
if(!QDELETED(helmet))
deleting_atom = helmet
helmet = null
mod_parts -= deleting_atom
qdel(deleting_atom)
if(!QDELETED(chestplate))
deleting_atom = chestplate
chestplate = null
mod_parts -= deleting_atom
qdel(deleting_atom)
if(!QDELETED(gauntlets))
deleting_atom = gauntlets
gauntlets = null
mod_parts -= deleting_atom
qdel(deleting_atom)
if(!QDELETED(boots))
deleting_atom = boots
boots = null
mod_parts -= deleting_atom
qdel(deleting_atom)
if(core)
QDEL_NULL(core)
QDEL_NULL(wires)
QDEL_NULL(mod_link)
return ..()
/obj/item/mod/control/atom_destruction(damage_flag)
for(var/obj/item/mod/module/module as anything in modules)
for(var/obj/item/item in module)
item.forceMove(drop_location())
uninstall(module)
for(var/obj/item/part as anything in mod_parts)
if(!overslotting_parts[part])
continue
var/obj/item/overslot = overslotting_parts[part]
overslot.forceMove(drop_location())
overslotting_parts[part] = null
if(ai_assistant)
if(ispAI(ai_assistant))
INVOKE_ASYNC(src, PROC_REF(remove_pai), /* user = */ null, /* forced = */ TRUE) // async to appease spaceman DMM because the branch we don't run has a do_after
else
for(var/datum/action/action as anything in actions)
if(action.owner == ai_assistant)
action.Remove(ai_assistant)
new /obj/item/mod/ai_minicard(drop_location(), ai_assistant)
return ..()
/obj/item/mod/control/examine(mob/user)
. = ..()
if(active)
. += span_notice("Charge: [core ? "[get_charge_percent()]%" : "No core"].")
. += span_notice("Selected module: [selected_module || "None"].")
if(!open && !active)
if(!wearer)
. += span_notice("You could equip it to turn it on.")
. += span_notice("You could open the cover with a <b>screwdriver</b>.")
else if(open)
. += span_notice("You could close the cover with a <b>screwdriver</b>.")
. += span_notice("You could use <b>modules</b> on it to install them.")
. += span_notice("You could remove modules with a <b>crowbar</b>.")
. += span_notice("You could update the access lock with an <b>ID</b>.")
. += span_notice("You could access the wire panel with a <b>wire tool</b>.")
if(core)
. += span_notice("You could remove [core] with a <b>wrench</b>.")
else
. += span_notice("You could use a <b>MOD core</b> on it to install one.")
if(isnull(ai_assistant))
. += span_notice("You could install a pAI with a <b>pAI card</b>.") // SKYRAT EDIT CHANGE - ORIGINAL: . += span_notice("You could install an AI or pAI using their <b>storage card</b>.")
else if(isAI(ai_assistant))
. += span_notice("You could remove [ai_assistant] with an <b>intellicard</b>.")
. += span_notice("You could copy/set link frequency with a <b>multitool</b>.")
. += span_notice("<i>You could examine it more thoroughly...</i>")
/obj/item/mod/control/examine_more(mob/user)
. = ..()
. += "<i>[extended_desc]</i>"
/obj/item/mod/control/process(seconds_per_tick)
if(seconds_electrified > MACHINE_NOT_ELECTRIFIED)
seconds_electrified--
if(mod_link.link_call)
subtract_charge(0.25 * DEFAULT_CHARGE_DRAIN * seconds_per_tick)
if(!active)
return
if(!get_charge() && active && !activating)
power_off()
return
var/malfunctioning_charge_drain = 0
if(malfunctioning)
malfunctioning_charge_drain = rand(0.2 * DEFAULT_CHARGE_DRAIN, 4 * DEFAULT_CHARGE_DRAIN) // About triple power usage on average.
subtract_charge((charge_drain + malfunctioning_charge_drain) * seconds_per_tick)
for(var/obj/item/mod/module/module as anything in modules)
if(malfunctioning && module.active && SPT_PROB(5, seconds_per_tick))
module.on_deactivation(display_message = TRUE)
module.on_process(seconds_per_tick)
/obj/item/mod/control/equipped(mob/user, slot)
..()
if(slot & slot_flags)
set_wearer(user)
else if(wearer)
unset_wearer()
/obj/item/mod/control/dropped(mob/user)
. = ..()
if(!wearer)
return
clean_up()
/obj/item/mod/control/item_action_slot_check(slot)
if(slot & slot_flags)
return TRUE
// Grant pinned actions to pin owners, gives AI pinned actions to the AI and not the wearer
/obj/item/mod/control/grant_action_to_bearer(datum/action/action)
if (!istype(action, /datum/action/item_action/mod/pinnable))
return ..()
var/datum/action/item_action/mod/pinnable/pinned = action
give_item_action(action, pinned.pinner, slot_flags)
/obj/item/mod/control/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if(!wearer || old_loc != wearer || loc == wearer)
return
clean_up()
/obj/item/mod/control/allow_attack_hand_drop(mob/user)
if(user != wearer)
return ..()
for(var/obj/item/part as anything in mod_parts)
if(part.loc != src)
balloon_alert(user, "retract parts first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE, SILENCED_SOUND_EXTRARANGE)
return FALSE
/obj/item/mod/control/MouseDrop(atom/over_object)
if(usr != wearer || !istype(over_object, /atom/movable/screen/inventory/hand))
return ..()
for(var/obj/item/part as anything in mod_parts)
if(part.loc != src)
balloon_alert(wearer, "retract parts first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE, SILENCED_SOUND_EXTRARANGE)
return
// SKYRAT EDIT ADDITION START - Can't remove your MODsuit from your back when it's still active (as it can cause runtimes and even the MODsuit control unit to delete itself)
if(active)
if(!wearer.incapacitated())
balloon_alert(wearer, "deactivate first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, FALSE, SILENCED_SOUND_EXTRARANGE)
return
// SKYRAT ADDITION END
if(!wearer.incapacitated())
var/atom/movable/screen/inventory/hand/ui_hand = over_object
if(wearer.putItemFromInventoryInHandIfPossible(src, ui_hand.held_index))
add_fingerprint(usr)
return ..()
/obj/item/mod/control/wrench_act(mob/living/user, obj/item/wrench)
if(..())
return TRUE
if(seconds_electrified && get_charge() && shock(user))
return TRUE
if(open)
if(!core)
balloon_alert(user, "no core!")
return TRUE
balloon_alert(user, "removing core...")
wrench.play_tool_sound(src, 100)
if(!wrench.use_tool(src, user, 3 SECONDS) || !open)
balloon_alert(user, "interrupted!")
return TRUE
wrench.play_tool_sound(src, 100)
balloon_alert(user, "core removed")
core.forceMove(drop_location())
return TRUE
return ..()
/obj/item/mod/control/screwdriver_act(mob/living/user, obj/item/screwdriver)
. = ..()
if(.)
return TRUE
if(active || activating || ai_controller)
balloon_alert(user, "deactivate suit first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
balloon_alert(user, "[open ? "closing" : "opening"] cover...")
screwdriver.play_tool_sound(src, 100)
if(screwdriver.use_tool(src, user, 1 SECONDS))
if(active || activating)
balloon_alert(user, "deactivate suit first!")
screwdriver.play_tool_sound(src, 100)
balloon_alert(user, "cover [open ? "closed" : "opened"]")
open = !open
else
balloon_alert(user, "interrupted!")
return TRUE
/obj/item/mod/control/crowbar_act(mob/living/user, obj/item/crowbar)
. = ..()
if(!open)
balloon_alert(user, "open the cover first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
if(!allowed(user))
balloon_alert(user, "insufficient access!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return
if(SEND_SIGNAL(src, COMSIG_MOD_MODULE_REMOVAL, user) & MOD_CANCEL_REMOVAL)
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
if(length(modules))
var/list/removable_modules = list()
for(var/obj/item/mod/module/module as anything in modules)
if(!module.removable)
continue
removable_modules += module
var/obj/item/mod/module/module_to_remove = tgui_input_list(user, "Which module to remove?", "Module Removal", removable_modules)
if(!module_to_remove?.mod)
return FALSE
uninstall(module_to_remove)
module_to_remove.forceMove(drop_location())
crowbar.play_tool_sound(src, 100)
SEND_SIGNAL(src, COMSIG_MOD_MODULE_REMOVED, user)
return TRUE
balloon_alert(user, "no modules!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
/obj/item/mod/control/attackby(obj/item/attacking_item, mob/living/user, params)
if(istype(attacking_item, /obj/item/pai_card))
if(!open)
balloon_alert(user, "open the cover first!")
return FALSE
insert_pai(user, attacking_item)
return TRUE
if(istype(attacking_item, /obj/item/mod/module))
if(!open)
balloon_alert(user, "open the cover first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
install(attacking_item, user)
SEND_SIGNAL(src, COMSIG_MOD_MODULE_ADDED, user)
return TRUE
else if(istype(attacking_item, /obj/item/mod/core))
if(!open)
balloon_alert(user, "open the cover first!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
if(core)
balloon_alert(user, "core already installed!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return FALSE
var/obj/item/mod/core/attacking_core = attacking_item
attacking_core.install(src)
balloon_alert(user, "core installed")
playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
return TRUE
else if(is_wire_tool(attacking_item) && open)
wires.interact(user)
return TRUE
else if(open && attacking_item.GetID())
update_access(user, attacking_item.GetID())
return TRUE
return ..()
/obj/item/mod/control/get_cell()
var/obj/item/stock_parts/cell/cell = get_charge_source()
if(!istype(cell))
return null
return cell
/obj/item/mod/control/GetAccess()
if(ai_controller)
return req_access.Copy()
else
return ..()
/obj/item/mod/control/emag_act(mob/user, obj/item/card/emag/emag_card)
locked = !locked
balloon_alert(user, "suit access [locked ? "locked" : "unlocked"]")
return TRUE
/obj/item/mod/control/emp_act(severity)
. = ..()
if(!active || !wearer)
return
to_chat(wearer, span_notice("[severity > 1 ? "Light" : "Strong"] electromagnetic pulse detected!"))
if(. & EMP_PROTECT_CONTENTS)
return
selected_module?.on_deactivation(display_message = TRUE)
wearer.apply_damage(5 / severity, BURN, spread_damage=TRUE)
to_chat(wearer, span_danger("You feel [src] heat up from the EMP, burning you slightly."))
if(wearer.stat < UNCONSCIOUS && prob(10))
wearer.emote("scream")
/obj/item/mod/control/visual_equipped(mob/user, slot, initial = FALSE)
if(slot & slot_flags)
set_wearer(user)
/obj/item/mod/control/on_outfit_equip(mob/living/carbon/human/outfit_wearer, visuals_only, item_slot)
. = ..()
quick_activation()
/obj/item/mod/control/doStrip(mob/stripper, mob/owner)
if(active && !toggle_activate(stripper, force_deactivate = TRUE))
return
for(var/obj/item/part as anything in mod_parts)
if(part.loc == src)
continue
retract(null, part)
return ..()
/obj/item/mod/control/update_icon_state()
icon_state = "[skin]-[base_icon_state][active ? "-sealed" : ""]"
return ..()
/obj/item/mod/control/proc/set_wearer(mob/living/carbon/human/user)
if (wearer == user)
// This should also not happen.
// This path is hit when equipping an outfit with visualsOnly, but only sometimes, and this eventually gets called twice.
// I'm not sure this proc should ever be being called by visualsOnly, but it is,
// and this was an emergency patch.
return
else if (!isnull(wearer))
stack_trace("set_wearer() was called with a new wearer without unset_wearer() being called")
wearer = user
SEND_SIGNAL(src, COMSIG_MOD_WEARER_SET, wearer)
RegisterSignal(wearer, COMSIG_ATOM_EXITED, PROC_REF(on_exit))
RegisterSignal(wearer, COMSIG_SPECIES_GAIN, PROC_REF(on_species_gain))
update_charge_alert()
for(var/obj/item/mod/module/module as anything in modules)
module.on_equip()
/obj/item/mod/control/proc/unset_wearer()
for(var/obj/item/mod/module/module as anything in modules)
module.on_unequip()
UnregisterSignal(wearer, list(COMSIG_ATOM_EXITED, COMSIG_SPECIES_GAIN))
SEND_SIGNAL(src, COMSIG_MOD_WEARER_UNSET, wearer)
wearer.update_spacesuit_hud_icon("0")
wearer = null
/obj/item/mod/control/proc/clean_up()
if(active || activating)
for(var/obj/item/mod/module/module as anything in modules)
if(!module.active)
continue
module.on_deactivation(display_message = FALSE)
for(var/obj/item/part as anything in mod_parts)
seal_part(part, seal = FALSE)
for(var/obj/item/part as anything in mod_parts)
retract(null, part)
if(active)
finish_activation(on = FALSE)
mod_link?.end_call()
var/mob/old_wearer = wearer
unset_wearer()
old_wearer.temporarilyRemoveItemFromInventory(src)
/obj/item/mod/control/proc/on_species_gain(datum/source, datum/species/new_species, datum/species/old_species)
SIGNAL_HANDLER
var/list/all_parts = mod_parts + src
for(var/obj/item/part in all_parts)
if(!(new_species.no_equip_flags & part.slot_flags) || is_type_in_list(new_species, part.species_exception))
continue
forceMove(drop_location())
return
/obj/item/mod/control/proc/quick_module(mob/user)
if(!length(modules))
return
var/list/display_names = list()
var/list/items = list()
for(var/obj/item/mod/module/module as anything in modules)
if(module.module_type == MODULE_PASSIVE)
continue
display_names[module.name] = REF(module)
var/image/module_image = image(icon = module.icon, icon_state = module.icon_state)
if(module == selected_module)
module_image.underlays += image(icon = 'icons/hud/radial.dmi', icon_state = "module_selected")
else if(module.active)
module_image.underlays += image(icon = 'icons/hud/radial.dmi', icon_state = "module_active")
if(!COOLDOWN_FINISHED(module, cooldown_timer))
module_image.add_overlay(image(icon = 'icons/hud/radial.dmi', icon_state = "module_cooldown"))
items += list(module.name = module_image)
if(!length(items))
return
var/radial_anchor = src
if(istype(user.loc, /obj/effect/dummy/phased_mob))
radial_anchor = get_turf(user.loc) //they're phased out via some module, anchor the radial on the turf so it may still display
var/pick = show_radial_menu(user, radial_anchor, items, custom_check = FALSE, require_near = TRUE, tooltips = TRUE)
if(!pick)
return
var/module_reference = display_names[pick]
var/obj/item/mod/module/picked_module = locate(module_reference) in modules
if(!istype(picked_module))
return
picked_module.on_select()
/obj/item/mod/control/proc/shock(mob/living/user)
if(!istype(user) || get_charge() < 1)
return FALSE
do_sparks(5, TRUE, src)
var/check_range = TRUE
return electrocute_mob(user, get_charge_source(), src, 0.7, check_range)
/obj/item/mod/control/proc/install(obj/item/mod/module/new_module, mob/user)
for(var/obj/item/mod/module/old_module as anything in modules)
if(is_type_in_list(new_module, old_module.incompatible_modules) || is_type_in_list(old_module, new_module.incompatible_modules))
if(user)
balloon_alert(user, "[new_module] incompatible with [old_module]!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return
var/complexity_with_module = complexity
complexity_with_module += new_module.complexity
if(complexity_with_module > complexity_max)
if(user)
balloon_alert(user, "[new_module] would make [src] too complex!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return
new_module.forceMove(src)
modules += new_module
complexity += new_module.complexity
new_module.mod = src
new_module.RegisterSignal(src, COMSIG_ITEM_GET_WORN_OVERLAYS, TYPE_PROC_REF(/obj/item/mod/module, add_module_overlay))
new_module.on_install()
if(wearer)
new_module.on_equip()
if(active)
new_module.on_suit_activation()
if(user)
balloon_alert(user, "[new_module] added")
playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
/obj/item/mod/control/proc/uninstall(obj/item/mod/module/old_module, deleting = FALSE)
modules -= old_module
complexity -= old_module.complexity
if(wearer)
old_module.on_unequip()
if(active)
old_module.on_suit_deactivation(deleting = deleting)
if(old_module.active)
old_module.on_deactivation(display_message = !deleting, deleting = deleting)
old_module.UnregisterSignal(src, COMSIG_ITEM_GET_WORN_OVERLAYS)
old_module.on_uninstall(deleting = deleting)
QDEL_LIST_ASSOC_VAL(old_module.pinned_to)
old_module.mod = null
/// Intended for callbacks, don't use normally, just get wearer by itself.
/obj/item/mod/control/proc/get_wearer()
return wearer
/obj/item/mod/control/proc/update_access(mob/user, obj/item/card/id/card)
if(!allowed(user))
balloon_alert(user, "insufficient access!")
playsound(src, 'sound/machines/scanbuzz.ogg', 25, TRUE, SILENCED_SOUND_EXTRARANGE)
return
req_access = card.access.Copy()
balloon_alert(user, "access updated")
/obj/item/mod/control/proc/get_charge_source()
return core?.charge_source()
/obj/item/mod/control/proc/get_charge()
return core?.charge_amount() || 0
/obj/item/mod/control/proc/get_max_charge()
return core?.max_charge_amount() || 1 //avoid dividing by 0
/obj/item/mod/control/proc/get_charge_percent()
return ROUND_UP((get_charge() / get_max_charge()) * 100)
/obj/item/mod/control/proc/add_charge(amount)
return core?.add_charge(amount) || FALSE
/obj/item/mod/control/proc/subtract_charge(amount)
return core?.subtract_charge(amount) || FALSE
/obj/item/mod/control/proc/check_charge(amount)
return core?.check_charge(amount) || FALSE
/**
* Updates the wearer's hud according to the current state of the MODsuit
*/
/obj/item/mod/control/proc/update_charge_alert()
if(isnull(wearer))
return
var/state_to_use
if(!active)
state_to_use = "0"
else if(isnull(core))
state_to_use = "coreless"
else
state_to_use = core.get_charge_icon_state()
wearer.update_spacesuit_hud_icon(state_to_use || "0")
/obj/item/mod/control/proc/update_speed()
var/list/all_parts = mod_parts + src
for(var/obj/item/part as anything in all_parts)
part.slowdown = (active ? slowdown_active : slowdown_inactive) / length(all_parts)
wearer?.update_equipment_speed_mods()
/obj/item/mod/control/proc/power_off()
balloon_alert(wearer, "no power!")
toggle_activate(wearer, force_deactivate = TRUE)
/obj/item/mod/control/proc/set_mod_color(new_color)
var/list/all_parts = mod_parts + src
for(var/obj/item/part as anything in all_parts)
part.remove_atom_colour(WASHABLE_COLOUR_PRIORITY)
part.add_atom_colour(new_color, FIXED_COLOUR_PRIORITY)
wearer?.regenerate_icons()
/obj/item/mod/control/proc/set_mod_skin(new_skin)
if(active)
CRASH("[src] tried to set skin while active!")
skin = new_skin
var/list/used_skin = theme.skins[new_skin]
if(used_skin[CONTROL_LAYER])
alternate_worn_layer = used_skin[CONTROL_LAYER]
var/list/skin_updating = mod_parts + src
for(var/obj/item/part as anything in skin_updating)
part.icon = used_skin[MOD_ICON_OVERRIDE] || 'icons/obj/clothing/modsuit/mod_clothing.dmi'
part.worn_icon = used_skin[MOD_WORN_ICON_OVERRIDE] || 'icons/mob/clothing/modsuit/mod_clothing.dmi'
part.icon_state = "[skin]-[part.base_icon_state]"
for(var/obj/item/clothing/part as anything in mod_parts)
var/used_category
if(part == helmet)
used_category = HELMET_FLAGS
if(part == chestplate)
used_category = CHESTPLATE_FLAGS
if(part == gauntlets)
used_category = GAUNTLETS_FLAGS
if(part == boots)
used_category = BOOTS_FLAGS
var/list/category = used_skin[used_category]
part.clothing_flags = category[UNSEALED_CLOTHING] || NONE
part.visor_flags = category[SEALED_CLOTHING] || NONE
part.flags_inv = category[UNSEALED_INVISIBILITY] || NONE
part.visor_flags_inv = category[SEALED_INVISIBILITY] || NONE
part.flags_cover = category[UNSEALED_COVER] || NONE
part.visor_flags_cover = category[SEALED_COVER] || NONE
part.alternate_worn_layer = category[UNSEALED_LAYER]
mod_parts[part] = part.alternate_worn_layer
/* SKYRAT EDIT START - All MODsuit parts can be worn as overslots.
if(!category[CAN_OVERSLOT])
if(overslotting_parts[part])
var/obj/item/overslot = overslotting_parts[part]
overslot.forceMove(drop_location())
overslotting_parts -= part
continue
*/ // SKYRAT EDIT END
overslotting_parts |= part
wearer?.regenerate_icons()
/obj/item/mod/control/proc/on_exit(datum/source, atom/movable/part, direction)
SIGNAL_HANDLER
if(part.loc == src)
return
if(part == core)
core.uninstall()
return
if(part.loc == wearer)
return
if(part in modules)
uninstall(part)
return
if(part in mod_parts)
if(!wearer)
part.forceMove(src)
return
retract(wearer, part)
if(active)
INVOKE_ASYNC(src, PROC_REF(toggle_activate), wearer, TRUE)
/obj/item/mod/control/proc/on_part_destruction(obj/item/part, damage_flag)
SIGNAL_HANDLER
if(overslotting_parts[part])
var/obj/item/overslot = overslotting_parts[part]
overslot.forceMove(drop_location())
overslotting_parts[part] = null
if(QDELETED(src))
return
atom_destruction(damage_flag)
/obj/item/mod/control/proc/on_part_deletion(obj/item/part) //the part doesnt count as being qdeleted, so our destroying does an infinite loop, fix later
SIGNAL_HANDLER
if(QDELETED(src))
return
qdel(src)
/obj/item/mod/control/proc/on_overslot_exit(datum/source, atom/movable/overslot, direction)
SIGNAL_HANDLER
if(overslot != overslotting_parts[source])
return
overslotting_parts[source] = null
/obj/item/mod/control/proc/on_potion(atom/movable/source, obj/item/slimepotion/speed/speed_potion, mob/living/user)
SIGNAL_HANDLER
if(slowdown_inactive <= 0)
to_chat(user, span_warning("[src] has already been coated with red, that's as fast as it'll go!"))
return SPEED_POTION_STOP
if(active)
to_chat(user, span_warning("It's too dangerous to smear [speed_potion] on [src] while it's active!"))
return SPEED_POTION_STOP
to_chat(user, span_notice("You slather the red gunk over [src], making it faster."))
set_mod_color(COLOR_RED)
slowdown_inactive = 0
slowdown_active = 0
update_speed()
qdel(speed_potion)
return SPEED_POTION_STOP
/// Disables the mod link frequency attached to this unit.
/obj/item/mod/control/proc/disable_modlink()
if(isnull(mod_link))
return
mod_link.end_call()
mod_link.frequency = null