Files
Bubberstation/code/modules/power/power_store.dm
SmArtKar dbadf01a35 Readds sovlful IEDs and adds assembly beakerbombs (#94965)
## About The Pull Request

This PR sort-of brings back old IEDs, albeit much more freeform compared
to their previous iteration. Using #94861's ``spark_act`` interactions
they can now be made by filling up a soda can with a variety of
chemicals (welding fuel being the easiest to obtain), dunking in a piece
of wire, taping it up (optional, required if using welding fuel or
plasma) and lighting it on fire with a lighter or welding tool (anything
that's hot enough works, really)
They have a random delay of 2-4 seconds, and can be disarmed by snipping
the fuse with wirecutters in time before they detonate (or don't,
depending on the mixture)

There's also a new, more "professional" improvised chemical explosive in
the form of beakerbombs. These can be assembled by putting a lid on a
beaker (alt-click, prevents the beaker from spilling its contents when
thrown) and attaching an assembly with an igniter or a condenser to it.

<img width="150" height="90" alt="dreamseeker_2OKpZeN7ay"
src="https://github.com/user-attachments/assets/599e75ea-9653-4db9-a915-b2ca6307635f"
/>

When triggered, igniters will heat the reagents up by a bit while
condensers will cool them down. However, you can also attach and wire up
a power cell, which will cause it to dump all of its power into the
beaker when the igniter fires off, triggering ``spark_act`` interactions
potentially causing a larger explosion.

<img width="534" height="447" alt="dreamseeker_k9Uzf18Eoa"
src="https://github.com/user-attachments/assets/4cfed5b4-3875-4522-9bfa-5f6a3c8e0864"
/>

Decently sized boom from The Contraption.

Plasma and welding fuel's potency inside of beakerbombs/soda cans is
significantly reduced compared to other riggable objects (strengthdiv is
3 times higher) as they're very easy to obtain and would make for very
powerful explosives, considering their very strong strengthdiv as most
rigging interactions use very little fuel/plasma to cause a large
explosion, which was carried over to the new system. (Don't worry, this
still leaves them at sensible values with welding fuel being about as
strong as old IEDs on average)

Closes #94990 via having plasma's electrical power modifier have a
decline past a certain point based on its volume

## Why It's Good For The Game

#81529 didn't justify IED removal whatsoever and I think with new
mechanics these can be used as easier (but weaker) to make improvised
bombs, being much less clunky to make and use than pipebombs.
Beakerbombs are essentially a somewhat weaker (even with metamat
beakers, you're still 20u short of a normal large beaker grenade at
200u) form of grenades, but have access to new interactions involving
charged up explosions, which could make for some variety among chemists'
weaponry.

## Changelog
🆑
add: Added back IEDs made by attaching some wire and tape to a soda can
filled with fuel, plasma, or any other explosive of your choice. They
need to be lit on fire with a lighter or a welding tool.
add: You can now attach a lid to beakers with alt-click, preventing them
from being spilled when thrown.
add: Added beakerbombs, made by attaching an assembly (with optional
power cell and wiring) to a lidded beaker.
balance: Rigged explosions now create flames.
balance: Plasma explosions now limit their explosion potency past a
certain point based on their volume
/🆑
2026-01-28 09:11:50 +13:00

347 lines
12 KiB
Plaintext

#define ETHEREAL_CELL_DRAIN_TIME (3.5 SECONDS)
#define ETHEREAL_CELL_POWER_DRAIN (0.75 * STANDARD_CELL_CHARGE)
/// The factor by which we multiply drain to get how much we gain
#define ETHEREAL_CELL_POWER_GAIN_FACTOR 0.08
/**
* # Power store abstract type
*
* Abstract type for a stock part that holds power.
*/
/obj/item/stock_parts/power_store
name = "power store abstract"
/// The size icon overlay prefix.
var/cell_size_prefix = "cell"
///Current charge in cell units
var/charge = 0
/// Standard cell charge used for rating
var/rating_base = STANDARD_CELL_CHARGE
///Maximum charge in cell units
var/maxcharge = STANDARD_CELL_CHARGE
///If the power cell was damaged by an explosion, chance for it to become corrupted and function the same as if it was rigged with plasma.
var/corrupted = FALSE
///How much power is given per second in a recharger.
var/chargerate = STANDARD_CELL_RATE * 0.05
///If true, the cell will state it's maximum charge in it's description
var/ratingdesc = TRUE
///If it's a grown that acts as a battery, add a wire overlay to it.
var/grown_battery = FALSE
///What charge lige sprite to use, null if no light
var/charge_light_type = "standard"
///What connector sprite to use when in a cell charger, null if no connectors
var/connector_type = "standard"
///Does the cell start without any charge?
var/empty = FALSE
// Damage multiplier the cells take from emps to prevent stuff like bluespace cells taking 40 shots to drain.
var/emp_damage_modifier = 1
/obj/item/stock_parts/power_store/get_cell()
return src
/obj/item/stock_parts/power_store/get_save_vars()
. = ..()
. += NAMEOF(src, charge)
. += NAMEOF(src, corrupted)
return .
/obj/item/stock_parts/power_store/Initialize(mapload, override_maxcharge)
. = ..()
create_reagents(5, INJECTABLE | DRAINABLE)
if (override_maxcharge)
maxcharge = override_maxcharge
rating = max(round(maxcharge / (rating_base * 10), 1), 1)
if(!empty && !charge)
charge = maxcharge
if(ratingdesc)
desc += " This one has a rating of [display_energy(maxcharge)][prob(10) ? ", and you should not swallow it" : ""]." //joke works better if it's not on every cell
update_appearance()
RegisterSignal(src, COMSIG_ITEM_MAGICALLY_CHARGED, PROC_REF(on_magic_charge))
var/static/list/loc_connections = list(
COMSIG_ITEM_MAGICALLY_CHARGED = PROC_REF(on_magic_charge),
)
AddElement(/datum/element/connect_loc, loc_connections)
/obj/item/stock_parts/power_store/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if(!isturf(old_loc))
update_appearance()
/**
* Signal proc for [COMSIG_ITEM_MAGICALLY_CHARGED]
*
* If we, or the item we're located in, is subject to the charge spell, gain some charge back
*/
/obj/item/stock_parts/power_store/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/charge/spell, mob/living/caster)
SIGNAL_HANDLER
// This shouldn't be running if we're not being held by a mob,
// or if we're not within an object being held by a mob, but just in case...
if(!ismovable(loc))
return
. = COMPONENT_ITEM_CHARGED
if(prob(80))
maxcharge -= rating_base * 0.2
if(maxcharge <= 1) // Div by 0 protection
maxcharge = 1
. |= COMPONENT_ITEM_BURNT_OUT
charge = maxcharge
update_appearance()
// Guns need to process their chamber when we've been charged
if(isgun(loc))
var/obj/item/gun/gun_loc = loc
gun_loc.process_chamber()
// The thing we're in might have overlays or icon states for whether the cell is charged
if(!ismob(loc))
loc.update_appearance()
return .
/obj/item/stock_parts/power_store/update_overlays()
. = ..()
if(grown_battery)
. += mutable_appearance('icons/obj/machines/cell_charger.dmi', "grown_wires")
if((charge < 0.01) || !charge_light_type)
return
. += mutable_appearance('icons/obj/machines/cell_charger.dmi', "[cell_size_prefix]-[charge_light_type]-o[(percent() >= 99.5) ? 2 : 1]")
/obj/item/stock_parts/power_store/vv_edit_var(vname, vval)
if(vname == NAMEOF(src, charge))
charge = clamp(vval, 0, maxcharge)
return TRUE
if(vname == NAMEOF(src, maxcharge))
if(charge > vval)
charge = vval
if(vname == NAMEOF(src, corrupted) && vval && !corrupted)
corrupt(TRUE)
return TRUE
return ..()
/**
* Returns the percentage of the cell's charge.
*/
/obj/item/stock_parts/power_store/proc/percent() // return % charge of cell
return 100 * charge / maxcharge
/**
* Returns the maximum charge of the cell.
*/
/obj/item/stock_parts/power_store/proc/max_charge()
return maxcharge
/**
* Returns the current charge of the cell.
*/
/obj/item/stock_parts/power_store/proc/charge()
return charge
/**
* Returns the amount of charge used on the cell.
*/
/obj/item/stock_parts/power_store/proc/used_charge()
return maxcharge - charge
/// Use power from the cell.
/// Args:
/// - used: Amount of power in joules to use.
/// - force: If true, uses the remaining power from the cell if there isn't enough power to supply the demand.
/// Returns: The power used from the cell in joules.
/obj/item/stock_parts/power_store/use(used, force = FALSE)
var/power_used = min(used, charge)
if(power_used > 0 && try_explode())
return 0 // The cell decided to explode so we won't be able to use it.
if(!force && charge < used)
return 0
charge -= power_used
if(!istype(loc, /obj/machinery/power/apc))
SSblackbox.record_feedback("tally", "cell_used", 1, type)
return power_used
/// Recharge the cell.
/// Args:
/// - amount: The amount of energy to give to the cell in joules.
/// Returns: The power given to the cell in joules.
/obj/item/stock_parts/power_store/proc/give(amount)
var/power_used = min(maxcharge-charge,amount)
charge += power_used
if (amount)
try_explode()
return power_used
/**
* Changes the charge of the cell.
* Args:
* - amount: The energy to give to the cell (can be negative).
* Returns: The energy that was given to the cell (can be negative).
*/
/obj/item/stock_parts/power_store/proc/change(amount)
var/energy_used = clamp(amount, -charge, maxcharge - charge)
charge += energy_used
if(energy_used)
try_explode()
return energy_used
/obj/item/stock_parts/power_store/examine(mob/user)
. = ..()
if(corrupted)
. += span_danger("This [name] seems to be faulty!")
else if(!isnull(charge_light_type))
. += "The charge meter reads [CEILING(percent(), 0.1)]%." //so it doesn't say 0% charge when the overlay indicates it still has charge
/obj/item/stock_parts/power_store/proc/try_explode(max_charge = FALSE)
var/check_charge = charge
if (max_charge)
check_charge = maxcharge
if(!check_charge)
return FALSE
if (!corrupted)
if (!(reagents?.spark_act(check_charge, SPARK_ACT_ENCLOSED) & SPARK_ACT_DESTRUCTIVE))
return FALSE
message_admins("[ADMIN_LOOKUPFLW(usr)] has triggered a rigged power cell explosion at [AREACOORD(loc)].")
usr?.log_message("triggered a rigged power cell explosion", LOG_GAME)
usr?.log_message("triggered a rigged power cell explosion", LOG_VICTIM, log_globally = FALSE)
qdel(src)
return TRUE
var/range_heavy = round(sqrt(check_charge / (3.6 * rating_base)))
var/range_light = round(sqrt(check_charge / (0.9 * rating_base)))
if(!range_light)
corrupt()
return FALSE
message_admins("[ADMIN_LOOKUPFLW(usr)] has triggered a corrupted power cell explosion at [AREACOORD(loc)].")
usr?.log_message("triggered a corrupted power cell explosion", LOG_GAME)
usr?.log_message("triggered a corrupted power cell explosion", LOG_VICTIM, log_globally = FALSE)
explosion(src, devastation_range = -1, heavy_impact_range = range_heavy, light_impact_range = range_light, flash_range = range_light)
qdel(src)
return TRUE
/obj/item/stock_parts/power_store/proc/corrupt(force)
charge /= 2
maxcharge = max(maxcharge/2, chargerate)
if (force || prob(10))
corrupted = TRUE
/obj/item/stock_parts/power_store/emp_act(severity)
. = ..()
if(. & EMP_PROTECT_SELF)
return
use((STANDARD_CELL_CHARGE /severity) * emp_damage_modifier , force = TRUE)
/obj/item/stock_parts/power_store/ex_act(severity, target)
. = ..()
if(QDELETED(src))
return FALSE
switch(severity)
if(EXPLODE_HEAVY)
if(prob(50))
corrupt()
if(EXPLODE_LIGHT)
if(prob(25))
corrupt()
return TRUE
/obj/item/stock_parts/power_store/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is licking the electrodes of [src]! It looks like [user.p_theyre()] trying to commit suicide!"))
do_sparks(2, TRUE, user)
var/eating_success = do_after(user, 5 SECONDS, src)
if(QDELETED(user))
return SHAME
if(!eating_success || QDELETED(src) || charge == 0)
user.visible_message(span_suicide("[user] chickens out!"))
return SHAME
playsound(user, 'sound/effects/sparks/sparks1.ogg', charge / maxcharge)
var/damage = charge / (1 KILO JOULES)
var/discharged_energy = charge
user.electrocute_act(damage, src, 1, SHOCK_IGNORE_IMMUNITY|SHOCK_DELAY_STUN|SHOCK_NOGLOVES)
charge = 0
update_appearance()
if(user.stat != DEAD)
to_chat(user, span_suicide("There's not enough charge in [src] to kill you!"))
return SHAME
addtimer(CALLBACK(src, PROC_REF(gib_user), user, discharged_energy), 3 SECONDS)
return MANUAL_SUICIDE
/obj/item/stock_parts/power_store/proc/gib_user(mob/living/user, discharged_energy)
if(QDELETED(user))
return
if(discharged_energy < STANDARD_BATTERY_CHARGE)
return
user.dropItemToGround(src)
user.dust(just_ash = TRUE, drop_items = TRUE)
playsound(src, 'sound/effects/magic/lightningshock.ogg', 50, TRUE, 10)
tesla_zap(source = src, zap_range = 10, power = discharged_energy)
/obj/item/stock_parts/power_store/attack_self(mob/user)
. = ..()
if(.)
return
if(!ishuman(user))
return
var/mob/living/carbon/human/human_user = user
var/obj/item/organ/stomach/ethereal/user_stomach = human_user.get_organ_slot(ORGAN_SLOT_STOMACH)
if(!istype(user_stomach))
return
if(user_stomach.drain_time > world.time)
return
ethereal_drain(human_user, user_stomach)
/// Handles letting an ethereal drain our charge into their stomach
/obj/item/stock_parts/power_store/proc/ethereal_drain(mob/living/carbon/human/user, obj/item/organ/stomach/ethereal/used_stomach)
if(charge() <= 0)
balloon_alert(user, "out of charge!")
return
var/obj/item/stock_parts/power_store/stomach_cell = used_stomach.cell
used_stomach.drain_time = world.time + ETHEREAL_CELL_DRAIN_TIME
to_chat(user, span_notice("You begin clumsily channeling power from [src] into your body."))
while(do_after(user, ETHEREAL_CELL_DRAIN_TIME, target = src))
if(isnull(used_stomach) || (used_stomach != user.get_organ_slot(ORGAN_SLOT_STOMACH)))
balloon_alert(user, "stomach removed!?")
return
var/our_charge = charge()
var/scaled_stomach_used_charge = stomach_cell.used_charge() / ETHEREAL_CELL_POWER_GAIN_FACTOR
var/potential_charge = min(our_charge, scaled_stomach_used_charge)
var/to_drain = min(ETHEREAL_CELL_POWER_DRAIN, potential_charge)
var/energy_drained = use(to_drain, force = TRUE)
used_stomach.adjust_charge(energy_drained * ETHEREAL_CELL_POWER_GAIN_FACTOR)
update_appearance(UPDATE_OVERLAYS)
if(stomach_cell.used_charge() <= 0)
balloon_alert(user, "your charge is full!")
return
if(charge() <= 0)
balloon_alert(user, "out of charge!")
return
/obj/item/stock_parts/power_store/blob_act(obj/structure/blob/B)
SSexplosions.high_mov_atom += src
/obj/item/stock_parts/power_store/proc/get_electrocute_damage()
return ELECTROCUTE_DAMAGE(charge / max(0.001 * STANDARD_CELL_CHARGE, 1)) // Wouldn't want it to consider more energy than whatever is actually in the cell if for some strange reason someone set the STANDARD_CELL_CHARGE to below 1kJ.
/obj/item/stock_parts/power_store/get_part_rating()
return maxcharge * 10 + charge
#undef ETHEREAL_CELL_DRAIN_TIME
#undef ETHEREAL_CELL_POWER_DRAIN
#undef ETHEREAL_CELL_POWER_GAIN_FACTOR