diff --git a/beestation.dme b/beestation.dme index a65cde70d91f8..b1001e79df74a 100644 --- a/beestation.dme +++ b/beestation.dme @@ -1267,6 +1267,7 @@ #include "code\game\objects\effects\decals\cleanable\food.dm" #include "code\game\objects\effects\decals\cleanable\humans.dm" #include "code\game\objects\effects\decals\cleanable\misc.dm" +#include "code\game\objects\effects\decals\cleanable\nuclear_waste.dm" #include "code\game\objects\effects\decals\cleanable\robots.dm" #include "code\game\objects\effects\decals\turfdecal\dirt.dm" #include "code\game\objects\effects\decals\turfdecal\guideline.dm" @@ -3520,6 +3521,13 @@ #include "code\modules\power\lighting\light_items.dm" #include "code\modules\power\lighting\light_mapping_helpers.dm" #include "code\modules\power\lighting\light_wallframes.dm" +#include "code\modules\power\rbmk\_rbmk_defines.dm" +#include "code\modules\power\rbmk\fuel_rods.dm" +#include "code\modules\power\rbmk\rbmk_core.dm" +#include "code\modules\power\rbmk\rbmk_main_processes.dm" +#include "code\modules\power\rbmk\rbmk_monitoring.dm" +#include "code\modules\power\rbmk\rbmk_parts.dm" +#include "code\modules\power\rbmk\rbmk_procs.dm" #include "code\modules\power\singularity\anomaly.dm" #include "code\modules\power\singularity\boh_tear.dm" #include "code\modules\power\singularity\collector.dm" diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 691d5c17c5e91..6561d4595bf47 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -42,6 +42,7 @@ #define WIRE_LAYER 2.4 #define WIRE_TERMINAL_LAYER 2.45 #define UNDER_CATWALK 2.454 +#define NUCLEAR_REACTOR_LAYER 2.456 //Below atmospheric devices, above hidden pipes and catwalks. #define CATWALK_LATTICE 2.455 #define GAS_SCRUBBER_LAYER 2.46 #define GAS_PIPE_VISIBLE_LAYER 2.47 diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm index 18c1b7f0fbf3a..a0f27fd812bf8 100644 --- a/code/__DEFINES/machines.dm +++ b/code/__DEFINES/machines.dm @@ -113,6 +113,11 @@ #define SUPERMATTER_EMERGENCY 5 // Integrity < 25% #define SUPERMATTER_DELAMINATING 6 // Pretty obvious. +#define NUCLEAR_REACTOR_ERROR -1 +#define NUCLEAR_REACTOR_INACTIVE 0 +#define NUCLEAR_REACTOR_ACTIVE 1 +#define NUCLEAR_EXPLODING 2 + //Nuclear bomb stuff #define NUKESTATE_INTACT 5 #define NUKESTATE_UNSCREWED 4 diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm index 24224a15f0ba1..f0bbf3fc42143 100644 --- a/code/datums/looping_sounds/machinery_sounds.dm +++ b/code/datums/looping_sounds/machinery_sounds.dm @@ -80,3 +80,21 @@ falloff_exponent = 5 falloff_distance = 3 volume = 150 + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/datum/looping_sound/rbmk + mid_sounds = list('sound/effects/rbmk/alarm.ogg' = 1) + volume = 100 + extra_range = 10 + mid_length = 58 + ignore_walls = TRUE + +/datum/looping_sound/rbmk_ambience + mid_sounds = list('sound/effects/rbmk/ambience.ogg' = 1) + mid_length = 19 + volume = 20 + extra_range = 10 + falloff_exponent = 10 + falloff_distance = 5 + vary = FALSE diff --git a/code/game/objects/effects/decals/cleanable/nuclear_waste.dm b/code/game/objects/effects/decals/cleanable/nuclear_waste.dm new file mode 100644 index 0000000000000..103b6b9e88174 --- /dev/null +++ b/code/game/objects/effects/decals/cleanable/nuclear_waste.dm @@ -0,0 +1,46 @@ +/obj/effect/decal/cleanable/nuclear_waste + name = "plutonium sludge" + desc = "A writhing pool of heavily irradiated, spent reactor fuel. You probably shouldn't step through this..." + icon = 'icons/obj/machines/rbmkparts.dmi' + icon_state = "nuclearwaste" + alpha = 150 + light_color = LIGHT_COLOR_CYAN + color = "#ff9eff" + +/obj/effect/decal/cleanable/nuclear_waste/Initialize(mapload) + . = ..() + set_light(3) + + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) + +/obj/effect/decal/cleanable/nuclear_waste/ex_act(severity, target) + if(severity != EXPLODE_DEVASTATE) + return + qdel(src) + +/obj/effect/decal/cleanable/nuclear_waste/on_entered(datum/source, atom/movable/entered_mob) + if(isliving(entered_mob)) + var/mob/living/L = entered_mob + playsound(loc, 'sound/effects/gib_step.ogg', HAS_TRAIT(L, TRAIT_LIGHT_STEP) ? 20 : 50, 1) + radiation_pulse(src, 625, 5) //MORE RADS + +/obj/effect/decal/cleanable/nuclear_waste/attackby(obj/item/tool, mob/user) + if(tool.tool_behaviour == TOOL_SHOVEL) + radiation_pulse(src, 500, 5) //MORE RADS //The careful clearing of sludge should not give off as much radiation as casually running through it. + to_chat(user, "You start to clear [src]...") + if(tool.use_tool(src, user, 50, volume=100)) + to_chat(user, "You clear [src]. ") + qdel(src) + return + . = ..() + +/obj/effect/decal/cleanable/nuclear_waste/epicenter //The one that actually does the irradiating. This is to avoid every bit of sludge PROCESSING + name = "dense nuclear sludge" + + +/obj/effect/decal/cleanable/nuclear_waste/epicenter/Initialize(mapload) + . = ..() + AddComponent(/datum/component/radioactive, 1500, src, 0) diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm index 753d1cdf365df..fc2b81dc187b9 100644 --- a/code/game/objects/items/circuitboards/computer_circuitboards.dm +++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm @@ -194,6 +194,10 @@ icon_state = "engineering" build_path = /obj/machinery/computer/turbine_computer +/obj/item/circuitboard/computer/control_rods + name = "rbmk reactor control rod console (Computer Board)" + icon_state = "engineering" + build_path = /obj/machinery/computer/reactor/control_rods //Generic diff --git a/code/modules/cargo/packs.dm b/code/modules/cargo/packs.dm index 1fdd34360450f..1a8190f39f3cb 100644 --- a/code/modules/cargo/packs.dm +++ b/code/modules/cargo/packs.dm @@ -1099,6 +1099,17 @@ crate_name = "power cell crate" crate_type = /obj/structure/closet/crate/engineering/electrical +/datum/supply_pack/engineering/sealant + name = "Engine Sealant Crate" + desc = "Nuclear reactor looking a bit cracked? Don't be afraid to slap on some NT brand sealant to patch those holes right up!" + cost = 1000 + max_supply = 1 + contains = list(/obj/item/sealant, + /obj/item/sealant, + /obj/item/sealant) + crate_name = "sealant crate" + crate_type = /obj/structure/closet/crate/engineering/ + /datum/supply_pack/engineering/shuttle_engine name = "Shuttle Engine Crate" desc = "Through advanced bluespace-shenanigans, our engineers have managed to fit an entire shuttle engine into one tiny little crate. Requires CE access to open." @@ -1125,6 +1136,20 @@ /obj/item/storage/toolbox/mechanical) crate_name = "toolbox crate" +/datum/supply_pack/engineering/fuel_rods + name = "Uranium Fuel Rod Crate" + desc = "A five nuclear reactor grade fuel rod crate. Don't forget to wear radiation protection!" + cost = 3000 + max_supply = 2 + access_budget = ACCESS_ENGINE + contains = list(/obj/item/fuel_rod, + /obj/item/fuel_rod, + /obj/item/fuel_rod, + /obj/item/fuel_rod, + /obj/item/fuel_rod) + crate_name = "fuel rod crate" + crate_type = /obj/structure/closet/crate/secure/engineering + /datum/supply_pack/engineering/vending/engineering name = "Engineering Vending Crate" desc = "Sick of assistants breaking into engineering for tools? Contains one Engi-Vend refill and one YouTool refill." @@ -1336,6 +1361,28 @@ /obj/machinery/power/rad_collector) crate_name = "collector crate" +/datum/supply_pack/engine/nuclear_reactor + name = "RBMK Nuclear Reactor Engine Crate" + desc = "Contains the boards for an NT certified nuclear power engine! Don't forget to wear a radiation suit!" + cost = 7000 + max_supply = 1 + access = ACCESS_CE + access_budget = ACCESS_CE + contains = list(/obj/item/RBMK_box/core, + /obj/item/RBMK_box/body/coolant_input, + /obj/item/RBMK_box/body/moderator_input, + /obj/item/RBMK_box/body/waste_output, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/circuitboard/computer/control_rods, + /obj/item/book/manual/wiki/rbmk) + crate_name = "nuclear engine crate" + crate_type = /obj/structure/closet/crate/secure/engineering + dangerous = TRUE + /datum/supply_pack/engine/sing_gen name = "Singularity Generator Crate" desc = "The key to unlocking the power of Lord Singuloth. Particle Accelerator not included." diff --git a/code/modules/power/rbmk/_rbmk_defines.dm b/code/modules/power/rbmk/_rbmk_defines.dm new file mode 100644 index 0000000000000..0790e82d1bd7d --- /dev/null +++ b/code/modules/power/rbmk/_rbmk_defines.dm @@ -0,0 +1,38 @@ +#define RBMK_TEMPERATURE_OPERATING 640 //Celsius +#define RBMK_TEMPERATURE_WARNING 800 //At this point the entire station is alerted to a meltdown. +#define RBMK_TEMPERATURE_CRITICAL 900 //Kablowey + +#define RBMK_NO_COOLANT_TOLERANCE 5 //How many process()ing ticks the reactor can sustain without coolant before slowly taking damage + +#define RBMK_PRESSURE_OPERATING 6500 //KPA +#define RBMK_PRESSURE_WARNING 8200 //KPA, At this point the entire station is alerted to a meltdown. +#define RBMK_PRESSURE_CRITICAL 10100 //KPA, Kaboom + +#define RBMK_MAX_CRITICALITY 3 //No more criticality than N for now. + +#define RBMK_POWER_FLAVOURISER 800 //To turn those KWs into something usable + +#define WARNING_TIME_DELAY 60 //to prevent accent sounds from layering +#define REACTOR_COUNTDOWN_TIME 30 SECONDS + +///High pressure damage +#define RBMK_PRESSURE_DAMAGE (1<<0) +///High temperature damage +#define RBMK_TEMPERATURE_DAMAGE (1<<1) + +#define REACTOR_INACTIVE 0 // No or minimal energy +#define REACTOR_NOMINAL 1 // Normal operation +#define REACTOR_WARNING 2 // Integrity damaged +#define REACTOR_DANGER 3 // Integrity < 50% +#define REACTOR_EMERGENCY 4 // Integrity < 25% +#define REACTOR_MELTING 5 // Pretty obvious. + +#define REACTOR_MELTING_PERCENT 5 +#define REACTOR_EMERGENCY_PERCENT 25 +#define REACTOR_DANGER_PERCENT 50 +#define REACTOR_WARNING_PERCENT 100 + +#define REACTOR_NEW_SEALS 0.875 +#define REACTOR_CRACKED_SEALS 0.5 + +#define SAFE_POWER_LEVEL 20 diff --git a/code/modules/power/rbmk/fuel_rods.dm b/code/modules/power/rbmk/fuel_rods.dm new file mode 100644 index 0000000000000..098b5d2212cb0 --- /dev/null +++ b/code/modules/power/rbmk/fuel_rods.dm @@ -0,0 +1,253 @@ +/obj/item/fuel_rod + name = "uranium-238 fuel rod" + desc = "A titanium sheathed rod containing a measure of enriched uranium-dioxide powder, used to kick off a fission reaction." + icon = 'icons/obj/control_rod.dmi' + icon_state = "irradiated" + w_class = WEIGHT_CLASS_BULKY + + var/depletion = 0 //Each fuel rod will deplete in around 30 minutes. + var/fuel_power = 0.10 + + var/rad_strength = 500 + var/half_life = 2000 // how many depletion ticks are needed to half the fuel_power (1 tick = 1 second) + var/time_created = 0 + var/og_fuel_power = 0.20 //the original fuel power value + var/process = FALSE + // The depletion where depletion_final() will be called (and does something) + var/depletion_threshold = 100 + // How fast this rod will deplete + var/depletion_speed_modifier = 1 + var/depleted_final = FALSE // depletion_final should run only once + var/depletion_conversion_type = "plutonium" + +/obj/item/fuel_rod/Initialize(mapload) + . = ..() + time_created = world.time + AddComponent(/datum/component/two_handed, require_twohands=TRUE) + AddComponent(/datum/component/radioactive, rad_strength, src) // This should be temporary for it won't make rads go lower than 350 + if(process) + START_PROCESSING(SSobj, src) + +/obj/item/fuel_rod/Destroy() + if(process) + STOP_PROCESSING(SSobj, src) + if(istype(loc, /obj/machinery/atmospherics/components/unary/rbmk/core)) + var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core = loc + reactor_core.fuel_rods -= src + return ..() + +// This proc will try to convert your fuel rod if you don't override this proc +// So, ideally, you should write an override of this for every fuel rod you want to create +/obj/item/fuel_rod/proc/depletion_final(result_rod) + if(!result_rod) + return + // Rod conversion is moot when you can't find the reactor + if(istype(loc, /obj/machinery/atmospherics/components/unary/rbmk/core)) + var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core = loc + var/obj/item/fuel_rod/reactor_fuel_rod + // You can add your own depletion scheme and not override this proc if you are going to convert a fuel rod into another type + switch(result_rod) + if("plutonium") + reactor_fuel_rod = new /obj/item/fuel_rod/plutonium(loc) + reactor_fuel_rod.depletion = depletion + if("depleted") + if(fuel_power < 10) + fuel_power = 0 + playsound(loc, 'sound/effects/supermatter.ogg', 100, TRUE) + reactor_fuel_rod = new /obj/item/fuel_rod/depleted(loc) + reactor_fuel_rod.depletion = depletion + + // Finalization of conversion + if(istype(reactor_fuel_rod)) + reactor_core.fuel_rods += reactor_fuel_rod + qdel(src) + else + depleted_final = FALSE // Maybe try again later? + +/obj/item/fuel_rod/proc/deplete(amount=0.035) + depletion += amount * depletion_speed_modifier + if(depletion >= depletion_threshold && !depleted_final) + depleted_final = TRUE + depletion_final(depletion_conversion_type) + +/obj/item/fuel_rod/plutonium + fuel_power = 0.20 + name = "plutonium-239 fuel rod" + desc = "A highly energetic titanium sheathed rod containing a sizeable measure of weapons grade plutonium, it's highly efficient as nuclear fuel, but will cause the reaction to get out of control if not properly utilised." + icon_state = "inferior" + rad_strength = 1500 + process = TRUE + depletion_threshold = 300 + depletion_conversion_type = "depleted" + +/obj/item/fuel_rod/process() + fuel_power = og_fuel_power * 0.5**((world.time - time_created) / half_life SECONDS) // halves the fuel power every half life (33 minutes) + +/obj/item/fuel_rod/depleted + fuel_power = 0.05 + name = "depleted fuel rod" + desc = "A highly radioactive fuel rod which has expended most of it's useful energy." + icon_state = "normal" + rad_strength = 6000 // smelly + depletion_conversion_type = null // we don't want it to turn into anything + process = TRUE + +// Master type for material optional (or requiring, wyci) and/or producing rods +/obj/item/fuel_rod/material + // Whether the rod has been harvested. Should be set in expend(). + var/expended = FALSE + // The material that will be inserted and then multiplied (or not). Should be some sort of /obj/item/stack + var/material_type + // The name of material that'll be used for texts + var/material_name + var/material_name_singular + var/initial_amount = 0 + // The maximum amount of material the rod can hold + var/max_initial_amount = 10 + var/grown_amount = 0 + // The multiplier for growth. 1 for the same 2 for double etc etc + var/multiplier = 2 + // After this depletion, you won't be able to add new materials + var/material_input_deadline = 25 + // Material fuel rods generally don't get converted into another fuel object + depletion_conversion_type = null + +// Called when the rod is fully harvested +/obj/item/fuel_rod/material/proc/expend() + expended = TRUE + +// Basic checks for material rods +/obj/item/fuel_rod/material/proc/check_material_input(mob/user) + if(depletion >= material_input_deadline) + to_chat(user, "The sample slots have sealed themselves shut, it's too late to add [material_name] now!") // no cheesing in crystals at 100% + return FALSE + if(expended) + to_chat(user, "\The [src]'s material slots have already been used.") + return FALSE + return TRUE + +// The actual growth +/obj/item/fuel_rod/material/depletion_final(result_rod) + if(result_rod) + ..() // So if you put anything into depletion_conversion_type then your fuel rod will be converted (or not) and *won't grow* + else + grown_amount = initial_amount * multiplier + +/obj/item/fuel_rod/material/attackby(obj/item/attacked_item, mob/user, params) + var/obj/item/stack/material = attacked_item + if(istype(material, material_type)) + if(!check_material_input(user)) + return + if(initial_amount < max_initial_amount) + var/adding = min((max_initial_amount - initial_amount), material.amount) + material.amount -= adding + initial_amount += adding + if(adding == 1) + to_chat(user, "You insert [adding] [material_name_singular] into \the [src].") + else + to_chat(user, "You insert [adding] [material_name] into \the [src].") + material.is_zero_amount() + else + to_chat(user, "\The [src]'s material slots are full!") + return + else + return ..() + +/obj/item/fuel_rod/material/attack_self(mob/user) + if(expended) + to_chat(user, "You have already removed [material_name] from \the [src].") + return + + if(depleted_final) + new material_type(user.loc, grown_amount) + if(grown_amount == 1) + to_chat(user, "You harvest [grown_amount] [material_name_singular] from \the [src].") // Unlikely + else + to_chat(user, "You harvest [grown_amount] [material_name] from \the [src].") + playsound(loc, 'sound/effects/stonedoor_openclose.ogg', 50, 1) + grown_amount = 0 + expend() + return + if(depletion) + to_chat(user, "\The [src] has not fissiled enough to fully grow the sample. The progress bar shows it is [min(depletion/depletion_threshold*100,100)]% complete.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 0) + return + if(initial_amount) + new material_type(user.loc, initial_amount) + if(initial_amount == 1) + to_chat(user, "You remove [initial_amount] [material_name_singular] from \the [src].") + else + to_chat(user, "You remove [initial_amount] [material_name] from \the [src].") + playsound(loc, 'sound/effects/stonedoor_openclose.ogg', 50, 1) + initial_amount = 0 + +/obj/item/fuel_rod/material/examine(mob/user) + . = ..() + if(expended) + . += "The material slots have been slagged by the extreme heat, you can't grow [material_name] in this rod again..." + return + if(depleted_final) + . += "This fuel rod's [material_name] are now fully grown, and it currently bears [grown_amount] harvestable [material_name]\s." + return + if(depletion) + . += "The sample is [min(depletion/depletion_threshold*100,100)]% fissiled." + . += "[initial_amount]/[max_initial_amount] of the slots for [material_name] are full." + +/obj/item/fuel_rod/material/telecrystal + name = "telecrystal fuel rod" + desc = "A disguised titanium sheathed rod containing several small slots infused with uranium dioxide. Permits the insertion of telecrystals for transmutation. Fissiles much faster than its standard counterpart." + icon_state = "inferior" + fuel_power = 0.30 // twice as powerful as a normal rod + depletion_speed_modifier = 3 // headstart, otherwise it takes two hours + rad_strength = 1500 + max_initial_amount = 8 + multiplier = 3 + material_type = /obj/item/stack/sheet/telecrystal + material_name = "telecrystals" + material_name_singular = "telecrystal" + +/obj/item/fuel_rod/material/telecrystal/depletion_final(result_rod) + ..() + if(result_rod) + return + fuel_power = 0.60 // thrice as powerful as plutonium, you'll want to get this one out quick! + name = "exhausted [name]" + desc = "A highly energetic, disguised titanium sheathed rod containing a number of slots filled with greatly expanded telecrystals which can be removed by hand. It's extremely efficient as nuclear fuel, but will cause the reaction to get out of control if not properly utilised." + icon_state = "tc_used" + AddComponent(/datum/component/radioactive, 3000, src) + +/obj/item/fuel_rod/material/bananium + name = "bananium fuel rod" + desc = "A hilarious heavy-duty fuel rod which fissiles a bit slower than its cowardly counterparts. However, its cutting-edge cosmic clown technology allows rooms for extraordinarily exhilarating extraterrestrial element called bananium to menacingly multiply." + icon_state = "bananium" + fuel_power = 0.15 + depletion_speed_modifier = 3 + rad_strength = 350 + max_initial_amount = 10 + multiplier = 3 + material_type = /obj/item/stack/sheet/mineral/bananium + material_name = "sheets of bananium" + material_name_singular = "sheet of bananium" + +/obj/item/fuel_rod/material/bananium/deplete(amount=0.035) + ..() + if(initial_amount == max_initial_amount && prob(10)) + playsound(src, pick('sound/items/bikehorn.ogg', 'sound/misc/bikehorn_creepy.ogg'), 50) // HONK + +/obj/item/fuel_rod/material/bananium/depletion_final(result_rod) + ..() + if(result_rod) + return + fuel_power = 0.3 // Be warned + name = "exhausted [name]" + desc = "A hilarious heavy-duty fuel rod which fissiles a bit slower than it cowardly counterparts. Its greatly grimacing grwoth stage is now over, and bananium outgrowth hums as if it's blatantly honking bike horns." + icon_state = "bananium_used" + AddComponent(/datum/component/radioactive, 1250, src) + +/obj/item/sealant + name = "Flexi-seal" + desc = "A neat spray can that can repair torn reactor segments!" + icon = 'icons/obj/tools.dmi' + icon_state = "sealant" + w_class = 1 + var/repair_power = 10 //How much damage does the sealant repair per use diff --git a/code/modules/power/rbmk/rbmk_core.dm b/code/modules/power/rbmk/rbmk_core.dm new file mode 100644 index 0000000000000..6d8b7ce25f82e --- /dev/null +++ b/code/modules/power/rbmk/rbmk_core.dm @@ -0,0 +1,288 @@ +/* + This section contains the RBMK's core with all the variables and the Initialize() and Destroy() procs + +//Reference: Heaters go up to 500K. +//Hot plasmaburn: 14164.95 C. + +Moderators list (Not gonna keep this accurate forever): +Fuel Type: +Oxygen: Power production multiplier. Allows you to run a low plasma, high oxy mix, and still get a lot of power. +Plasma: Power production gas. More plasma -> more power, but it enriches your fuel and makes the reactor much, much harder to control. +Tritium: Extremely efficient power production gas. Will cause chernobyl if used improperly. + +Moderation Type: +N2: Helps you regain control of the reaction by increasing control rod effectiveness, will massively boost the rad production of the reactor. +CO2: Super effective shutdown gas for runaway reactions. MASSIVE RADIATION PENALTY! +Pluoxium: Same as N2, but no cancer-rads! + +Permeability Type: +BZ: Increases your reactor's ability to transfer its heat to the coolant, thus letting you cool it down faster (but your output will get hotter) +Water Vapour: More efficient permeability modifier +Hyper Noblium: Extremely efficient permeability increase. (10x as efficient as bz) + +Depletion type: +Nitryl: When you need weapons grade plutonium yesterday. Causes your fuel to deplete much, much faster. Not a huge amount of use outside of sabotage. + +Sabotage: + +Meltdown: +Flood reactor moderator with plasma, they won't be able to mitigate the reaction with control rods. +Shut off coolant entirely. Raise control rods. +Swap all fuel out with spent fuel, as it's way stronger. + +Blowout: +Shut off exit valve for quick overpressure. +Cause a pipefire in the coolant line (LETHAL). +Tack heater onto coolant line (can also cause straight meltdown) + +Tips: +Be careful to not exhaust your plasma supply. I recommend you DON'T max out the moderator input when youre running plasma + o2, or you're at a tangible risk of running out of those gasses from atmos. +The reactor CHEWS through moderator. It does not do this slowly. Be very careful with that! + +Remember kids. If the reactor itself is not physically powered by an APC, it cannot shove coolant in! +*/ + +/obj/machinery/atmospherics/components/unary/rbmk/core + name = "\improper Advanced Gas-Cooled Nuclear Reactor" + desc = "A tried and tested design which can output stable power at an acceptably low risk. The moderator can be changed to provide different effects." + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor" + use_power = IDLE_POWER_USE + idle_power_usage = IDLE_POWER_USE + layer = NUCLEAR_REACTOR_LAYER + ///Vars for the state of the icon of the object (open, closed, fuel rod counts (1>5)) + icon_state_open = "reactor_open" + icon_state_off = "reactor" + + + //Processing checks + + ///Checks if the user has started the machine + var/start_power = FALSE + ///Checks for the cooling to start + var/start_cooling = FALSE + ///Checks for the moderators to be injected + var/start_moderator = FALSE + + // RBMK internal gasmix + + //Stores the information for the control rods computer + var/obj/machinery/computer/reactor/control_rods/linked_interface + //Stores the information of the moderator input + var/obj/machinery/atmospherics/components/unary/rbmk/moderator_input/linked_moderator + ///Stores the information of the fuel input + var/obj/machinery/atmospherics/components/unary/rbmk/coolant_input/linked_input + ///Stores the information of the waste output + var/obj/machinery/atmospherics/components/unary/rbmk/waste_output/linked_output + ///Stores the information of the corners of the machine + var/list/corners = list() + ///Stores the three inputs/outputs of the RBMK + var/list/machine_parts = list() + + //Variables essential to operation + var/temperature = 0//Lose control of this -> Meltdown + var/pressure = 0 //Lose control of this -> Blowout + var/rate_of_reaction = 0 //Rate of reaction. + var/desired_reate_of_reaction = 0 + var/control_rod_effectiveness = 0.65 //Starts off with a lot of control over rate_of_reaction. If you flood this thing with plasma, you lose your ability to control rate_of_reaction as easily. + var/power = 0 //0-100%. A function of the maximum heat you can achieve within operating temperature + var/power_modifier = 1 //Upgrade me with parts, science! Flat out increase to physical power output when loaded with plasma. + var/list/fuel_rods = list() + var/gas_absorption_effectiveness = 0.5 + var/gas_absorption_constant = 0.5 //We refer to this one as it's set on init, randomized. + var/minimum_coolant_level = 5 + + ///Our internal radio + var/obj/item/radio/radio + ///The key our internal radio uses + var/radio_key = /obj/item/encryptionkey/headset_eng + ///The engineering channel + var/engineering_channel = "Engineering" + ///The common channel + var/common_channel = null + + //Our soundloop for the alarm + var/datum/looping_sound/rbmk/alarmloop + var/alarm = FALSE //Is the alarm playing already? + + //Soundloop for ambience + var/datum/looping_sound/rbmk_ambience/soundloop + + //Console statistics + var/last_coolant_temperature = 0 + var/last_output_temperature = 0 + var/last_heat_delta = 0 //For administrative cheating only. Knowing the delta lets you know EXACTLY what to set rate_of_reaction at. + var/no_coolant_ticks = 0 //How many times in succession did we not have enough coolant? Decays twice as fast as it accumulates. + + ///Time in 1/10th of seconds since the last sent warning + var/lastwarning = 0 + ///Boolean used for logging if we've passed the emergency point + var/has_reached_emergency = FALSE + + ///Integrity of the machine, if reaches 900 the machine will explode. 1 so it doesnt stunlock itself and never change for damage calculations + var/critical_threshold_proximity = 0 + ///Store the integrity for calculations + var/critical_threshold_proximity_archived = 0 + ///Our "Shit is no longer fucked" message. We send it when critical_threshold_proximity is less then critical_threshold_proximity_archived + var/safe_alert = "RBMK reactor returning to safe operating parameters." + ///The point at which we should start sending messeges about the critical_threshold_proximity to the engi channels. + var/warning_point = 50 + ///The alert we send when we've reached warning_point + var/warning_alert = "Danger! RBMK reactor faltering!" + ///The point at which we start sending messages to the common channel + var/emergency_point = 700 + ///The alert we send when we've reached emergency_point + var/emergency_alert = "NUCLEAR REACTOR MELTDOWN IMMINENT." + ///The point at which we melt + var/melting_point = 900 + //Light flicker timer + var/next_flicker = 0 + //For logging purposes + var/last_power_produced = 0 + //Power modifier for producing power. + var/base_power_modifier = RBMK_POWER_FLAVOURISER + ///Var used in the meltdown phase + var/final_countdown = FALSE + + ///Flags used in the alert proc to select what messages to show when the reactor is delaminating (RBMK_PRESSURE_DAMAGE | RBMK_TEMPERATURE_DAMAGE) + var/warning_damage_flags = NONE + + //Counter for number of reactors on a server + var/static/reactorcount = 0 + + //Grilling. + var/grill_time = 0 + var/datum/looping_sound/grill/grill_loop + var/obj/item/food/grilled_item + +/obj/effect/overlay/reactor_top_0 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_0" + +/obj/effect/overlay/reactor_top_1 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_1" + +/obj/effect/overlay/reactor_top_2 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_2" + +/obj/effect/overlay/reactor_top_3 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_3" + +/obj/effect/overlay/reactor_top_4 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_4" + +/obj/effect/overlay/reactor_top_5 + name = "reactor overlay" + icon = 'icons/obj/machines/rbmk.dmi' + icon_state = "reactor_top_5" + +/obj/machinery/atmospherics/components/unary/rbmk/core/Initialize(mapload) + . = ..() + grill_loop = new(src, FALSE) + radio = new/obj/item/radio(src) + radio.keyslot = new radio_key + radio.set_listening(FALSE) + radio.recalculateChannels() + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + COMSIG_ATOM_EXITED = PROC_REF(on_exited) + ) + AddElement(/datum/element/connect_loc, loc_connections) + AddElement(/datum/element/point_of_interest) + investigate_log("has been created.", INVESTIGATE_ENGINES) + pixel_x = -32 //This is to offset the entire reactor by one tile down and left, so it actually looks right. + pixel_y = -32 + reactorcount++ + src.name = name + " ([reactorcount])" + gas_absorption_effectiveness = rand(5, 6)/10 //All reactors are slightly different. This will result in you having to figure out what the balance is for rate_of_reaction. + gas_absorption_constant = gas_absorption_effectiveness //And set this up for the rest of the round. + soundloop = new(src, FALSE) + alarmloop = new(src, FALSE) + check_part_connectivity() + update_appearance() + update_pipenets() + +/obj/machinery/atmospherics/components/unary/rbmk/core/Destroy() + soundloop.stop() + alarmloop.stop() + unregister_signals(TRUE) + if(linked_input) + QDEL_NULL(linked_input) + if(linked_output) + QDEL_NULL(linked_output) + if(linked_moderator) + QDEL_NULL(linked_moderator) + if(linked_interface) + QDEL_NULL(linked_interface) + grilled_item = null + QDEL_NULL(grill_loop) + QDEL_NULL(radio) + QDEL_NULL(soundloop) + QDEL_NULL(alarmloop) + cut_overlays() + machine_parts = null + return ..() + +/obj/machinery/atmospherics/components/unary/rbmk/core/update_appearance() + . = ..() + if(panel_open) + icon_state = icon_state_open + else if((start_power == FALSE) && (power == 0)) + icon_state = icon_state_off + else if((start_power == TRUE) && power < 20) + icon_state = "reactor_startup" + else + icon_state = "reactor_active" + cut_overlays() + switch(length(fuel_rods)) + if(0) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_0")) + if(1) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_1")) + if(2) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_2")) + if(3) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_3")) + if(4) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_4")) + if(5) + add_overlay(mutable_appearance('icons/obj/machines/rbmk.dmi', "reactor_top_5")) + + // Lord forgive me for what I'm about to do. + var/mutable_appearance/InputOverlay = mutable_appearance('icons/obj/machines/rbmk.dmi',"input") + InputOverlay.transform = InputOverlay.transform.Turn(dir2angle(linked_input.dir)) + add_overlay(InputOverlay) + var/mutable_appearance/OutputOverlay = mutable_appearance('icons/obj/machines/rbmk.dmi',"output") + OutputOverlay.transform = OutputOverlay.transform.Turn(dir2angle(linked_output.dir)) + add_overlay(OutputOverlay) + var/mutable_appearance/ModeratorOverlay = mutable_appearance('icons/obj/machines/rbmk.dmi',"moderator") + ModeratorOverlay.transform = ModeratorOverlay.transform.Turn(dir2angle(linked_moderator.dir)) + add_overlay(ModeratorOverlay) + +/obj/machinery/atmospherics/components/unary/rbmk/core/examine(mob/user) + . = ..() + var/percent = get_integrity_percent() + var/msg = "The reactor looks operational." + switch(percent) + if(0 to 10) + msg = "[src]'s seals are dangerously warped and you can see cracks all over the reactor vessel! " + if(10 to 40) + msg = "[src]'s seals are heavily warped and cracked! " + if(40 to 60) + msg = "[src]'s seals are holding, but barely. You can see some micro-fractures forming in the reactor vessel." + if(60 to 80) + msg = "[src]'s seals are in-tact, but slightly worn. There are no visible cracks in the reactor vessel." + if(80 to 90) + msg = "[src]'s seals are in good shape, and there are no visible cracks in the reactor vessel." + if(95 to 100) + msg = "[src]'s seals look factory new, and the reactor's in excellent shape." + . += msg diff --git a/code/modules/power/rbmk/rbmk_main_processes.dm b/code/modules/power/rbmk/rbmk_main_processes.dm new file mode 100644 index 0000000000000..0634b6f73598d --- /dev/null +++ b/code/modules/power/rbmk/rbmk_main_processes.dm @@ -0,0 +1,140 @@ +/** + * Main atmos processes + * process() Organizes all other calls, and is the best starting point for top-level logic. + */ + +/obj/machinery/atmospherics/components/unary/rbmk/core/process(delta_time) + //Pre-Checks + + //first check if the machine is active + if(!active) + return + + //then check if the other machines are still there + if(!check_part_connectivity()) + deactivate() + return + // Run the reaction if it is either live or being started + if (start_power || power) + atmos_process(delta_time) + damage_handler(delta_time) + check_alert() + soundloop.volume = clamp((power / 2), 0, 35) + update_pipenets() + update_appearance() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/atmos_process(delta_time) + var/datum/gas_mixture/coolant_input = linked_input.airs[1] + var/datum/gas_mixture/moderator_input = linked_moderator.airs[1] + var/datum/gas_mixture/coolant_output = linked_output.airs[1] + + //Firstly, heat up the reactor based off of rate_of_reaction. + var/input_moles = coolant_input.total_moles() //Firstly. Do we have enough moles of coolant? + if(input_moles >= minimum_coolant_level) + last_coolant_temperature = coolant_input.return_temperature()-273.15 + //Important thing to remember, once you slot in the fuel rods, this thing will not stop making heat, at least, not unless you can live to be thousands of years old which is when the spent fuel finally depletes fully. + var/heat_delta = ((coolant_input.return_temperature()-273.15) / 100) * gas_absorption_effectiveness //Take in the gas as a cooled input, cool the reactor a bit. The optimum, 100% balanced reaction sits at rate_of_reaction=1, coolant input temp of 200K / -73 celsius. + last_heat_delta = heat_delta + temperature += heat_delta + coolant_output.merge(coolant_input) //And now, shove the input into the output. + coolant_input.clear() //Clear out anything left in the input gate. + no_coolant_ticks = max(0, no_coolant_ticks-2) //Needs half as much time to recover the ticks than to acquire them + else + if(has_fuel()) + no_coolant_ticks++ + if(no_coolant_ticks > RBMK_NO_COOLANT_TOLERANCE) + temperature += temperature / 500 //This isn't really harmful early game, but when your reactor is up to full power, this can get out of hand quite quickly. + critical_threshold_proximity += temperature / 200 //Think fast loser. + check_alert() + playsound(src, 'sound/weapons/smash.ogg', 50, 1) //Just for the sound effect, to let you know you've fucked up. + + //Now, heat up the output and set our pressure. + coolant_output.set_temperature(temperature+273.15) //Heat the coolant output gas that we just had pass through us. + last_output_temperature = coolant_output.return_temperature()-273.15 + pressure = coolant_output.return_pressure() + power = (temperature / RBMK_TEMPERATURE_CRITICAL) * 100 + if(power < 0) // Not letting power get into the negatives, because -22% power is just absurd. + temperature = 0 + var/radioactivity_spice_multiplier = 1 //Some gasses make the reactor a bit spicy. + var/depletion_modifier = 0.035 //How rapidly do your rods decay + gas_absorption_effectiveness = gas_absorption_constant + last_power_produced = 0 + //Next up, handle moderators! + if(moderator_input.total_moles() >= minimum_coolant_level) + var/total_fuel_moles = moderator_input.get_moles(GAS_PLASMA) + (moderator_input.get_moles(GAS_NITROUS)*2)+ (moderator_input.get_moles(GAS_TRITIUM)*10) //n2o is 50% more efficient as fuel than plasma, but is harder to produce + var/power_modifier = max((moderator_input.get_moles(GAS_O2) / moderator_input.total_moles() * 10), 1) //You can never have negative IPM. For now. + if(total_fuel_moles >= minimum_coolant_level) //You at least need SOME fuel. + var/power_produced = max((total_fuel_moles / moderator_input.total_moles() * 10), 1) + last_power_produced = max(0,((power_produced*power_modifier)*moderator_input.total_moles())) + last_power_produced *= (max(0,power)/100) //Aaaand here comes the cap. Hotter reactor => more power. + last_power_produced *= base_power_modifier //Finally, we turn it into actual usable numbers. + radioactivity_spice_multiplier += moderator_input.get_moles(GAS_TRITIUM) / 5 //Chernobyl 2. + if(power >= 20) + coolant_output.adjust_moles(GAS_TRITIUM, total_fuel_moles/20) //Shove out tritium into the air when it's fuelled. You need to filter this off, or you're gonna have a bad time. + + var/total_control_moles = moderator_input.get_moles(GAS_N2) + (moderator_input.get_moles(GAS_CO2)*4) + (moderator_input.get_moles(GAS_PLUOXIUM)*8) //N2 helps you control the reaction at the cost of making it absolutely blast you with rads. Pluoxium has the same effect but without the rads! + if(total_control_moles >= minimum_coolant_level) + var/control_bonus = total_control_moles / 250 //1 mol of n2 -> 0.002 bonus control rod effectiveness, if you want a super controlled reaction, you'll have to sacrifice some power. + control_rod_effectiveness = initial(control_rod_effectiveness) + control_bonus + radioactivity_spice_multiplier += moderator_input.get_moles(GAS_N2) / 25 //An example setup of 50 moles of n2 (for dealing with spent fuel) leaves us with a radioactivity spice multiplier of 3. + radioactivity_spice_multiplier += moderator_input.get_moles(GAS_CO2) / 12.5 + var/total_permeability_moles = moderator_input.get_moles(GAS_BZ) + (moderator_input.get_moles(GAS_H2O)*2) + (moderator_input.get_moles(GAS_HYPERNOB)*10) + if(total_permeability_moles >= minimum_coolant_level) + var/permeability_bonus = total_permeability_moles / 500 + gas_absorption_effectiveness = gas_absorption_constant + permeability_bonus + var/total_degradation_moles = moderator_input.get_moles(GAS_NITRYL) //Because it's quite hard to get. + if(total_degradation_moles >= minimum_coolant_level*0.5) //I'll be nice. + depletion_modifier += total_degradation_moles / 15 //Oops! All depletion. This causes your fuel rods to get SPICY. + playsound(src, pick('sound/machines/sm/accent/normal/1.ogg','sound/machines/sm/accent/normal/2.ogg','sound/machines/sm/accent/normal/3.ogg','sound/machines/sm/accent/normal/4.ogg','sound/machines/sm/accent/normal/5.ogg'), 100, TRUE) + //From this point onwards, we clear out the remaining gasses. + moderator_input.clear() //Woosh. And the soul is gone. + rate_of_reaction += total_fuel_moles / 1000 + var/fuel_power = 0 //So that you can't magically generate rate_of_reaction with your control rods. + if(!has_fuel()) //Reactor must be fuelled and ready to go before we can heat it up boys. + rate_of_reaction = 0 + else + for(var/obj/item/fuel_rod/reactor_fuel_rod in fuel_rods) + rate_of_reaction += reactor_fuel_rod.fuel_power + fuel_power += reactor_fuel_rod.fuel_power + reactor_fuel_rod.deplete(depletion_modifier) + //Firstly, find the difference between the two numbers. + var/difference = abs(rate_of_reaction - desired_reate_of_reaction) + //Then, hit as much of that goal with our cooling per tick as we possibly can. + difference = clamp(difference, 0, control_rod_effectiveness) //And we can't instantly zap the rate_of_reaction to what we want, so let's zap as much of it as we can manage.... + if(difference > fuel_power && desired_reate_of_reaction > rate_of_reaction) + difference = fuel_power //Again, to stop you being able to run off of 1 fuel rod. + if(rate_of_reaction != desired_reate_of_reaction) + if(desired_reate_of_reaction > rate_of_reaction) + rate_of_reaction += difference + else if(desired_reate_of_reaction < rate_of_reaction) + rate_of_reaction -= difference + + rate_of_reaction = clamp(rate_of_reaction, 0, RBMK_MAX_CRITICALITY) + if(has_fuel()) + temperature += rate_of_reaction + else + temperature -= 10 //Nothing to heat us up, so. + update_icon() + radiation_pulse(src, temperature*radioactivity_spice_multiplier) + if(power >= 90 && world.time >= next_flicker) //You're overloading the reactor. Give a more subtle warning that power is getting out of control. + next_flicker = world.time + 1 MINUTES + for(var/obj/machinery/light/light in GLOB.machines) + if(DT_PROB(75, delta_time)) //If youre running the reactor cold though, no need to flicker the lights. + light.flicker() + for(var/atom/movable/object in get_turf(src)) + if(isliving(object) && temperature > 0) + var/mob/living/living_mob = object + living_mob.adjust_bodytemperature(clamp(temperature, BODYTEMP_COOLING_MAX, BODYTEMP_HEATING_MAX)) //If you're on fire, you heat up! + if(grilled_item) + SEND_SIGNAL(grilled_item, COMSIG_ITEM_GRILLED, grilled_item, delta_time) + grill_time += delta_time + grilled_item.AddComponent(/datum/component/sizzle) + + if(!last_power_produced) + last_power_produced = 150000 //Passively make 150KW if we dont have moderator + var/turf/reactor_turf = get_turf(src) + var/obj/structure/cable/reactor_cable = reactor_turf.get_cable_node() + if(reactor_cable) + reactor_cable.get_connections() + reactor_cable.add_avail(last_power_produced) + diff --git a/code/modules/power/rbmk/rbmk_monitoring.dm b/code/modules/power/rbmk/rbmk_monitoring.dm new file mode 100644 index 0000000000000..9693354378f11 --- /dev/null +++ b/code/modules/power/rbmk/rbmk_monitoring.dm @@ -0,0 +1,152 @@ +// RBMK Monitoring was moved here for QoL + +//Monitoring program. +/datum/computer_file/program/nuclear_monitor + filename = "rbmkmonitor" + filedesc = "Nuclear Reactor Monitoring" + category = PROGRAM_CATEGORY_ENGI + ui_header = "smmon_0.gif" + program_icon_state = "smmon_0" + extended_desc = "This program connects to specially calibrated sensors to provide information on the status of nuclear reactors." + requires_ntnet = TRUE + transfer_access = list(ACCESS_CONSTRUCTION) + network_destination = "rbmk monitoring system" + size = 5 + tgui_id = "NtosRbmkStats" + program_icon = "radiation" + alert_able = TRUE + var/active = TRUE //Easy process throttle + var/last_status + COOLDOWN_DECLARE(next_stat_interval) + var/list/kpaData = list() + var/list/powerData = list() + var/list/tempInputData = list() + var/list/tempOutputdata = list() + var/list/reactors + var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor // Currently selected RBMK Reactor. + +/datum/computer_file/program/nuclear_monitor/Destroy() + clear_signals() + reactor = null + return ..() + +/datum/computer_file/program/nuclear_monitor/process_tick() + ..() + if(!reactor || !active) + return FALSE + var/new_status = get_status() + if(last_status != new_status) + last_status = new_status + ui_header = "smmon_[last_status].gif" + program_icon_state = "smmon_[last_status]" + if(istype(computer)) + computer.update_icon() + if(COOLDOWN_FINISHED(src, next_stat_interval)) + COOLDOWN_START(src, next_stat_interval, 1 SECONDS) //You only get a slow tick. + kpaData += (reactor) ? reactor.pressure : 0 + if(length(kpaData) > 100) //Only lets you track over a certain timeframe. + kpaData.Cut(1, 2) + powerData += (reactor) ? reactor.power*10 : 0 //We scale up the figure for a consistent:tm: scale + if(length(powerData) > 100) //Only lets you track over a certain timeframe. + powerData.Cut(1, 2) + tempInputData += (reactor) ? reactor.last_coolant_temperature : 0 //We scale up the figure for a consistent:tm: scale + if(length(tempInputData) > 100) //Only lets you track over a certain timeframe. + tempInputData.Cut(1, 2) + tempOutputdata += (reactor) ? reactor.last_output_temperature : 0 //We scale up the figure for a consistent:tm: scale + if(length(tempOutputdata) > 100) //Only lets you track over a certain timeframe. + tempOutputdata.Cut(1, 2) + +/datum/computer_file/program/nuclear_monitor/on_start(mob/living/user) + . = ..(user) + //No reactor? Go find one then. + if(!reactor) + var/user_z_level = user.get_virtual_z_level() + for(var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core in GLOB.machines) + if(compare_z(reactor_core.get_virtual_z_level(), user_z_level)) + reactor = reactor_core + break + active = TRUE + +/datum/computer_file/program/nuclear_monitor/kill_program(forced = FALSE) + active = FALSE + ..() + +/datum/computer_file/program/nuclear_monitor/ui_data() + var/list/data = list() + data["integrity"] = reactor ? reactor.get_integrity_percent() : 100 + data["powerData"] = powerData + data["kpaData"] = kpaData + data["tempInputData"] = tempInputData + data["tempOutputdata"] = tempOutputdata + data["coolantInput"] = reactor ? reactor.last_coolant_temperature : 0 + data["coolantOutput"] = reactor ? reactor.last_output_temperature : 0 + data["power"] = reactor ? reactor.power : 0 + data["kpa"] = reactor ? reactor.pressure : 0 + return data + +/datum/computer_file/program/nuclear_monitor/ui_act(action, params) + if(..()) + return TRUE + + switch(action) + if("swap_reactor") + var/list/choices = list() + var/user_z_level = usr.get_virtual_z_level() + for(var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core in GLOB.machines) + if(!compare_z(reactor_core.get_virtual_z_level(), user_z_level)) + continue + choices += reactor_core + reactor = input(usr, "What reactor do you wish to monitor?", "Nuclear Monitoring Selector", null) as null|anything in choices + powerData = list() + kpaData = list() + tempInputData = list() + tempOutputdata = list() + return TRUE + +/datum/computer_file/program/nuclear_monitor/proc/set_signals() + if(reactor) + RegisterSignal(reactor, COMSIG_SUPERMATTER_DELAM_ALARM, PROC_REF(send_alert), override = TRUE) + RegisterSignal(reactor, COMSIG_SUPERMATTER_DELAM_START_ALARM, PROC_REF(send_start_alert), override = TRUE) + +/datum/computer_file/program/nuclear_monitor/proc/clear_signals() + if(reactor) + UnregisterSignal(reactor, COMSIG_SUPERMATTER_DELAM_ALARM) + UnregisterSignal(reactor, COMSIG_SUPERMATTER_DELAM_START_ALARM) + +/datum/computer_file/program/nuclear_monitor/proc/get_status() + . = NUCLEAR_REACTOR_INACTIVE + for(var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core in reactors) + . = max(., reactor_core.get_status()) + +/datum/computer_file/program/nuclear_monitor/proc/send_alert() + if(!computer.get_ntnet_status()) + return + if(computer.active_program != src) + computer.alert_call(src, "Nuclear reactor meltdown in progress!") + alert_pending = TRUE + +/datum/computer_file/program/nuclear_monitor/proc/send_start_alert() + if(!computer.get_ntnet_status()) + return + if(computer.active_program == src) + computer.alert_call(src, "Nuclear reactor meltdown in progress!") + +// Nuclear reactor UI for ghosts only. Inherited attack_ghost will call this. +/obj/machinery/atmospherics/components/unary/rbmk/core/ui_interact(mob/user, datum/tgui/ui) + if(!isobserver(user)) + return FALSE + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "NtosGhostRbmkStats") + ui.set_autoupdate(TRUE) + ui.open() + +/obj/machinery/atmospherics/components/unary/rbmk/core/ui_data() + var/list/data = list() + data["integrity"] = get_integrity_percent() + data["coolantInput"] = last_coolant_temperature + data["coolantOutput"] = last_output_temperature + data["power"] = power + data["kpa"] = pressure + return data diff --git a/code/modules/power/rbmk/rbmk_parts.dm b/code/modules/power/rbmk/rbmk_parts.dm new file mode 100644 index 0000000000000..d93e28d7a58be --- /dev/null +++ b/code/modules/power/rbmk/rbmk_parts.dm @@ -0,0 +1,257 @@ +/** + * This file contain the five main parts of the RBMK, those are the: fuel input, moderator input, waste output, control rod computer and rbmk core + */ + +/obj/machinery/computer/reactor + name = "reactor control console" + desc = "Scream" + light_color = "#55BA55" + light_power = 1 + light_range = 3 + var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor + var/active = FALSE + +/obj/machinery/computer/reactor/Initialize(mapload) + . = ..() + +/obj/machinery/computer/reactor/control_rods + name = "control rod management computer" + desc = "A computer which can remotely raise / lower the control rods of an RBMK class nuclear reactor." + circuit = /obj/item/circuitboard/computer/control_rods + icon_screen = "smmon_1" + icon_keyboard = "tech_key" + +/obj/machinery/computer/reactor/control_rods/process() + . = ..() + if(reactor) + var/last_status = reactor.desired_reate_of_reaction + switch(last_status) + if(0 to 0.5) + icon_screen = "smmon_1" + if(0.5 to 1) + icon_screen = "smmon_2" + if(1 to 1.5) + icon_screen = "smmon_3" + if(1.5 to 2) + icon_screen = "smmon_4" + if(2 to 2.5) + icon_screen = "smmon_5" + if(2.5 to 3) + icon_screen = "smmon_6" + else + icon_screen = "smmon_1" + update_overlays() + update_appearance() + +/obj/machinery/computer/reactor/control_rods/attack_hand(mob/living/user) + . = ..() + ui_interact(user) + +/obj/machinery/computer/reactor/control_rods/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "RbmkControlRods") + ui.open() + ui.set_autoupdate(TRUE) + +/obj/machinery/computer/reactor/control_rods/ui_act(action, params) + if(..()) + return + if(!reactor) + return + if(action == "input") + var/input = text2num(params["target"]) + reactor.desired_reate_of_reaction = clamp(input, 0, 3) + +/obj/machinery/computer/reactor/control_rods/ui_data(mob/user) + var/list/data = list() + data["control_rods"] = 0 + data["k"] = 0 + data["desiredK"] = 0 + if(reactor) + data["k"] = reactor.rate_of_reaction + data["desiredK"] = reactor.desired_reate_of_reaction + data["control_rods"] = 100 - (reactor.desired_reate_of_reaction / 3 * 100) //Rod insertion is extrapolated as a function of the percentage of rate_of_reaction + return data + +/obj/machinery/computer/reactor/attack_robot(mob/user) + . = ..() + attack_hand(user) + +/obj/machinery/computer/reactor/attack_ai(mob/user) + . = ..() + attack_hand(user) + +/obj/machinery/computer/reactor/attackby(obj/item/tool, mob/user, params) + if(tool.tool_behaviour == TOOL_MULTITOOL) + var/datum/component/buffer/heldmultitool = get_held_buffer_item(user) + if(heldmultitool) + var/obj/machinery/atmospherics/components/unary/rbmk/core/reactor_core = heldmultitool.target + if(istype(reactor_core) && reactor_core != src) + if(!(src in reactor_core.linked_interface)) + reactor_core.linked_interface = src + reactor_core.ui_update() + reactor = reactor_core + to_chat(user, "You upload the link to the [src].") + + +/obj/machinery/computer/reactor/proc/get_held_buffer_item(mob/user) + if(isAI(user)) + var/mob/living/silicon/ai/ai_user = user + return ai_user.aiMulti.GetComponent(/datum/component/buffer) + + var/obj/item/held_item = user.get_active_held_item() + var/found_component = held_item?.GetComponent(/datum/component/buffer) + if(found_component && in_range(user, src)) + return found_component + + return null + +/obj/machinery/atmospherics/components/unary/rbmk + icon = 'icons/obj/machines/rbmkparts.dmi' + name = "thermomachine" + desc = "Heats or cools gas in connected pipes." + anchored = TRUE + density = FALSE + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 + layer = OBJ_LAYER + pipe_flags = PIPING_ONE_PER_TURF | PIPING_DEFAULT_LAYER_ONLY + circuit = /obj/item/circuitboard/machine/thermomachine + ///Check if the machine has been activated + var/active = FALSE + ///Vars for the state of the icon of the object (open, off, active) + var/icon_state_open + var/icon_state_off + +/obj/machinery/atmospherics/components/unary/rbmk/Initialize(mapload) + . = ..() + initialize_directions = dir + +/obj/machinery/atmospherics/components/unary/rbmk/update_overlays() + . = ..() + +/obj/machinery/atmospherics/components/unary/rbmk/update_layer() + return + +/obj/machinery/atmospherics/components/unary/rbmk/coolant_input + name = "RBMK coolant input port" + desc = "Input port for the RBMK Fusion Reactor, designed to take in coolant." + icon = 'icons/obj/machines/rbmk.dmi' + layer = GAS_PIPE_HIDDEN_LAYER + +/obj/machinery/atmospherics/components/unary/rbmk/waste_output + name = "RBMK waste output port" + desc = "Waste port for the RBMK Fusion Reactor, designed to output the hot waste gases coming from the core of the machine." + icon = 'icons/obj/machines/rbmk.dmi' + layer = GAS_PIPE_HIDDEN_LAYER + +/obj/machinery/atmospherics/components/unary/rbmk/moderator_input + name = "RBMK moderator input port" + desc = "Moderator port for the RBMK Fusion Reactor, designed to move gases inside the machine to cool and control the flow of the reaction." + icon = 'icons/obj/machines/rbmk.dmi' + layer = GAS_PIPE_HIDDEN_LAYER + +/* +* Interface and corners +*/ +/obj/machinery/rbmk + name = "rbmk_core" + desc = "rbmk_core" + icon = 'icons/obj/machines/rbmkparts.dmi' + icon_state = "core" + move_resist = INFINITY + anchored = TRUE + density = FALSE + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 + power_channel = AREA_USAGE_ENVIRON +/obj/item/book/manual/wiki/rbmk + name = "\improper Hayne's nuclear reactor owner's manual" + icon_state ="bookEngineering2" + author = "CogWerk Engineering Reactor Design Department" + title = "Haynes nuclear reactor owner's manual" + page_link = "Guide_to_the_Nuclear_Reactor" + +/obj/item/RBMK_box + name = "RBMK box" + desc = "If you see this, call the police." + icon = 'icons/obj/machines/rbmkparts.dmi' + icon_state = "core" + item_flags = NO_PIXEL_RANDOM_DROP + var/box_type = "impossible" // ///What kind of box are we handling? + var/part_path //What's the path of the machine we making? + +/obj/item/RBMK_box/body + name = "RBMK box body" + desc = "A main storage body housing for your RBMK nuclear reactor." + icon_state = "wall" + box_type = "normal" + +/obj/item/RBMK_box/body/coolant_input + name = "RBMK box coolant input" + icon_state = "input" + part_path = /obj/machinery/atmospherics/components/unary/rbmk/coolant_input + box_type = "coolant_input" + +/obj/item/RBMK_box/body/moderator_input + name = "RBMK box moderator input" + icon_state = "moderator" + part_path = /obj/machinery/atmospherics/components/unary/rbmk/moderator_input + box_type = "moderator_input" + +/obj/item/RBMK_box/body/waste_output + name = "RBMK box waste output" + icon_state = "output" + part_path = /obj/machinery/atmospherics/components/unary/rbmk/waste_output + box_type = "waste_output" + +/obj/item/RBMK_box/core + name = "RBMK box core" + icon_state = "core" + desc = "A box for the center piece core of the RBMK nuclear reactor." + part_path = /obj/machinery/atmospherics/components/unary/rbmk/core + box_type = "center" + +/obj/item/RBMK_box/core/multitool_act(mob/living/user, obj/item/I) + . = ..() + var/list/parts = list() + var/types_seen = list() + for(var/obj/item/RBMK_box/box in orange(1,src)) + + var/direction = get_dir(src, box) + box.dir = direction + if(box.box_type in list("coolant_input", "waste_output", "moderator_input")) + if(box.Adjacent(box, src)) + if(box.box_type in types_seen) + return + else + parts |= box + types_seen += box.box_type + else + parts |= box + if(length(parts) == 8) + build_reactor(parts) + return + +/obj/item/RBMK_box/core/proc/build_reactor(list/parts) + for(var/obj/item/RBMK_box/box in orange(1,src)) + if(box.box_type == "coolant_input") + var/obj/machinery/atmospherics/components/unary/rbmk/coolant_input/coolant_input_machine = new/obj/machinery/atmospherics/components/unary/rbmk/coolant_input(box.loc, TRUE) + coolant_input_machine.dir = box.dir + coolant_input_machine.SetInitDirections() + coolant_input_machine.build_network() + else if(box.box_type == "moderator_input") + var/obj/machinery/atmospherics/components/unary/rbmk/moderator_input/moderator_input_machine = new/obj/machinery/atmospherics/components/unary/rbmk/moderator_input(box.loc, TRUE) + moderator_input_machine.dir = box.dir + moderator_input_machine.SetInitDirections() + moderator_input_machine.build_network() + else if(box.box_type == "waste_output") + var/obj/machinery/atmospherics/components/unary/rbmk/waste_output/waste_output_machine = new/obj/machinery/atmospherics/components/unary/rbmk/waste_output(box.loc, TRUE) + waste_output_machine.dir = box.dir + waste_output_machine.SetInitDirections() + waste_output_machine.build_network() + new /obj/machinery/atmospherics/components/unary/rbmk/core(loc, TRUE) + for(var/obj/item/RBMK_box/box in parts) + qdel(box) + qdel(src) diff --git a/code/modules/power/rbmk/rbmk_procs.dm b/code/modules/power/rbmk/rbmk_procs.dm new file mode 100644 index 0000000000000..cfd85ac3866fe --- /dev/null +++ b/code/modules/power/rbmk/rbmk_procs.dm @@ -0,0 +1,614 @@ +//This section contain all procs that helps building, destroy and control the RBMK + +/obj/machinery/atmospherics/components/unary/rbmk/core/attackby(obj/item/attacked_item, mob/user, params) + if(istype(attacked_item, /obj/item/fuel_rod)) + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You cannot insert fuel into [src] when it has been raised above [SAFE_POWER_LEVEL]% power.") + return FALSE + if(length(fuel_rods) >= 5) + to_chat(user, "[src] is already at maximum fuel load.") + return FALSE + to_chat(user, "You start to insert [attacked_item] into [src]...") + radiation_pulse(src, temperature) //Wear protective equipment when even breathing near a reactor! + if(do_after(user, 5 SECONDS, target=src)) + if(length(fuel_rods) >= 5) + to_chat(user, "[src] is already at maximum fuel load.") + return FALSE + else if(length(fuel_rods) == 0) + activate(user) //That was the first fuel rod. Let's heat it up. + else // Not the first fuel rod? Play the sound. + playsound(src, pick('sound/effects/rbmk/switch1.ogg','sound/effects/rbmk/switch2.ogg','sound/effects/rbmk/switch3.ogg'), 100, FALSE) + fuel_rods += attacked_item + attacked_item.forceMove(src) + update_appearance() + return TRUE + if(istype(attacked_item, /obj/item/sealant)) + var/obj/item/sealant/sealant = attacked_item + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You cannot repair [src] while it is running at above [SAFE_POWER_LEVEL]% power.") + return FALSE + if(critical_threshold_proximity <= REACTOR_NEW_SEALS * critical_threshold_proximity_archived) + to_chat(user, "[src]'s seals are already in-tact, repairing them further would require a new set of seals.") + return FALSE + if(critical_threshold_proximity >= REACTOR_CRACKED_SEALS * critical_threshold_proximity_archived) //Heavily damaged. + to_chat(user, "[src]'s reactor vessel is cracked and worn, you need to repair the cracks with a welder before you can repair the seals.") + return FALSE + if(do_after(user, 5 SECONDS, target=src)) + if(critical_threshold_proximity <= REACTOR_NEW_SEALS*critical_threshold_proximity_archived) //They might've stacked doafters + to_chat(user, "[src]'s seals are already in-tact, repairing them further would require a new set of seals.") + return FALSE + playsound(src, 'sound/effects/spray2.ogg', 50, 1, -6) + user.visible_message("[user] applies sealant to some of [src]'s worn out seals.", "You apply sealant to some of [src]'s worn out seals.") + critical_threshold_proximity -= sealant.repair_power // Default is 10 + critical_threshold_proximity = clamp(critical_threshold_proximity, 0, initial(critical_threshold_proximity)) + return TRUE + if(attacked_item.tool_behaviour == TOOL_WELDER) + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You can't repair [src] while it is running at above [SAFE_POWER_LEVEL]% power.") + return FALSE + if(critical_threshold_proximity < REACTOR_CRACKED_SEALS * critical_threshold_proximity_archived) + to_chat(user, "[src] is free from cracks. Further repairs must be carried out with flexi-seal sealant.") + return FALSE + if(attacked_item.use_tool(src, user, 0, volume=40)) + if(critical_threshold_proximity < REACTOR_CRACKED_SEALS * critical_threshold_proximity_archived) + to_chat(user, "[src] is free from cracks. Further repairs must be carried out with flexi-seal sealant.") + return FALSE + critical_threshold_proximity -= 20 + to_chat(user, "You weld together some of [src]'s cracks. This'll do for now.") + return TRUE + if(attacked_item.tool_behaviour == TOOL_SCREWDRIVER) + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You can't open the maintenance panel of the [src] while it's still above [SAFE_POWER_LEVEL]% power!") + return FALSE + if (length(fuel_rods) > 0) + to_chat(user, "You can't open the maintenance panel of the [src] while it still has fuel rods inside!") + return FALSE + default_deconstruction_screwdriver(user, "reactor", "reactor_open", attacked_item) + update_appearance() + return TRUE + if(attacked_item.tool_behaviour == TOOL_CROWBAR) + if(panel_open) + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You can't deconstruct the [src] while it's still above [SAFE_POWER_LEVEL]% power!") + return FALSE + if (length(fuel_rods) > 0) + to_chat(user, "You can't deconstruct [src] while it still has fuel rods inside!") + return FALSE + disassemble(attacked_item) + return TRUE + else + if(power >= SAFE_POWER_LEVEL) + to_chat(user, "You can't remove any fuel rods while the [src] is above [SAFE_POWER_LEVEL]% power!") + return FALSE + if (length(fuel_rods) == 0) + to_chat(user, "The [src] is empty of fuel rods!") + return FALSE + removeFuelRod(user, src) + update_appearance() + return TRUE + if(attacked_item.tool_behaviour == TOOL_MULTITOOL) + var/datum/component/buffer/heldmultitool = get_held_buffer_item(usr) + STORE_IN_BUFFER(heldmultitool.parent, src) + . = TRUE + to_chat(user, "You download the link from the nuclear reactor.") + return ..() + +/* +Called by multitool_act() in rbmk_parts.dm, by atmos_process() in rbmk_main_processes.dm and by atmos_process() in the same file +This proc checks the surrounding of the core to ensure that the machine has been build correctly, returns false if there is a missing piece/wrong placed one +*/ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/check_part_connectivity() + . = TRUE + if(!anchored || panel_open) + return FALSE + + for(var/obj/machinery/rbmk/object in orange(1,src)) + if(. == FALSE) + break + + if(object.panel_open) + . = FALSE + + if(get_step(object,REVERSE_DIR(object.dir)) != loc) + . = FALSE + + for(var/obj/machinery/atmospherics/components/unary/rbmk/object in orange(1,src)) + if(. == FALSE) + break + + if(object.panel_open) + . = FALSE + + if(get_step(object,REVERSE_DIR(object.dir)) != loc) + . = FALSE + + if(istype(object,/obj/machinery/atmospherics/components/unary/rbmk/coolant_input)) + if(linked_input && linked_input != object) + . = FALSE + linked_input = object + machine_parts |= object + + if(istype(object,/obj/machinery/atmospherics/components/unary/rbmk/waste_output)) + if(linked_output && linked_output != object) + . = FALSE + linked_output = object + machine_parts |= object + + if(istype(object,/obj/machinery/atmospherics/components/unary/rbmk/moderator_input)) + if(linked_moderator && linked_moderator != object) + . = FALSE + linked_moderator = object + machine_parts |= object + + if(!linked_input || !linked_moderator || !linked_output) + . = FALSE + +/* +Called by multitool_act() in rbmk_parts.dm +It sets the pieces to active, allowing the player to start the main reaction +Arguments: +* -user: the player doing the action +*/ + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/activate(mob/living/user) + if(active) + to_chat(user, ("You already activated the machine.")) + return + to_chat(user, ("You activate the machine.")) + active = TRUE + start_power = TRUE + update_appearance() + if (linked_interface) + linked_interface.active = TRUE + linked_interface.update_appearance() + RegisterSignal(linked_interface, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals)) + linked_input.active = TRUE + linked_input.update_appearance() + RegisterSignal(linked_input, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals)) + linked_output.active = TRUE + linked_output.update_appearance() + RegisterSignal(linked_output, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals)) + linked_moderator.active = TRUE + linked_moderator.update_appearance() + RegisterSignal(linked_moderator, COMSIG_PARENT_QDELETING, PROC_REF(unregister_signals)) + START_PROCESSING(SSmachines, src) + desired_reate_of_reaction = 1 + can_unwrench = 0 + var/startup_sound = pick('sound/effects/rbmk/startup.ogg', 'sound/effects/rbmk/startup2.ogg') + playsound(loc, startup_sound, 50) + SSblackbox.record_feedback("tally", "engine_stats", 1, "agcnr") + SSblackbox.record_feedback("tally", "engine_stats", 1, "started") + soundloop.start() + +/obj/machinery/atmospherics/components/unary/rbmk/proc/get_held_buffer_item(mob/user) + if(isAI(user)) + var/mob/living/silicon/ai/ai_user = user + return ai_user.aiMulti.GetComponent(/datum/component/buffer) + + var/obj/item/held_item = user.get_active_held_item() + var/found_component = held_item?.GetComponent(/datum/component/buffer) + if(found_component && in_range(user, src)) + return found_component + +/* + * Called when a part gets deleted around the rbmk, called on Destroy() of the rbmk core in rbmk_core.dm + * Unregister the signals attached to the core from the various machines, if only_signals is false it will also call deactivate() + * Arguments: + * only_signals: default FALSE, if true the proc will not call the deactivate() proc + */ + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/unregister_signals(only_signals = FALSE) + SIGNAL_HANDLER + if(linked_interface) + UnregisterSignal(linked_interface, COMSIG_PARENT_QDELETING) + if(linked_input) + UnregisterSignal(linked_input, COMSIG_PARENT_QDELETING) + if(linked_output) + UnregisterSignal(linked_output, COMSIG_PARENT_QDELETING) + if(linked_moderator) + UnregisterSignal(linked_moderator, COMSIG_PARENT_QDELETING) + if(!only_signals) + deactivate() + +/** + * Called by unregister_signals() in this file, called when the main fusion processes check_part_connectivity() returns false + * Deactivate the various machines by setting the active var to false, updates the machines icon and set the linked machine vars to null + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/deactivate() + if(!active) + return + active = FALSE + start_power = FALSE + update_appearance() + if(linked_interface) + linked_interface.active = FALSE + linked_interface.update_appearance() + if(linked_input) + linked_input.active = FALSE + linked_input.update_appearance() + if(linked_output) + linked_output.active = FALSE + linked_output.update_appearance() + if(linked_moderator) + linked_moderator.active = FALSE + linked_moderator.update_appearance() + STOP_PROCESSING(SSmachines, src) + rate_of_reaction = 0 + can_unwrench = 1 + desired_reate_of_reaction = 0 + temperature = 0 + soundloop.stop() + update_appearance() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/disassemble(obj/item/I) + unregister_signals() + deactivate() + var/parts = list(/obj/item/RBMK_box/core, + /obj/item/RBMK_box/body/coolant_input, + /obj/item/RBMK_box/body/moderator_input, + /obj/item/RBMK_box/body/waste_output, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body, + /obj/item/RBMK_box/body) + for(var/item in parts) + new item(get_turf(src)) + I.play_tool_sound(src, 50) + qdel(src) + +/** + * Updates all related pipenets from all connected components + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/update_pipenets() + update_parents() + linked_input.update_parents() + linked_output.update_parents() + linked_moderator.update_parents() + +/** + * Called by the main fusion processes in hfr_main_processes.dm + * Check the power use of the machine, return TRUE if there is enough power in the powernet + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/check_power_use() + if(machine_stat & (NOPOWER|BROKEN)) + return FALSE + if(use_power == ACTIVE_POWER_USE) + use_power((power + 1) * IDLE_POWER_USE) + return TRUE + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/has_fuel() + return length(fuel_rods) + + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/removeFuelRod(mob/user, /obj/machinery/atmospherics/components/unary/rbmk/core/reactor) + if(src.power > SAFE_POWER_LEVEL) + to_chat(user, "You cannot remove fuel from [src] when it is above [SAFE_POWER_LEVEL]% power.") + return FALSE + if(length(fuel_rods) == 0) + to_chat(user, "[src] does not have any fuel rods loaded.") + return FALSE + var/atom/movable/fuel_rod = input(usr, "Select a fuel rod to remove", "Fuel Rods List", null) as null|anything in src.fuel_rods + if(!fuel_rod) + return + playsound(src, pick('sound/effects/rbmk/switch1.ogg','sound/effects/rbmk/switch2.ogg','sound/effects/rbmk/switch3.ogg'), 100, FALSE) + fuel_rod.forceMove(get_turf(src)) + src.fuel_rods -= fuel_rod + +/** + * Check the integrity level and returns the status of the machine + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/get_status() + switch(get_integrity_percent()) + if(0 to REACTOR_MELTING_PERCENT) + return REACTOR_MELTING + if(REACTOR_MELTING_PERCENT to REACTOR_EMERGENCY_PERCENT) + return REACTOR_EMERGENCY + if(REACTOR_EMERGENCY_PERCENT to REACTOR_DANGER_PERCENT) + return REACTOR_DANGER + if(REACTOR_DANGER_PERCENT to REACTOR_WARNING_PERCENT) + return REACTOR_NOMINAL + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/start_alarm() + if(alarm == FALSE) + alarm = TRUE + alarmloop.start() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/end_alarm() + alarmloop.stop() + alarm = FALSE + +/** + * Getter for the machine integrity + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/get_integrity_percent() + var/integrity = critical_threshold_proximity / melting_point + integrity = round(100 - integrity * 100, 0.01) + integrity = integrity < 0 ? 0 : integrity + return integrity + +/** + * Get how charged the area's APC is + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/get_area_cell_percent() + // Make sure to get APC levels from the same area the core draws from + // Just in case people build an HFR across boundaries + var/area/area = get_area(src) + if (!area) + return 0 + var/obj/machinery/power/apc/apc = area.apc + if (!apc) + return 0 + var/obj/item/stock_parts/cell/cell = apc.cell + if (!cell) + return 0 + return cell.percent() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/on_entered(datum/source, atom/movable/movable_atom, oldloc) + SIGNAL_HANDLER + if(istype(movable_atom, /obj/item/food)) + grilled_item = movable_atom + grillStart(grilled_item) + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/on_exited(atom/movable/gone_atom, direction) + if(direction == grilled_item) + finish_grill() + grilled_item = null + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/grillStart(/obj/item/food/grilled_item) + RegisterSignal(grilled_item, COMSIG_GRILL_COMPLETED, PROC_REF(grill_complete)) + grill_loop.start() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/finish_grill() + SEND_SIGNAL(grilled_item, COMSIG_GRILL_FOOD, grilled_item, grill_time) + grill_time = 0 + UnregisterSignal(grilled_item, COMSIG_GRILL_COMPLETED, PROC_REF(grill_complete)) + grill_loop.stop() + +///Called when a food is transformed by the grillable component +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/grill_complete(obj/item/source, atom/grilled_result) + SIGNAL_HANDLER + grilled_item = grilled_result //use the new item!! + + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/damage_handler(delta_time) + critical_threshold_proximity_archived = critical_threshold_proximity + if(rate_of_reaction <= 0 && temperature <= 0 && !has_fuel()) + deactivate() + //First alert condition: Overheat + var/turf/core_turf = get_turf(src) + if(temperature >= RBMK_TEMPERATURE_CRITICAL) + var/damagevalue = (temperature - 900)/250 + critical_threshold_proximity += damagevalue + warning_damage_flags |= RBMK_TEMPERATURE_DAMAGE + check_alert() + if(critical_threshold_proximity >= melting_point) + countdown() //Oops! All meltdown + return + if(temperature < -200) //That's as cold as I'm letting you get it, engineering. + temperature = -200 + if (pressure >= RBMK_PRESSURE_CRITICAL) + playsound(src, 'sound/machines/clockcult/steam_whoosh.ogg', 100, TRUE) + core_turf.atmos_spawn_air("water_vapor=[pressure/100];TEMP=[temperature+273.15]") + // Warning: Pressure reaching critical thresholds! + var/damagevalue = (pressure-10100)/1500 + critical_threshold_proximity += damagevalue + warning_damage_flags |= RBMK_PRESSURE_DAMAGE + check_alert() + if(critical_threshold_proximity >= melting_point) + countdown() + return +/** + * Called by process_atmos() in rbmk_main_processes.dm + * Called after checking the damage of the machine, calls alarm() and countdown() + * Broadcast messages into engi and common radio + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/check_alert() + if(critical_threshold_proximity < warning_point) + end_alarm() + return + if((REALTIMEOFDAY - lastwarning) / 10 >= WARNING_TIME_DELAY) + if(critical_threshold_proximity > emergency_point) + radio.talk_into(src, "[emergency_alert] Integrity: [get_integrity_percent()]%", common_channel) + lastwarning = REALTIMEOFDAY + if(!has_reached_emergency) + investigate_log("has reached the emergency point for the first time.", INVESTIGATE_ENGINES) + message_admins("[src] has reached the emergency point [ADMIN_JMP(src)].") + has_reached_emergency = TRUE + send_radio_explanation() + start_alarm() + else if(critical_threshold_proximity > critical_threshold_proximity_archived) // The damage is still going up + lastwarning = REALTIMEOFDAY - (WARNING_TIME_DELAY * 5) + send_radio_explanation() + start_alarm() + else if (critical_threshold_proximity < critical_threshold_proximity_archived)// Phew, we're safe, damage going down + radio.talk_into(src, "[safe_alert] Integrity: [get_integrity_percent()]%", engineering_channel) + lastwarning = REALTIMEOFDAY + end_alarm() + +/** + * Called by check_alert() in this file + * Called to explain in radio what the issues are with the HFR + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/send_radio_explanation() + if(warning_damage_flags & RBMK_PRESSURE_DAMAGE) + radio.talk_into(src, "Warning: Reactor overpressurized! Integrity: [get_integrity_percent()]%", engineering_channel) + warning_damage_flags &= RBMK_PRESSURE_DAMAGE + warning_damage_flags &= RBMK_TEMPERATURE_DAMAGE //If it is both overpressurized and overheating, just send the more important message + else if(warning_damage_flags & RBMK_TEMPERATURE_DAMAGE) + radio.talk_into(src, "Warning: Reactor overheating! Integrity: [get_integrity_percent()]%", engineering_channel) + warning_damage_flags &= RBMK_TEMPERATURE_DAMAGE + +/** + * Called by check_alert() in this file + * Called when the damage has reached critical levels, start the countdown before the destruction, calls meltdown() + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/countdown() + set waitfor = FALSE + + if(final_countdown) // We're already doing it go away + return + final_countdown = TRUE + var/speaking = "[emergency_alert] The RBMK has reached critical integrity failure. Emergency control rods lowered." + radio.talk_into(src, speaking, common_channel, language = get_selected_language()) + + notify_ghosts( + "The [src] has begun melting down!", + source = src, + header = "Meltdown Incoming", + ghost_sound = 'sound/machines/warning-buzzer.ogg', + notify_volume = 75, + ) + + for(var/i in REACTOR_COUNTDOWN_TIME to 0 step -10) + if(critical_threshold_proximity < melting_point) // Cutting it a bit close there engineers + radio.talk_into(src, "[safe_alert] Failsafe has been disengaged.", common_channel) + final_countdown = FALSE + return + else if((i % 50) != 0 && i > 50) // A message once every 5 seconds until the final 5 seconds which count down individualy + sleep(1 SECONDS) + continue + else if(i > 50) + if(i == 5 SECONDS) + sound_to_playing_players('sound/effects/rbmk/explode.ogg') + speaking = "[DisplayTimeText(i, TRUE)] remain before total integrity failure." + else + speaking = "[i*0.1]..." + radio.talk_into(src, speaking, common_channel) + sleep(1 SECONDS) + + if(pressure > RBMK_PRESSURE_CRITICAL) + blowout() + else if(temperature > RBMK_TEMPERATURE_CRITICAL) + meltdown() + else + meltdown() //This is caused if neither pressure nor temperature was in critical. We still want to explode + + +/** + * Called by countdown() in this file + * Create the explosion before deleting the machine core. + */ +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/meltdown() + set waitfor = FALSE + SSair.atmos_machinery -= src //Annd we're now just a useless brick. + update_icon() + STOP_PROCESSING(SSmachines, src) + AddComponent(/datum/component/radioactive, 15000 , src) + var/turf/reactor_turf = get_turf(src) + var/rbmkzlevel = reactor_turf.get_virtual_z_level() + for(var/mob/player_mob in GLOB.player_list) + if(compare_z(rbmkzlevel, player_mob.get_virtual_z_level())) + to_chat(player_mob, "You hear a horrible metallic hissing.") + SEND_SIGNAL(player_mob, COMSIG_ADD_MOOD_EVENT, "delam", /datum/mood_event/delam) //Might as well use the same moodlet since its essentialy the same thing happening + + for(var/obj/machinery/power/apc/apc in GLOB.apcs_list) + if(prob(70) && compare_z(rbmkzlevel, apc.get_virtual_z_level())) + apc.overload_lighting() + var/datum/gas_mixture/coolant_input = linked_input.airs[1] + var/datum/gas_mixture/moderator_input = linked_moderator.airs[1] + var/datum/gas_mixture/coolant_output = linked_output.airs[1] + coolant_input.set_temperature((temperature+273.15)*2) + moderator_input.set_temperature((temperature+273.15)*2) + coolant_output.set_temperature((temperature+273.15)*2) + reactor_turf.assume_air(coolant_input) + reactor_turf.assume_air(moderator_input) + reactor_turf.assume_air(coolant_output) + explosion(get_turf(src), 0, 5, 10, 20, TRUE, TRUE) + empulse(get_turf(src), 20, 30) + SSblackbox.record_feedback("tally", "engine_stats", 1, "failed") + SSblackbox.record_feedback("tally", "engine_stats", 1, "agcnr") + Destroy() + +/obj/machinery/atmospherics/components/unary/rbmk/core/proc/blowout() + explosion(get_turf(src), GLOB.MAX_EX_DEVESTATION_RANGE, GLOB.MAX_EX_HEAVY_RANGE, GLOB.MAX_EX_LIGHT_RANGE, GLOB.MAX_EX_FLASH_RANGE) + var/turf/reactor_turf = get_turf(src) + var/rbmkzlevel = reactor_turf.get_virtual_z_level() + for(var/mob/player_mob in GLOB.player_list) + if(compare_z(rbmkzlevel, player_mob.get_virtual_z_level())) + SEND_SOUND(player_mob, 'sound/effects/rbmk/explode.ogg') + to_chat(player_mob, "You hear a horrible metallic explosion.") + SEND_SIGNAL(player_mob, COMSIG_ADD_MOOD_EVENT, "delam", /datum/mood_event/delam) //Might as well use the same moodlet since its essentialy the same thing happening + for(var/nuclear_sludge_landmark in GLOB.landmarks_list) + if(istype(nuclear_sludge_landmark, /obj/modules/power/rbmk/nuclear_sludge_spawner)) + var/obj/modules/power/rbmk/nuclear_sludge_spawner/nuclear_sludge_spawner = nuclear_sludge_landmark + if(compare_z(rbmkzlevel, nuclear_sludge_spawner.get_virtual_z_level())) //Begin the SLUDGING + nuclear_sludge_spawner.fire() + var/obj/modules/power/rbmk/nuclear_sludge_spawner/nuclear_sludge_spawner = new /obj/modules/power/rbmk/nuclear_sludge_spawner/strong(get_turf(src)) + nuclear_sludge_spawner.fire() //This will take out engineering for a decent amount of time as they have to clean up the sludge. + meltdown() //Double kill. + +//Plutonium sludge + +#define PLUTONIUM_SLUDGE_RANGE 500 +#define PLUTONIUM_SLUDGE_RANGE_STRONG 1000 +#define PLUTONIUM_SLUDGE_RANGE_WEAK 300 + +#define PLUTONIUM_SLUDGE_CHANCE 15 + + +/obj/modules/power/rbmk/nuclear_sludge_spawner //Clean way of spawning nuclear gunk after a reactor core meltdown. + name = "nuclear waste spawner" + var/range = PLUTONIUM_SLUDGE_RANGE //tile radius to spawn goop + var/center_sludge = TRUE // Whether or not the center turf should spawn sludge or not. + var/static/list/avoid_objs = typecacheof(list( // List of objs that the waste does not spawn on + /obj/structure/stairs, // Sludge is hidden below stairs + /obj/structure/ladder, // Going down the ladder directly on sludge bad + /obj/effect/decal/cleanable/nuclear_waste, // No stacked sludge + /obj/structure/girder, + /obj/structure/grille, + /obj/structure/window/fulltile, + /obj/structure/window/plasma/fulltile, + /obj/structure/window/plasma/reinforced/fulltile, + /obj/structure/window/plastitanium, + /obj/structure/window/reinforced/fulltile, + /obj/structure/window/reinforced/clockwork/fulltile, + /obj/structure/window/reinforced/tinted/fulltile, + /obj/structure/window, + /obj/structure/window/shuttle, + /obj/machinery/gateway, + /obj/machinery/gravity_generator, + )) +/// Tries to place plutonium sludge on 'floor'. Returns TRUE if the turf has been successfully processed, FALSE otherwise. +/obj/modules/power/rbmk/nuclear_sludge_spawner/proc/place_sludge(turf/open/floor, epicenter = FALSE) + if(!floor) + return FALSE + + if(epicenter) + for(var/obj/effect/decal/cleanable/nuclear_waste/waste in floor) //Replace nuclear waste with the stronger version + qdel(waste) + new /obj/effect/decal/cleanable/nuclear_waste/epicenter (floor) + return TRUE + + if(!prob(PLUTONIUM_SLUDGE_CHANCE)) //Scatter the sludge, don't smear it everywhere + return TRUE + + for(var/obj/object in floor) + if(avoid_objs[object.type]) + return TRUE + + new /obj/effect/decal/cleanable/nuclear_waste (floor) + return TRUE + +/obj/modules/power/rbmk/nuclear_sludge_spawner/strong + range = PLUTONIUM_SLUDGE_RANGE_STRONG + +/obj/modules/power/rbmk/nuclear_sludge_spawner/weak + range = PLUTONIUM_SLUDGE_RANGE_WEAK + center_sludge = FALSE + +/obj/modules/power/rbmk/nuclear_sludge_spawner/proc/fire() + playsound(src, 'sound/effects/gib_step.ogg', 100) + + if(center_sludge) + place_sludge(get_turf(src), TRUE) + + for(var/turf/open/floor in orange(range, get_turf(src))) + place_sludge(floor, FALSE) + + qdel(src) + +#undef PLUTONIUM_SLUDGE_RANGE +#undef PLUTONIUM_SLUDGE_RANGE_STRONG +#undef PLUTONIUM_SLUDGE_RANGE_WEAK +#undef PLUTONIUM_SLUDGE_CHANCE diff --git a/code/modules/uplink/uplink_items.dm b/code/modules/uplink/uplink_items.dm index c21d24dceb12b..12ac288bcd5ba 100644 --- a/code/modules/uplink/uplink_items.dm +++ b/code/modules/uplink/uplink_items.dm @@ -2579,3 +2579,12 @@ GLOBAL_LIST_INIT(illegal_tech_blacklist, typecacheof(list( cost = 3 surplus = 0 disabled = TRUE // #11346 Currently in a broken state, lasso'd mobs will never unregister a target once they have locked onto one, making them unusable. + +/datum/uplink_item/device_tools/tc_rod + name = "Telecrystal Fuel Rod" + desc = "This special fuel rod has eight material slots that can be inserted with telecrystals, \ + once the rod has been fully depleted, you will be able to harvest the extra telecrystals. \ + Please note: This Rod fissiles much faster than it's nanotrasen counterpart, it doesn't take \ + much to overload the reactor with these..." + item = /obj/item/fuel_rod/material/telecrystal + cost = 7 diff --git a/icons/obj/computer.dmi b/icons/obj/computer.dmi index c3e5b099aa89e..a929578cfafe2 100644 Binary files a/icons/obj/computer.dmi and b/icons/obj/computer.dmi differ diff --git a/icons/obj/control_rod.dmi b/icons/obj/control_rod.dmi new file mode 100644 index 0000000000000..f8540c4f70a55 Binary files /dev/null and b/icons/obj/control_rod.dmi differ diff --git a/icons/obj/machines/rbmk.dmi b/icons/obj/machines/rbmk.dmi new file mode 100644 index 0000000000000..fdef8929f5c01 Binary files /dev/null and b/icons/obj/machines/rbmk.dmi differ diff --git a/icons/obj/machines/rbmkparts.dmi b/icons/obj/machines/rbmkparts.dmi new file mode 100644 index 0000000000000..a2992981510c3 Binary files /dev/null and b/icons/obj/machines/rbmkparts.dmi differ diff --git a/icons/obj/tools.dmi b/icons/obj/tools.dmi index 0611c2226bbc2..3b81e03393629 100644 Binary files a/icons/obj/tools.dmi and b/icons/obj/tools.dmi differ diff --git a/sound/effects/rbmk/alarm.ogg b/sound/effects/rbmk/alarm.ogg new file mode 100644 index 0000000000000..834834b2179e8 Binary files /dev/null and b/sound/effects/rbmk/alarm.ogg differ diff --git a/sound/effects/rbmk/ambience.ogg b/sound/effects/rbmk/ambience.ogg new file mode 100644 index 0000000000000..0bb7743366cc3 Binary files /dev/null and b/sound/effects/rbmk/ambience.ogg differ diff --git a/sound/effects/rbmk/explode.ogg b/sound/effects/rbmk/explode.ogg new file mode 100644 index 0000000000000..a16a0634b93d1 Binary files /dev/null and b/sound/effects/rbmk/explode.ogg differ diff --git a/sound/effects/rbmk/reactor_hum.ogg b/sound/effects/rbmk/reactor_hum.ogg new file mode 100644 index 0000000000000..adc1e867ed129 Binary files /dev/null and b/sound/effects/rbmk/reactor_hum.ogg differ diff --git a/sound/effects/rbmk/startup.ogg b/sound/effects/rbmk/startup.ogg new file mode 100644 index 0000000000000..4df6e736c50d5 Binary files /dev/null and b/sound/effects/rbmk/startup.ogg differ diff --git a/sound/effects/rbmk/startup2.ogg b/sound/effects/rbmk/startup2.ogg new file mode 100644 index 0000000000000..b11e80fc82e91 Binary files /dev/null and b/sound/effects/rbmk/startup2.ogg differ diff --git a/sound/effects/rbmk/switch1.ogg b/sound/effects/rbmk/switch1.ogg new file mode 100644 index 0000000000000..668d3b0310565 Binary files /dev/null and b/sound/effects/rbmk/switch1.ogg differ diff --git a/sound/effects/rbmk/switch2.ogg b/sound/effects/rbmk/switch2.ogg new file mode 100644 index 0000000000000..b9cd7d1540212 Binary files /dev/null and b/sound/effects/rbmk/switch2.ogg differ diff --git a/sound/effects/rbmk/switch3.ogg b/sound/effects/rbmk/switch3.ogg new file mode 100644 index 0000000000000..b89cbab4cc822 Binary files /dev/null and b/sound/effects/rbmk/switch3.ogg differ diff --git a/tgui/packages/tgui/interfaces/NtosGhostRbmkStats.js b/tgui/packages/tgui/interfaces/NtosGhostRbmkStats.js new file mode 100644 index 0000000000000..3e9cdf08ec1af --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosGhostRbmkStats.js @@ -0,0 +1,40 @@ +// NSV13 + +import { map, sortBy } from 'common/collections'; +import { flow } from 'common/fp'; +import { toFixed } from 'common/math'; +import { pureComponentHooks } from 'common/react'; +import { Component, Fragment } from 'inferno'; +import { Box, Button, Chart, ColorBox, Flex, Icon, LabeledList, ProgressBar, Section, Table } from '../components'; +import { NtosWindow } from '../layouts'; +import { useBackend, useLocalState } from '../backend'; + +export const NtosGhostRbmkStats = (props, context) => { + const { act, data } = useBackend(context); + return ( + + +
act('swap_reactor')} content="Change Reactor" />}> + Reactor Integrity (%): + + Reactor Power (%): + + Reactor Pressure (KPA): + + {data.kpa} KPA + + Coolant temperature (°C): + + {data.coolantInput} °C + + Outlet temperature (°C): + + {data.coolantOutput} °C + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosRbmkStats.js b/tgui/packages/tgui/interfaces/NtosRbmkStats.js new file mode 100644 index 0000000000000..8d753c942810c --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosRbmkStats.js @@ -0,0 +1,79 @@ +// NSV13 + +import { map, sortBy } from 'common/collections'; +import { flow } from 'common/fp'; +import { toFixed } from 'common/math'; +import { pureComponentHooks } from 'common/react'; +import { Component, Fragment } from 'inferno'; +import { Box, Button, Chart, ColorBox, Flex, Icon, LabeledList, ProgressBar, Section, Table } from '../components'; +import { NtosWindow } from '../layouts'; +import { useBackend, useLocalState } from '../backend'; + +export const NtosRbmkStats = (props, context) => { + const { act, data } = useBackend(context); + const powerData = data.powerData.map((value, i) => [i, value]); + const kpaData = data.kpaData.map((value, i) => [i, value]); + const tempInputData = data.tempInputData.map((value, i) => [i, value]); + const tempOutputdata = data.tempOutputdata.map((value, i) => [i, value]); + return ( + + +
act('swap_reactor')} content="Change Reactor" />}> + Reactor Integrity (%): + + Reactor Power (%): + +
+ Reactor Pressure (KPA): + + {data.kpa} KPA + + Coolant temperature (°C): + + {data.coolantInput} °C + + Outlet temperature (°C): + + {data.coolantOutput} °C + +
+
+ + + + +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/RbmkControlRods.js b/tgui/packages/tgui/interfaces/RbmkControlRods.js new file mode 100644 index 0000000000000..6a082e795c9e9 --- /dev/null +++ b/tgui/packages/tgui/interfaces/RbmkControlRods.js @@ -0,0 +1,58 @@ +// NSV13 + +import { Fragment } from 'inferno'; +import { useBackend, useLocalState } from '../backend'; +import { Section, ProgressBar, Slider } from '../components'; +import { Window } from '../layouts'; + +export const RbmkControlRods = (props, context) => { + const { act, data } = useBackend(context); + const control_rods = data.control_rods; + const k = data.k; + const desiredK = data.desiredK; + return ( + + +
+ Control Rod Insertion: + +
+ Neutrons per generation (K): +
+ + {k} + +
+ Target criticality: +
+ + act('input', { + target: value, + }) + } + /> +
+
+
+ ); +};