diff --git a/code/datums/components/radioactive_emitter.dm b/code/datums/components/radioactive_emitter.dm new file mode 100644 index 00000000000..f956c51e10d --- /dev/null +++ b/code/datums/components/radioactive_emitter.dm @@ -0,0 +1,87 @@ +/// Minimum duration between pulses for the radioactive emitter component. +/// This is chosen arbitrarily. It can theoretically go down to 0.1 SECONDS but god please don't +#define MIN_PULSE_COOLDOWN 0.5 SECONDS + +/** + * # Radioactive Emitter + * + * Simple component that you can attach to something to make it emit radiation pulses over time. + */ +/datum/component/radioactive_emitter + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + + /// The actual cooldown between rad pulses + COOLDOWN_DECLARE(rad_pulse_cooldown) + + /// The length of the cooldown between radiation pulses + var/cooldown_time = 5 SECONDS + /// How far the radiation pulses aggregate with other radiation pulses (see: [proc/radiation_pulse]) + var/range = 1 + /// How much radiation protection threshold is passed to the radiation pulse (see: [proc/radiation_pulse]) + var/threshold = RAD_MEDIUM_INSULATION + /// Optional - What is shown on examine of the parent? + var/examine_text + +/datum/component/radioactive_emitter/Initialize( + cooldown_time = 5 SECONDS, + range = 1, + threshold = RAD_MEDIUM_INSULATION, + examine_text, +) + + if(!isturf(parent) && !ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + src.cooldown_time = max(cooldown_time, MIN_PULSE_COOLDOWN) + src.range = range + src.threshold = threshold + src.examine_text = examine_text + + // We process on fastprocess even though we're on a cooldown based system. + // Easier to handle edits to the cooldown duration, prevents timer spam for short cooldown emitters + START_PROCESSING(SSfastprocess, src) + +/datum/component/radioactive_emitter/Destroy(force, silent) + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/component/radioactive_emitter/RegisterWithParent() + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + +/datum/component/radioactive_emitter/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_PARENT_EXAMINE) + +/datum/component/radioactive_emitter/InheritComponent( + datum/component/new_comp, + i_am_original, + cooldown_time = 5 SECONDS, + range = 1, + threshold = RAD_NO_INSULATION, + examine_text, +) + + if(!i_am_original) + return + + // Only care about modifying our rad wave argument. + src.cooldown_time = cooldown_time + src.range = range + src.threshold = threshold + // Don't touch examine text or whatever else. + +/datum/component/radioactive_emitter/process(delta_time) + if(!COOLDOWN_FINISHED(src, rad_pulse_cooldown)) + return + + COOLDOWN_START(src, rad_pulse_cooldown, cooldown_time) + radiation_pulse(parent, range, threshold) + +/datum/component/radioactive_emitter/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + if(!examine_text) + return + + examine_list += examine_text + +#undef MIN_PULSE_COOLDOWN diff --git a/code/datums/mutations/radioactive.dm b/code/datums/mutations/radioactive.dm index b163a1f6c9e..4f1edf721ed 100644 --- a/code/datums/mutations/radioactive.dm +++ b/code/datums/mutations/radioactive.dm @@ -6,24 +6,38 @@ instability = 5 difficulty = 8 power_coeff = 1 - - COOLDOWN_DECLARE(last_radioactive_pulse) - -/datum/mutation/human/radioactive/on_life(delta_time, times_fired) - if (!COOLDOWN_FINISHED(src, last_radioactive_pulse)) - return - - COOLDOWN_START(src, last_radioactive_pulse, 5 SECONDS) - radiation_pulse( - owner, - max_range = 1 * (GET_MUTATION_POWER(src) * 2), - threshold = RAD_MEDIUM_INSULATION, - ) + /// Weakref to our radiation emitter component + var/datum/weakref/radioactivity_source_ref /datum/mutation/human/radioactive/New(class_ = MUT_OTHER, timer, datum/mutation/human/copymut) - ..() + . = ..() if(!(type in visual_indicators)) visual_indicators[type] = list(mutable_appearance('icons/effects/genetics.dmi', "radiation", -MUTATIONS_LAYER)) /datum/mutation/human/radioactive/get_visual_indicator() return visual_indicators[type][1] + +/datum/mutation/human/radioactive/on_acquiring(mob/living/carbon/human/acquirer) + . = ..() + var/datum/component/radioactive_emitter/radioactivity_source = make_radioactive(acquirer) + radioactivity_source_ref = WEAKREF(radioactivity_source) + +/datum/mutation/human/radioactive/modify() + . = ..() + make_radioactive(owner) + +/** + * Makes the passed mob radioactive, or if they're already radioactive, + * update their radioactivity to the newly set values + */ +/datum/mutation/human/radioactive/proc/make_radioactive(mob/living/carbon/human/who) + return who.AddComponent( + /datum/component/radioactive_emitter, \ + cooldown_time = 5 SECONDS, \ + range = 1 * (GET_MUTATION_POWER(src) * 2), \ + threshold = RAD_MEDIUM_INSULATION, \ + ) + +/datum/mutation/human/radioactive/on_losing(mob/living/carbon/human/owner) + QDEL_NULL(radioactivity_source_ref) + return ..() diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm index 545ab247bea..8b8d59aa25b 100644 --- a/code/modules/events/_event.dm +++ b/code/modules/events/_event.dm @@ -170,12 +170,15 @@ Runs the event var/datum/round_event_control/control /// When in the lifetime to call start(). + /// This is in seconds - so 1 = ~2 seconds in. var/start_when = 0 /// When in the lifetime to call announce(). If you don't want it to announce use announce_chance, below. + /// This is in seconds - so 1 = ~2 seconds in. var/announce_when = 0 /// Probability of announcing, used in prob(), 0 to 100, default 100. Called in process, and for a second time in the ion storm event. var/announce_chance = 100 /// When in the lifetime the event should end. + /// This is in seconds - so 1 = ~2 seconds in. var/end_when = 0 /// How long the event has existed. You don't need to change this. diff --git a/code/modules/events/radiation_leak.dm b/code/modules/events/radiation_leak.dm new file mode 100644 index 00000000000..a0e40816245 --- /dev/null +++ b/code/modules/events/radiation_leak.dm @@ -0,0 +1,167 @@ +/datum/round_event_control/radiation_leak + name = "Radiation Leak" + description = "A radiation leak happens somewhere on the station, emanating radiation around a machine in the area. \ + Engineering can stop the leak by using certain tools on it." + typepath = /datum/round_event/radiation_leak + weight = 15 + max_occurrences = 3 + category = EVENT_CATEGORY_ENGINEERING + +/datum/round_event/radiation_leak + start_when = 1 // 2 seconds in + announce_when = 10 // 20 seconds in + end_when = 150 // 300 seconds / ~5 minutes in + + /// Weakref to the machine spitting out rads + var/datum/weakref/picked_machine_ref + /// List of signals added to the picked machine, so we can clear them later + var/list/signals_to_add + +/datum/round_event/radiation_leak/setup() + // Pick a generic event spawn somewhere in the world. + // We will try to find a machine within a few turfs of it to start spewing rads from. + var/list/possible_locs = GLOB.generic_event_spawns.Copy() + while(length(possible_locs)) + var/turf/chosen_loc = get_turf(pick_n_take(possible_locs)) + for(var/obj/machinery/sick_device in range(3, chosen_loc)) + // Look for dense machinery. Basically stops stuff like wall mounts and pipes, silly ones. + // But keep in vents and scrubbers. I think it's funny if they start spitting out radiation + if(!sick_device.density && !istype(sick_device, /obj/machinery/atmospherics/components/unary)) + continue + // Skip invisible stuff + if(sick_device.invisibility || !sick_device.alpha || !sick_device.mouse_opacity) + continue + // Skip undertiles + if(sick_device.IsObscured()) + continue + // Very basic check for atmos passability here + // We don't want to put our smoke inside something that can't spread it out, like airlocks + if(sick_device.can_atmos_pass() != ATMOS_PASS_YES) + continue + + // We found something, we can just return now + picked_machine_ref = WEAKREF(sick_device) + return + +/datum/round_event/radiation_leak/announce(fake) + var/obj/machinery/the_source_of_our_problems = picked_machine_ref?.resolve() + var/area/station/location_descriptor + + if(fake) + location_descriptor = pick(GLOB.the_station_areas) + + else if(the_source_of_our_problems) + location_descriptor = get_area(the_source_of_our_problems) + + priority_announce("A radiation leak has been detected in [location_descriptor || "an unknown area"]. \ + All crew are to evacuate the affected area. Our [pick("mechanics", "engineers", "scientists", "interns", "sensors", "readings")] \ + report that a machine within is causing it - repair it quickly to stop the leak.") + +/datum/round_event/radiation_leak/start() + var/obj/machinery/the_source_of_our_problems = picked_machine_ref?.resolve() + if(!the_source_of_our_problems) + return + + // We'll add some tool acts to the thing that allow people to "repair the machine" + // The key of this assoc list is the "method" of how they're fixing the thing (just flavor for examine), + // and the value is what tool they actually need to use on the thing to fix it + var/list/how_do_we_fix_it = list( + "wrenching a few valves" = TOOL_WRENCH, + "tightening its bolts" = TOOL_WRENCH, + "crowbaring its panel [pick("down", "up")]" = TOOL_CROWBAR, + "tightening some screws" = TOOL_SCREWDRIVER, + "checking its [pick("wires", "circuits")]" = TOOL_MULTITOOL, + "welding its panel [pick("open", "shut")]" = TOOL_WELDER, + "analyzing its readings" = TOOL_ANALYZER, + "cutting some excess wires" = TOOL_WIRECUTTER, + ) + var/list/fix_it_keys = assoc_to_keys(how_do_we_fix_it) // Returns a copy that we can pick and take from, fortunately + + // Select a few methods of how to fix it + var/list/methods_to_fix = list() + for(var/i in 1 to rand(1, 3)) + methods_to_fix += pick_n_take(fix_it_keys) + + // Construct the signals + signals_to_add = list() + for(var/tool_method in methods_to_fix) + signals_to_add += COMSIG_ATOM_TOOL_ACT(how_do_we_fix_it[tool_method]) + + the_source_of_our_problems.visible_message(span_danger("[the_source_of_our_problems] starts to emanate a horrible green gas!")) + // Add the component that makes the thing radioactive + the_source_of_our_problems.AddComponent( + /datum/component/radioactive_emitter, \ + cooldown_time = 2 SECONDS, \ + range = 5, \ + threshold = RAD_MEDIUM_INSULATION, \ + examine_text = span_green("It's emanating a green gas... You could probably stop it by [english_list(methods_to_fix, and_text = " or ")]."), \ + ) + // Register signals to make it fixable + if(length(signals_to_add)) + RegisterSignals(the_source_of_our_problems, signals_to_add, PROC_REF(on_machine_tooled)) + + // And yknow puffs some nasty reagents into the air, just to seal the deal + puff_some_smoke(the_source_of_our_problems) + // Let ghosts know + announce_to_ghosts(the_source_of_our_problems) + +/datum/round_event/radiation_leak/tick() + // Puff some smoke into the air around our machine roughly 3 times before we stop + if(activeFor % (end_when / 3) != 0) + return + + var/obj/machinery/impromptu_smoke_machine = picked_machine_ref?.resolve() + if(!impromptu_smoke_machine) + return + + puff_some_smoke(impromptu_smoke_machine) + +/datum/round_event/radiation_leak/end() + var/obj/machinery/the_end_of_our_problems = picked_machine_ref?.resolve() + if(!the_end_of_our_problems) + return + + the_end_of_our_problems.visible_message(span_notice("The gas emanating from [the_end_of_our_problems] dissipates.")) + qdel(the_end_of_our_problems.GetComponent(/datum/component/radioactive_emitter)) + if(length(signals_to_add)) + UnregisterSignal(the_end_of_our_problems, signals_to_add) + picked_machine_ref = null + signals_to_add = null + +/// Helper to shoot some smoke into the air around the passed atom +/datum/round_event/radiation_leak/proc/puff_some_smoke(atom/where) + var/turf/below_where = get_turf(where) + var/datum/effect_system/fluid_spread/smoke/chem/gross_smoke = new() + gross_smoke.chemholder.add_reagent(/datum/reagent/toxin/polonium, 10) // Polonium (it causes radiation) + gross_smoke.chemholder.add_reagent(/datum/reagent/toxin/mutagen, 10) // Mutagen (it causes mutations. Also it's green... Primarily because it's green.) + gross_smoke.attach(below_where) + gross_smoke.set_up(2, holder = where, location = below_where, silent = TRUE) + gross_smoke.start() + playsound(below_where, 'sound/effects/smoke.ogg', 50, vary = TRUE) + +/** + * Signal proc for [COMSIG_ATOM_TOOL_ACT], from a variety of signals, registered on the machine spitting radiation + * + * We allow for someone to stop the event early by using the proper tools, hinted at in examine, on the machine + */ +/datum/round_event/radiation_leak/proc/on_machine_tooled(obj/machinery/source, mob/living/user, obj/item/tool) + SIGNAL_HANDLER + + INVOKE_ASYNC(src, PROC_REF(try_remove_radiation), source, user, tool) + return COMPONENT_BLOCK_TOOL_ATTACK + +/// Attempts a do_after, and if successful, stops the event +/datum/round_event/radiation_leak/proc/try_remove_radiation(obj/machinery/source, mob/living/user, obj/item/tool) + source.balloon_alert(user, "fixing leak...") + // Fairly long do after. It shouldn't be SUPER easy to just run in and stop it. + // A tider can fix it if they want to soak a bunch of rads and inhale noxious fumes, + // but only an equipped engineer should be able to handle it painlessly. + if(!tool.use_tool(source, user, 30 SECONDS, amount = (tool.tool_behaviour == TOOL_WELDER ? 2 : 0), volume = 50)) + source.balloon_alert(user, "interrupted!") + return + + source.balloon_alert(user, "leak repaired") + // Force end the event + processing = FALSE + end() + kill() diff --git a/tgstation.dme b/tgstation.dme index 0eac021c347..60929f84213 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -999,6 +999,7 @@ #include "code\datums\components\punchcooldown.dm" #include "code\datums\components\puzzgrid.dm" #include "code\datums\components\radiation_countdown.dm" +#include "code\datums\components\radioactive_emitter.dm" #include "code\datums\components\reagent_refiller.dm" #include "code\datums\components\regenerator.dm" #include "code\datums\components\religious_tool.dm" @@ -3225,6 +3226,7 @@ #include "code\modules\events\mice_migration.dm" #include "code\modules\events\portal_storm.dm" #include "code\modules\events\processor_overload.dm" +#include "code\modules\events\radiation_leak.dm" #include "code\modules\events\radiation_storm.dm" #include "code\modules\events\sandstorm.dm" #include "code\modules\events\scrubber_clog.dm"