From 7144fe758c5f19081e48f0137d55e5f606dda7f6 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 3 Oct 2022 08:21:09 +0200 Subject: [PATCH] [MIRROR] Changed Supermatter Internal Math + UI Additions [MDB IGNORE] (#16603) * Changed Supermatter Internal Math + UI Additions * merge conflicts Co-authored-by: vincentiusvin <54709710+vincentiusvin@users.noreply.github.com> Co-authored-by: tastyfish --- _maps/shuttles/emergency_supermatter.dmm | 2 +- code/__DEFINES/supermatter.dm | 79 +- .../file_system/programs/sm_monitor.dm | 249 +++---- code/modules/power/supermatter/supermatter.dm | 704 ++++++++++++------ .../supermatter_delamination/_sm_delam.dm | 11 +- .../supermatter_delamination/cascade_delam.dm | 8 +- .../supermatter_delamination/common_delams.dm | 2 +- .../delamination_effects.dm | 4 +- .../supermatter/supermatter_extra_effects.dm | 183 +++++ .../power/supermatter/supermatter_gas.dm | 212 +++--- .../supermatter/supermatter_hit_procs.dm | 58 +- .../power/supermatter/supermatter_process.dm | 355 --------- .../supermatter/supermatter_radiation.dm | 64 -- .../power/supermatter/supermatter_variants.dm | 48 ++ icons/obj/engine/supermatter.dmi | Bin 38939 -> 19705 bytes tgstation.dme | 4 +- tgui/packages/tgui/constants.js | 35 + .../tgui/interfaces/NtosSupermatter.tsx | 74 ++ .../interfaces/NtosSupermatterMonitor.tsx | 12 - tgui/packages/tgui/interfaces/Supermatter.tsx | 391 ++++++++++ .../tgui/interfaces/SupermatterMonitor.js | 195 ----- 21 files changed, 1542 insertions(+), 1148 deletions(-) create mode 100644 code/modules/power/supermatter/supermatter_extra_effects.dm delete mode 100644 code/modules/power/supermatter/supermatter_process.dm delete mode 100644 code/modules/power/supermatter/supermatter_radiation.dm create mode 100644 code/modules/power/supermatter/supermatter_variants.dm create mode 100644 tgui/packages/tgui/interfaces/NtosSupermatter.tsx delete mode 100644 tgui/packages/tgui/interfaces/NtosSupermatterMonitor.tsx create mode 100644 tgui/packages/tgui/interfaces/Supermatter.tsx delete mode 100644 tgui/packages/tgui/interfaces/SupermatterMonitor.js diff --git a/_maps/shuttles/emergency_supermatter.dmm b/_maps/shuttles/emergency_supermatter.dmm index fb420049b6f..05e4d018cff 100644 --- a/_maps/shuttles/emergency_supermatter.dmm +++ b/_maps/shuttles/emergency_supermatter.dmm @@ -100,7 +100,7 @@ /turf/open/floor/plating, /area/shuttle/escape) "aB" = ( -/obj/machinery/power/supermatter_crystal/shard/hugbox/fakecrystal, +/obj/machinery/power/supermatter_crystal/hugbox, /turf/open/floor/plating, /area/shuttle/escape) "aD" = ( diff --git a/code/__DEFINES/supermatter.dm b/code/__DEFINES/supermatter.dm index 6b9d52d665e..c5df44c4ead 100644 --- a/code/__DEFINES/supermatter.dm +++ b/code/__DEFINES/supermatter.dm @@ -14,24 +14,19 @@ /// How much the amount of CO2 consumed per tick scales with gasmix power ratio. Higher values means gasmix has a greater effect on the CO2 consumed. #define CO2_GASMIX_SCALING (0.1) -#define MOLE_PENALTY_THRESHOLD 1800 //Above this value we can get lord singulo and independent mol damage, below it we can heal damage -#define MOLE_HEAT_PENALTY 350 //Heat damage scales around this. Too hot setups with this amount of moles do regular damage, anything above and below is scaled -//Along with damage_penalty_point, makes flux anomalies. -/// The cutoff for the minimum amount of power required to trigger the crystal invasion delamination event. -#define EVENT_POWER_PENALTY_THRESHOLD 4500 +#define MOLE_PENALTY_THRESHOLD 1800 //Above this value we can get lord singulo and independent mol damage, below it we can heal damage #define POWER_PENALTY_THRESHOLD 5000 //The cutoff on power properly doing damage, pulling shit around, and delamming into a tesla. Low chance of pyro anomalies, +2 bolts of electricity #define SEVERE_POWER_PENALTY_THRESHOLD 7000 //+1 bolt of electricity, allows for gravitational anomalies, and higher chances of pyro anomalies #define CRITICAL_POWER_PENALTY_THRESHOLD 9000 //+1 bolt of electricity. #define HEAT_PENALTY_THRESHOLD 40 //Higher == Crystal safe operational temperature is higher. -#define DAMAGE_HARDCAP 0.002 -#define DAMAGE_INCREASE_MULTIPLIER 0.25 +#define DAMAGE_HARDCAP 1.8 #define THERMAL_RELEASE_MODIFIER 4 //Higher == less heat released during reaction, not to be confused with the above values #define PLASMA_RELEASE_MODIFIER 650 //Higher == less plasma released by reaction #define OXYGEN_RELEASE_MODIFIER 340 //Higher == less oxygen released at high temperature/power -#define REACTION_POWER_MODIFIER 0.65 //Higher == more overall power +#define REACTION_POWER_MODIFIER 0.65 //Higher == more waste gas #define MATTER_POWER_CONVERSION 10 //Crystal converts 1/this value of stored matter into energy. @@ -43,8 +38,7 @@ #define SUPERMATTER_WARNING_DELAY 60 SECONDS -#define HALLUCINATION_RANGE(P) (min(7, round(P ** 0.25))) - +#define SM_HALLUCINATION_RANGE(P) (min(7, round(P ** 0.25))) #define GRAVITATIONAL_ANOMALY "gravitational_anomaly" #define FLUX_ANOMALY "flux_anomaly" @@ -63,16 +57,12 @@ #define SLIGHTLY_CHARGED_ZAP_ICON_STATE "sm_arc_supercharged" #define OVER_9000_ZAP_ICON_STATE "sm_arc_dbz_referance" //Witty I know -#define MAX_SPACE_EXPOSURE_DAMAGE 10 - #define SUPERMATTER_CASCADE_PERCENT 80 /// The divisor scaling value for cubic power loss. #define POWERLOSS_CUBIC_DIVISOR 500 /// The rate at which the linear power loss function scales with power. #define POWERLOSS_LINEAR_RATE 0.83 -/// How much a psychologist can reduce power loss. -#define PSYCHOLOGIST_POWERLOSS_REDUCTION 0.2 /// Means it's not forced, sm decides itself by checking the [/datum/sm_delam/proc/can_select] #define SM_DELAM_PRIO_NONE 0 @@ -102,3 +92,64 @@ #define SUPERMATTER_EMERGENCY 5 /// Currently counting down to delamination. True [/obj/machinery/power/supermatter_crystal/var/final_countdown] #define SUPERMATTER_DELAMINATING 6 + +// SUPERMATTER FACTORS DEFINES +// This might feel obvious, but mentioning it doesnt hurt anyone: +// While we separate these factors into neat little boxes, order still matters in the code. +// There are a few factors that depends on other factors. So pay close attention on which values are interlinked. + +/// How much energy we get from external factors that are applied immediately. +#define SM_POWER_EXTERNAL_IMMEDIATE "External Power Gain" +/// How much energy we get from external factors that are applied over time. +#define SM_POWER_EXTERNAL_TRICKLE "External Power Trickle" +/// How much energy is gained from the temperature. Enabled by gas. +#define SM_POWER_HEAT "Gas Heat Power Gain" +/// How much energy the SM loses. Happens over time. +/// Order matters here. We depend on current power + power gained from the factors above for the loss calc. +#define SM_POWER_POWERLOSS "Internal Power Decay" +/// How much of the energy the SM loses is recouped. From gas factors here. +/// Order matters here. We depend on the powerloss amount. +#define SM_POWER_POWERLOSS_GAS "Gas Power Decay Negation" +/// How much of the energy the SM loses is recouped. From the psychologist this time. +/// Order matters here. We depend on the powerloss amount. +#define SM_POWER_POWERLOSS_SOOTHED "Psychologist Power Decay Negation" + +// These four internal damage factors, heat, power, moles, and heal heat dont depend on each other, but they are interlinked. +// They are going to be scaled to have a maximum damage hardcap of 1.8 per tick. +/// How many damage we take from heat. +#define SM_DAMAGE_HEAT "Heating Damage" +/// How many damage we take from too much internal energy. +#define SM_DAMAGE_POWER "Charge Damage" +/// How many damage we take from too much moles around us. +#define SM_DAMAGE_MOLES "Molar Damage" +/// How many we healed. Happens when temp is low. +#define SM_DAMAGE_HEAL_HEAT "Low Temperature Healing" +/// How many damage do we take from external factors. +/// This one is discrete. +#define SM_DAMAGE_EXTERNAL "External Damage" +/// How many damage do we take from space exposure. Here to prevent people from spacing SM chambers. +/// Also discrete. +#define SM_DAMAGE_SPACED "Space Exposure Damage" + +/// How much waste multiplier we get just from existing. +#define SM_WASTE_BASE "Base Waste Multiplier" +/// How much waste multiplier we get because of the gases around us. +#define SM_WASTE_GAS "Gas Waste Multiplier" +/// How much waste multiplier we (don't) get because there is a psychologist. +#define SM_WASTE_SOOTHED "Psychologist Waste Multiplier" + +/// How many kelvins we get before taking damage, Given by god. +#define SM_TEMP_LIMIT_BASE "Base Heat Resistance" +/// How many extra kelvins we get before taking damage, this time from gases. +/// Order matters, depends on base resistance. +#define SM_TEMP_LIMIT_GAS "Gas Heat Resistance" +/// How many extra kelvins we get before taking damage, this time from psychologist. +#define SM_TEMP_LIMIT_SOOTHED "Psychologist Heat Resistance" +/// How many extra kelvins we get before taking damage because our moles are low. I know, this is fucking hilarious. +/// Order matters, depends on base resistance. +#define SM_TEMP_LIMIT_LOW_MOLES "Low Moles Heat Resistance" + +/// How much we are multiplying our zap energy. +#define SM_ZAP_BASE "Base Zap Multiplier" +/// How much we are multiplying our zap energy because of gas factors. +#define SM_ZAP_GAS "Gas Zap Multiplier" diff --git a/code/modules/modular_computers/file_system/programs/sm_monitor.dm b/code/modules/modular_computers/file_system/programs/sm_monitor.dm index 20d0200128c..3f74ae0273c 100644 --- a/code/modules/modular_computers/file_system/programs/sm_monitor.dm +++ b/code/modules/modular_computers/file_system/programs/sm_monitor.dm @@ -8,18 +8,105 @@ requires_ntnet = TRUE transfer_access = list(ACCESS_CONSTRUCTION) size = 5 - tgui_id = "NtosSupermatterMonitor" + tgui_id = "NtosSupermatter" program_icon = "radiation" alert_able = TRUE var/last_status = SUPERMATTER_INACTIVE - var/list/supermatters - var/obj/machinery/power/supermatter_crystal/active // Currently selected supermatter crystal. + /// List of supermatters that we are going to send the data of. + var/list/obj/machinery/power/supermatter_crystal/supermatters = list() + /// The supermatter which will send a notification to us if it's delamming. + var/obj/machinery/power/supermatter_crystal/focused_supermatter -/datum/computer_file/program/supermatter_monitor/Destroy() - clear_signals() - active = null +/datum/computer_file/program/supermatter_monitor/on_start(mob/living/user) + . = ..() + refresh() + +/// Apparently destroy calls this [/datum/computer_file/Destroy]. Here just to clean our references. +/datum/computer_file/program/supermatter_monitor/kill_program(forced = FALSE) + for(var/supermatter in supermatters) + clear_supermatter(supermatter) return ..() +/// Refreshes list of active supermatter crystals +/datum/computer_file/program/supermatter_monitor/proc/refresh() + for(var/supermatter in supermatters) + clear_supermatter(supermatter) + var/turf/user_turf = get_turf(ui_host()) + if(!user_turf) + return + for(var/obj/machinery/power/supermatter_crystal/sm in GLOB.machines) + //Exclude Syndicate owned, Delaminating, not within coverage, not on a tile. + if (!sm.include_in_cims || !isturf(sm.loc) || !(is_station_level(sm.z) || is_mining_level(sm.z) || sm.z == user_turf.z)) + continue + supermatters += sm + RegisterSignal(sm, COMSIG_PARENT_QDELETING, .proc/clear_supermatter) + +/datum/computer_file/program/supermatter_monitor/ui_static_data(mob/user) + var/list/data = list() + data["gas_metadata"] = sm_gas_data() + return data + +/datum/computer_file/program/supermatter_monitor/ui_data(mob/user) + var/list/data = get_header_data() + data["sm_data"] = list() + for (var/obj/machinery/power/supermatter_crystal/sm as anything in supermatters) + data["sm_data"] += list(sm.sm_ui_data()) + data["focus_uid"] = focused_supermatter?.uid + return data + +/datum/computer_file/program/supermatter_monitor/ui_act(action, params) + . = ..() + if(.) + return + + switch(action) + if("PRG_refresh") + refresh() + return TRUE + if("PRG_focus") + for (var/obj/machinery/power/supermatter_crystal/sm in supermatters) + if(sm.uid == params["focus_uid"]) + if(focused_supermatter == sm) + unfocus_supermatter(sm) + else + focus_supermatter(sm) + return TRUE + +/// Sends an SM delam alert to the computer if our focused supermatter is delaminating. +/// [var/obj/machinery/power/supermatter_crystal/focused_supermatter]. +/datum/computer_file/program/supermatter_monitor/proc/send_alert() + SIGNAL_HANDLER + if(!computer.get_ntnet_status()) + return + computer.alert_call(src, "Crystal delamination in progress!") + alert_pending = TRUE + +/datum/computer_file/program/supermatter_monitor/proc/clear_supermatter(obj/machinery/power/supermatter_crystal/sm) + SIGNAL_HANDLER + supermatters -= sm + if(focused_supermatter == sm) + unfocus_supermatter() + UnregisterSignal(sm, COMSIG_PARENT_QDELETING) + +/datum/computer_file/program/supermatter_monitor/proc/focus_supermatter(obj/machinery/power/supermatter_crystal/sm) + if(sm == focused_supermatter) + return + if(focused_supermatter) + unfocus_supermatter() + RegisterSignal(sm, COMSIG_SUPERMATTER_DELAM_ALARM, .proc/send_alert) + focused_supermatter = sm + +/datum/computer_file/program/supermatter_monitor/proc/unfocus_supermatter() + if(!focused_supermatter) + return + UnregisterSignal(focused_supermatter, COMSIG_SUPERMATTER_DELAM_ALARM) + focused_supermatter = null + +/datum/computer_file/program/supermatter_monitor/proc/get_status() + . = SUPERMATTER_INACTIVE + for(var/obj/machinery/power/supermatter_crystal/S in supermatters) + . = max(., S.get_status()) + /datum/computer_file/program/supermatter_monitor/process_tick(delta_time) ..() var/new_status = get_status() @@ -29,153 +116,3 @@ program_icon_state = "smmon_[last_status]" if(istype(computer)) computer.update_appearance() - -/datum/computer_file/program/supermatter_monitor/on_start(mob/living/user) - . = ..(user) - if(!(active in GLOB.machines)) - active = null - refresh() - -/datum/computer_file/program/supermatter_monitor/kill_program(forced = FALSE) - for(var/supermatter in supermatters) - clear_supermatter(supermatter) - supermatters = null - ..() - -// Refreshes list of active supermatter crystals -/datum/computer_file/program/supermatter_monitor/proc/refresh() - for(var/supermatter in supermatters) - clear_supermatter(supermatter) - supermatters = list() - var/turf/user_turf = get_turf(ui_host()) - if(!user_turf) - return - for(var/obj/machinery/power/supermatter_crystal/crystal in GLOB.machines) - //Exclude Syndicate owned, Delaminating, not within coverage, not on a tile. - if (!crystal.include_in_cims || !isturf(crystal.loc) || !(is_station_level(crystal.z) || is_mining_level(crystal.z) || crystal.z == user_turf.z)) - continue - supermatters.Add(crystal) - RegisterSignal(crystal, COMSIG_PARENT_QDELETING, .proc/react_to_del) - -/datum/computer_file/program/supermatter_monitor/proc/get_status() - . = SUPERMATTER_INACTIVE - for(var/obj/machinery/power/supermatter_crystal/S in supermatters) - . = max(., S.get_status()) - -/** - * Sets up the signal listener for Supermatter delaminations. - * - * Unregisters any old listners for SM delams, and then registers one for the SM refered - * to in the `active` variable. This proc is also used with no active SM to simply clear - * the signal and exit. - */ -/datum/computer_file/program/supermatter_monitor/proc/set_signals() - if(active) - RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM, .proc/send_alert, override = TRUE) - RegisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM, .proc/send_start_alert, override = TRUE) - -/** - * Removes the signal listener for Supermatter delaminations from the selected supermatter. - * - * Pretty much does what it says. - */ -/datum/computer_file/program/supermatter_monitor/proc/clear_signals() - if(active) - UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_ALARM) - UnregisterSignal(active, COMSIG_SUPERMATTER_DELAM_START_ALARM) - -/** - * Sends an SM delam alert to the computer. - * - * Triggered by a signal from the selected supermatter, this proc sends a notification - * to the computer if the program is either closed or minimized. We do not send these - * notifications to the comptuer if we're the active program, because engineers fixing - * the supermatter probably don't need constant beeping to distract them. - */ -/datum/computer_file/program/supermatter_monitor/proc/send_alert() - SIGNAL_HANDLER - if(!computer.get_ntnet_status()) - return - if(computer.active_program != src) - computer.alert_call(src, "Crystal delamination in progress!") - alert_pending = TRUE - -/** - * Sends an SM delam start alert to the computer. - * - * Triggered by a signal from the selected supermatter at the start of a delamination, - * this proc sends a notification to the computer if this program is the active one. - * We do this so that people carrying a tablet with NT CIMS open but with the NTOS window - * closed will still get one audio alert. This is not sent to computers with the program - * minimized or closed to avoid double-notifications. - */ -/datum/computer_file/program/supermatter_monitor/proc/send_start_alert() - SIGNAL_HANDLER - if(!computer.get_ntnet_status()) - return - if(computer.active_program == src) - computer.alert_call(src, "Crystal delamination in progress!") - -/datum/computer_file/program/supermatter_monitor/ui_data(mob/user) - var/list/data = get_header_data() - - if(istype(active)) - var/turf/T = get_turf(active) - if(!T) - active = null - refresh() - return - var/datum/gas_mixture/air = T.return_air() - if(!air) - active = null - return - - data += active.ui_data() - data["singlecrystal"] = FALSE - - else - var/list/SMS = list() - for(var/obj/machinery/power/supermatter_crystal/S in supermatters) - var/area/A = get_area(S) - if(A) - SMS.Add(list(list( - "area_name" = A.name, - "integrity" = S.get_integrity_percent(), - "uid" = S.uid - ))) - - data["active"] = FALSE - data["supermatters"] = SMS - - return data - -/datum/computer_file/program/supermatter_monitor/ui_act(action, params) - . = ..() - if(.) - return - - switch(action) - if("PRG_clear") - clear_signals() - active = null - return TRUE - if("PRG_refresh") - refresh() - return TRUE - if("PRG_set") - var/newuid = text2num(params["target"]) - for(var/obj/machinery/power/supermatter_crystal/S in supermatters) - if(S.uid == newuid) - active = S - set_signals() - return TRUE - -/datum/computer_file/program/supermatter_monitor/proc/react_to_del(datum/source) - SIGNAL_HANDLER - clear_supermatter(source) - -/datum/computer_file/program/supermatter_monitor/proc/clear_supermatter(matter) - supermatters -= matter - if(matter == active) - active = null - UnregisterSignal(matter, COMSIG_PARENT_QDELETING) diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index 7b33fee08e4..84aa3f77760 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -18,7 +18,6 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) name = "supermatter crystal" desc = "A strangely translucent and iridescent crystal." icon = 'icons/obj/engine/supermatter.dmi' - icon_state = "darkmatter" density = TRUE anchored = TRUE layer = MOB_LAYER @@ -26,7 +25,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) light_range = 4 resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF critical_machine = TRUE - base_icon_state = "darkmatter" + base_icon_state = "sm" + icon_state = "sm" ///The id of our supermatter var/uid = 1 @@ -35,14 +35,33 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) ///Tracks the bolt color we are using var/zap_icon = DEFAULT_ZAP_ICON_STATE - ///Are we exploding? - var/final_countdown = FALSE + ///The portion of the gasmix we're on that we should remove + var/absorption_ratio = 0.15 + /// The gasmix we just recently absorbed. Tile's air multiplied by absorption_ratio + var/datum/gas_mixture/absorbed_gasmix - ///The amount of damage we have currently + ///Refered to as EER on the monitor. This value effects gas output, damage, and power generation. + var/internal_energy = 0 + var/list/internal_energy_factors + + ///The amount of damage we have currently. var/damage = 0 /// The damage we had before this cycle. - /// Used to limit the damage we can take each cycle, and to check if we are currently taking damage or healing. + /// Used to check if we are currently taking damage or healing. var/damage_archived = 0 + var/list/damage_factors + + /// How much extra power does the main zap generate. + var/zap_multiplier = 1 + var/list/zap_factors + + /// The temperature at which we start taking damage + var/temp_limit = T0C + HEAT_PENALTY_THRESHOLD + var/list/temp_limit_factors + + /// Multiplies our waste gas amount and temperature. + var/waste_multiplier = 0 + var/list/waste_multiplier_factors ///The point at which we consider the supermatter to be [SUPERMATTER_STATUS_WARNING] var/warning_point = 50 @@ -55,35 +74,38 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) var/emergency_channel = null // Need null to actually broadcast, lol. ///The point at which we delam [SUPERMATTER_STATUS_DELAMINATING] var/explosion_point = 900 - + ///Are we exploding? + var/final_countdown = FALSE ///A scaling value that affects the severity of explosions. var/explosion_power = 35 ///Time in 1/10th of seconds since the last sent warning var/lastwarning = 0 - ///Refered to as eer on the moniter. This value effects gas output, heat, damage, and radiation. - var/power = 0 - ///The list of gases mapped against their current comp. We use this to calculate different values the supermatter uses, like power or heat resistance. Ranges from 0 to 1 + + /// The list of gases mapped against their current comp. + /// We use this to calculate different values the supermatter uses, like power or heat resistance. + /// Ranges from 0 to 1 var/list/gas_percentage - ///The last air sample's total molar count, will always be above or equal to 0 - var/combined_gas = 0 - ///Total mole count of the environment we are in - var/environment_total_moles = 0 - ///Affects the power gain the sm experiances from heat - var/gasmix_power_ratio = 0 - ///Affects the amount of o2 and plasma the sm outputs, along with the heat it makes. - var/dynamic_heat_modifier = 1 - ///Affects the amount of damage and minimum point at which the sm takes heat damage - var/dynamic_heat_resistance = 1 - ///Uses powerloss_dynamic_scaling and combined_gas to lessen the effects of our powerloss functions - var/powerloss_inhibitor = 1 - ///Based on co2 percentage, slowly moves between 0 and 1. We use it to calc the powerloss_inhibitor - var/powerloss_dynamic_scaling= 0 - ///Affects the amount of radiation the sm makes. We multiply this with power to find the zap power. - var/power_transmission_bonus = 0 - ///Used to increase or lessen the amount of damage the sm takes from heat based on molar counts. - var/mole_heat_penalty = 0 - ///Takes the energy throwing things into the sm generates and slowly turns it into actual power - var/matter_power = 0 + + /// Affects the heat our SM makes. + var/gas_heat_modifier = 0 + /// Affects the minimum point at which the SM takes heat damage + var/gas_heat_resistance = 0 + /// How much power decay is negated. Complete power decay negation at 1. + var/gas_powerloss_inhibition = 0 + /// Affects the amount of power the main SM zap makes. + var/gas_power_transmission = 0 + /// Affects the power gain the SM experiances from heat. + var/gas_heat_power_generation = 0 + + /// External power that are added over time instead of immediately. + var/external_power_trickle = 0 + /// External power that are added to the sm on next [/obj/machinery/power/supermatter_crystal/process_atmos] call. + var/external_power_immediate = 0 + + /// External damage that are added to the sm on next [/obj/machinery/power/supermatter_crystal/process_atmos] call. + /// SM will not take damage if it's health is lower than emergency point. + var/external_damage_immediate = 0 + ///The cutoff for a bolt jumping, grows with heat, lowers with higher mol count, var/zap_cutoff = 1500 ///How much the bullets damage should be multiplied by when it is added to the internal variables @@ -91,27 +113,6 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) ///How much hallucination should we produce per unit of power? var/hallucination_power = 0.1 - ///Pressure bonus constants - ///If the SM is operating in sufficiently low pressure, increase power output. - ///This needs both a small amount of gas and a strong cooling system to keep temperature low in a low heat capacity environment. - - ///These constants are used to derive the values in the pressure bonus equation from human-meaningful values - ///If you're varediting these, call update_constants() to update the derived values - - ///What is the maximum multiplier reachable from having low pressure? - var/pressure_bonus_max_multiplier = 0.5 - ///At what environmental pressure, in kPa, should we start giving a pressure bonus? - var/pressure_bonus_max_pressure = 100 - ///How steeply angled is the pressure bonus curve? Higher values means more of the bonus is available at higher pressures. - ///Note that very low values can keep the bonus very close to 1 until it's nearly a vaccuum. Higher values can introduce diminishing returns on lower pressure. - var/pressure_bonus_curve_angle = 1.8 - - ///These values are calculated from the above in update_constants() and immediately overwritten - ///The default values will always result in a no-op 1x modifier, in case something breaks. - var/pressure_bonus_derived_constant = 1 - var/pressure_bonus_derived_steepness = 0 - - ///Our internal radio var/obj/item/radio/radio ///The key our internal radio uses @@ -132,22 +133,21 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) ///cooldown tracker for accent sounds var/last_accent_sound = 0 - ///Var that increases from 0 to 1 when a psycologist is nearby, and decreases in the same way - var/psyCoeff = 0 - ///Should we check the psy overlay? - var/psy_overlay = FALSE - ///A pinkish overlay used to denote the presance of a psycologist. We fade in and out of this depending on the amount of time they've spent near the crystal - var/obj/overlay/psy/psyOverlay = /obj/overlay/psy + ///Var that increases from 0 to 1 when a psychologist is nearby, and decreases in the same way + var/psy_coeff = 0 + + /// Disables all methods of taking damage. + var/disable_damage = FALSE + /// Disables the calculation of gas effects and production of waste. + /// SM still "breathes" though, still takes gas and spits it out. Nothing is done on them though. + /// Cleaner code this way. Get rid of if it's too wasteful. + var/disable_gas = FALSE + /// Disables power changes. + var/disable_power_change = FALSE + /// Disables the SM's proccessing totally. + /// Make sure absorbed_gasmix and gas_percentage isnt null if this is on. + var/disable_process = FALSE - //For making hugbox supermatters - ///Disables all methods of taking damage - var/takes_damage = TRUE - ///Disables the production of gas, and pretty much any handling of it we do. - var/produces_gas = TRUE - ///Disables power changes - var/power_changes = TRUE - ///Disables the sm's proccessing totally. - var/processes = TRUE ///Stores the time of when the last zap occurred var/last_power_zap = 0 ///Do we show this crystal in the CIMS modular program @@ -162,11 +162,6 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) ///The offset of the linear powerloss function set so the transition is differentiable. var/powerloss_linear_offset = 0 - ///The portion of the gasmix we're on that we should remove - var/absorption_ratio = 0.15 - /// The gasmix we just recently absorbed. Tile's air multiplied by absorption_ratio - var/datum/gas_mixture/absorbed_gasmix - /// How we are delaminating. var/datum/sm_delam/delamination_strategy /// Whether the sm is forced in a specific delamination_strategy or not. All truthy values means it's forced. @@ -175,6 +170,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) /obj/machinery/power/supermatter_crystal/Initialize(mapload) . = ..() + gas_percentage = list() + absorbed_gasmix = new() uid = gl_uid++ set_delam(SM_DELAM_PRIO_NONE, /datum/sm_delam/explosive) SSair.start_processing_machine(src) @@ -200,16 +197,13 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) AddComponent(/datum/component/supermatter_crystal, CALLBACK(src, .proc/wrench_act_callback), CALLBACK(src, .proc/consume_callback)) soundloop = new(src, TRUE) - if(ispath(psyOverlay)) - psyOverlay = new psyOverlay() - else - stack_trace("Supermatter created with non-path psyOverlay variable. This can break things, please fix.") - psyOverlay = new() if (!moveable) move_resist = MOVE_FORCE_OVERPOWERING // Avoid being moved by statues or other memes - update_constants() + // Damn math nerds + powerloss_linear_threshold = sqrt(POWERLOSS_LINEAR_RATE / 3 * POWERLOSS_CUBIC_DIVISOR ** 3) + powerloss_linear_offset = -1 * powerloss_linear_threshold * POWERLOSS_LINEAR_RATE + (powerloss_linear_threshold / POWERLOSS_CUBIC_DIVISOR) ** 3 /obj/machinery/power/supermatter_crystal/Destroy() if(warp) @@ -217,13 +211,12 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) QDEL_NULL(warp) investigate_log("has been destroyed.", INVESTIGATE_ENGINE) SSair.stop_processing_machine(src) + absorbed_gasmix = null QDEL_NULL(radio) QDEL_NULL(countdown) if(is_main_engine && GLOB.main_supermatter_engine == src) GLOB.main_supermatter_engine = null QDEL_NULL(soundloop) - if(psyOverlay) - QDEL_NULL(psyOverlay) return ..() /obj/machinery/power/supermatter_crystal/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents) @@ -233,20 +226,94 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) if(warp) SET_PLANE_EXPLICIT(warp, PLANE_TO_TRUE(warp.plane), src) -/obj/machinery/power/supermatter_crystal/proc/update_constants() - pressure_bonus_derived_steepness = (1 - 1 / pressure_bonus_max_multiplier) / (pressure_bonus_max_pressure ** pressure_bonus_curve_angle) - pressure_bonus_derived_constant = 1 / pressure_bonus_max_multiplier - pressure_bonus_derived_steepness - powerloss_linear_threshold = sqrt(POWERLOSS_LINEAR_RATE / 3 * POWERLOSS_CUBIC_DIVISOR ** 3) - powerloss_linear_offset = -1 * powerloss_linear_threshold * POWERLOSS_LINEAR_RATE + (powerloss_linear_threshold / POWERLOSS_CUBIC_DIVISOR) ** 3 - /obj/machinery/power/supermatter_crystal/examine(mob/user) . = ..() var/immune = HAS_TRAIT(user, TRAIT_MADNESS_IMMUNE) || (user.mind && HAS_TRAIT(user.mind, TRAIT_MADNESS_IMMUNE)) - if(isliving(user) && !immune && (get_dist(user, src) < HALLUCINATION_RANGE(power))) + if(isliving(user) && !immune && (get_dist(user, src) < SM_HALLUCINATION_RANGE(internal_energy))) . += span_danger("You get headaches just from looking at it.") . += delamination_strategy.examine(src) return . +/obj/machinery/power/supermatter_crystal/process_atmos() + // PART 1: PRELIMINARIES + if(disable_process) + return + + var/turf/local_turf = loc + if(!istype(local_turf))//We are in a crate or somewhere that isn't turf, if we return to turf resume processing but for now. + return //Yeah just stop. + if(isclosedturf(local_turf)) + var/turf/did_it_melt = local_turf.Melt() + if(!isclosedturf(did_it_melt)) //In case some joker finds way to place these on indestructible walls + visible_message(span_warning("[src] melts through [local_turf]!")) + return + + // PART 2: GAS PROCESSING + var/datum/gas_mixture/env = local_turf.return_air() + absorbed_gasmix = env?.remove_ratio(absorption_ratio) || new() + absorbed_gasmix.volume = (env?.volume || CELL_VOLUME) * absorption_ratio // To match the pressure. + calculate_gases() + // Extra effects should always fire after the compositions are all finished + // Some extra effects like [/datum/sm_gas/carbon_dioxide/extra_effects] + // needs more than one gas and rely on a fully parsed gas_percentage. + for (var/gas_path in absorbed_gasmix.gases) + var/datum/sm_gas/sm_gas = GLOB.sm_gas_behavior[gas_path] + sm_gas.extra_effects(src) + + // PART 3: POWER PROCESSING + internal_energy_factors = calculate_internal_energy() + zap_factors = calculate_zap_multiplier() + if(internal_energy && (last_power_zap + 4 SECONDS - (internal_energy * 0.001)) < world.time) + playsound(src, 'sound/weapons/emitter2.ogg', 70, TRUE) + hue_angle_shift = clamp(903 * log(10, (internal_energy + 8000)) - 3590, -50, 240) + var/zap_color = color_matrix_rotate_hue(hue_angle_shift) + supermatter_zap( + zapstart = src, + range = 3, + zap_str = 5 * internal_energy * zap_multiplier, + zap_flags = ZAP_SUPERMATTER_FLAGS, + zap_cutoff = 300, + power_level = internal_energy, + color = zap_color, + ) + last_power_zap = world.time + + // PART 4: DAMAGE PROCESSING + temp_limit_factors = calculate_temp_limit() + damage_factors = calculate_damage() + if(damage == 0) // Clear any in game forced delams if on full health. + set_delam(SM_DELAM_PRIO_IN_GAME, SM_DELAM_STRATEGY_PURGE) + else + set_delam(SM_DELAM_PRIO_NONE, SM_DELAM_STRATEGY_PURGE) // This one cant clear any forced delams. + delamination_strategy.delam_progress(src) + if(damage > explosion_point && !final_countdown) + count_down() + + // PART 5: WASTE GAS PROCESSING + waste_multiplier_factors = calculate_waste_multiplier() + var/device_energy = internal_energy * REACTION_POWER_MODIFIER + + /// Do waste on another gasmix so we can keep a copy of the gasmix we use for processing. + var/datum/gas_mixture/merged_gasmix = absorbed_gasmix.copy() + merged_gasmix.temperature += device_energy * waste_multiplier / THERMAL_RELEASE_MODIFIER + merged_gasmix.temperature = clamp(merged_gasmix.temperature, TCMB, 2500 * waste_multiplier) + merged_gasmix.assert_gases(/datum/gas/plasma, /datum/gas/oxygen) + merged_gasmix.gases[/datum/gas/plasma][MOLES] += max(device_energy * waste_multiplier / PLASMA_RELEASE_MODIFIER, 0) + merged_gasmix.gases[/datum/gas/oxygen][MOLES] += max(((device_energy + merged_gasmix.temperature * waste_multiplier) - T0C) / OXYGEN_RELEASE_MODIFIER, 0) + merged_gasmix.garbage_collect() + env.merge(merged_gasmix) + air_update_turf(FALSE, FALSE) + + // PART 6: EXTRA BEHAVIOUR + emit_radiation() + processing_sound() + handle_high_power() + psychological_examination() + if(prob(15)) + supermatter_pull(loc, min(internal_energy/850, 3))//850, 1700, 2550 + update_appearance() + return TRUE + // SupermatterMonitor UI for ghosts only. Inherited attack_ghost will call this. /obj/machinery/power/supermatter_crystal/ui_interact(mob/user, datum/tgui/ui) if(!isobserver(user)) @@ -254,55 +321,87 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) . = ..() ui = SStgui.try_update_ui(user, src, ui) if (!ui) - ui = new(user, src, "SupermatterMonitor") + ui = new(user, src, "Supermatter") ui.open() +/obj/machinery/power/supermatter_crystal/ui_static_data(mob/user) + var/list/data = list() + data["gas_metadata"] = sm_gas_data() + return data + +/// Returns data that are exclusively about this sm. +/obj/machinery/power/supermatter_crystal/proc/sm_ui_data() + var/list/data = list() + data["uid"] = uid + data["area_name"] = get_area_name(src) + + data["integrity"] = get_integrity_percent() + data["integrity_factors"] = list() + for (var/factor in damage_factors) + var/amount = round(damage_factors[factor], 0.01) + if(!amount) + continue + data["integrity_factors"] += list(list( + "name" = factor, + "amount" = amount * -1 + )) + data["internal_energy"] = internal_energy + data["internal_energy_factors"] = list() + for (var/factor in internal_energy_factors) + var/amount = round(internal_energy_factors[factor], 0.01) + if(!amount) + continue + data["internal_energy_factors"] += list(list( + "name" = factor, + "amount" = amount + )) + data["temp_limit"] = temp_limit + data["temp_limit_factors"] = list() + for (var/factor in temp_limit_factors) + var/amount = round(temp_limit_factors[factor], 0.01) + if(!amount) + continue + data["temp_limit_factors"] += list(list( + "name" = factor, + "amount" = amount + )) + data["waste_multiplier"] = waste_multiplier + data["waste_multiplier_factors"] = list() + for (var/factor in waste_multiplier_factors) + var/amount = round(waste_multiplier_factors[factor], 0.01) + if(!amount) + continue + data["waste_multiplier_factors"] += list(list( + "name" = factor, + "amount" = amount + )) + data["zap_multiplier"] = zap_multiplier + data["zap_multiplier_factors"] = list() + for (var/factor in zap_factors) + var/amount = round(zap_factors[factor], 0.01) + if(!amount) + continue + data["zap_multiplier_factors"] += list(list( + "name" = factor, + "amount" = amount + )) + data["absorbed_ratio"] = absorption_ratio + var/list/formatted_gas_percentage = list() + for (var/datum/gas/gas_path as anything in subtypesof(/datum/gas)) + formatted_gas_percentage[gas_path] = gas_percentage?[gas_path] || 0 + data["gas_composition"] = formatted_gas_percentage + data["gas_temperature"] = absorbed_gasmix.temperature + data["gas_total_moles"] = absorbed_gasmix.total_moles() + return data + /obj/machinery/power/supermatter_crystal/ui_data(mob/user) var/list/data = list() - - var/turf/local_turf = get_turf(src) - - var/datum/gas_mixture/air = local_turf.return_air() - - // singlecrystal set to true eliminates the back sign on the gases breakdown. - data["singlecrystal"] = TRUE - data["active"] = TRUE - data["SM_integrity"] = get_integrity_percent() - data["SM_power"] = power - data["SM_ambienttemp"] = air.temperature - data["SM_ambientpressure"] = air.return_pressure() - data["SM_bad_moles_amount"] = MOLE_PENALTY_THRESHOLD / absorption_ratio - data["SM_moles"] = 0 - data["SM_uid"] = uid - var/area/active_supermatter_area = get_area(src) - data["SM_area_name"] = active_supermatter_area.name - - var/list/gasdata = list() - - if(air.total_moles()) - data["SM_moles"] = air.total_moles() - for(var/gasid in air.gases) - gasdata.Add(list(list( - "name"= air.gases[gasid][GAS_META][META_GAS_NAME], - "amount" = round(100*air.gases[gasid][MOLES]/air.total_moles(),0.01)))) - - else - for(var/gasid in air.gases) - gasdata.Add(list(list( - "name"= air.gases[gasid][GAS_META][META_GAS_NAME], - "amount" = 0))) - - data["gases"] = gasdata - + data["sm_data"] = list(sm_ui_data()) return data /// Encodes the current state of the supermatter. /obj/machinery/power/supermatter_crystal/proc/get_status() - var/turf/local_turf = get_turf(src) - if(!local_turf) - return SUPERMATTER_ERROR - var/datum/gas_mixture/air = local_turf.return_air() - if(!air) + if(!absorbed_gasmix) return SUPERMATTER_ERROR if(final_countdown) return SUPERMATTER_DELAMINATING @@ -312,9 +411,9 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) return SUPERMATTER_DANGER if(damage >= warning_point) return SUPERMATTER_WARNING - if(absorbed_gasmix.temperature > (T0C + HEAT_PENALTY_THRESHOLD) * 0.8) + if(absorbed_gasmix.temperature > temp_limit * 0.8) return SUPERMATTER_NOTIFY - if(power) + if(internal_energy) return SUPERMATTER_NORMAL return SUPERMATTER_INACTIVE @@ -326,28 +425,26 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) /obj/machinery/power/supermatter_crystal/update_overlays() . = ..() + if(psy_coeff > 0) + var/mutable_appearance/psy_overlay = mutable_appearance(icon, "[base_icon_state]-psy", FLOAT_LAYER - 1) + psy_overlay.alpha = psy_coeff * 255 + . += psy_overlay if(delamination_strategy) . += delamination_strategy.overlays(src) + return . + +/obj/machinery/power/supermatter_crystal/update_icon(updates) + . = ..() + if(gas_heat_power_generation > 0.8) + icon_state = "[base_icon_state]-glow" + else + icon_state = base_icon_state /obj/machinery/power/supermatter_crystal/proc/force_delam() SIGNAL_HANDLER investigate_log("was forcefully delaminated", INVESTIGATE_ENGINE) INVOKE_ASYNC(delamination_strategy, /datum/sm_delam/proc/delaminate, src) -/obj/machinery/power/supermatter_crystal/proc/supermatter_pull(turf/center, pull_range = 3) - playsound(center, 'sound/weapons/marauder.ogg', 100, TRUE, extrarange = pull_range - world.view) - for(var/atom/movable/movable_atom in orange(pull_range,center)) - if((movable_atom.anchored || movable_atom.move_resist >= MOVE_FORCE_EXTREMELY_STRONG)) //move resist memes. - if(istype(movable_atom, /obj/structure/closet)) - var/obj/structure/closet/closet = movable_atom - closet.open(force = TRUE) - continue - if(ismob(movable_atom)) - var/mob/pulled_mob = movable_atom - if(pulled_mob.mob_negates_gravity()) - continue //You can't pull someone nailed to the deck - step_towards(movable_atom,center) - /** * Count down, spout some messages, and then execute the delam itself. * We guard for last second delam strat changes here, mostly because some have diff messages. @@ -364,7 +461,6 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) return final_countdown = TRUE - update_appearance() var/datum/sm_delam/last_delamination_strategy = delamination_strategy var/list/count_down_messages = delamination_strategy.count_down_messages() @@ -398,32 +494,258 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) if(healed) final_countdown = FALSE - update_appearance() return // delam averted sleep(1 SECONDS) delamination_strategy.delaminate(src) -/proc/supermatter_anomaly_gen(turf/anomalycenter, type = FLUX_ANOMALY, anomalyrange = 5, has_changed_lifespan = TRUE) - var/turf/local_turf = pick(orange(anomalyrange, anomalycenter)) - if(!local_turf) +// All the calculate procs should only update variables. +// Move the actual real-world effects to [/obj/machinery/power/supermatter_crystal/process_atmos]. + +/** + * Perform calculation for variables that depend on gases. + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/list/gas_percentage] + * [/obj/machinery/power/supermatter_crystal/var/gas_power_transmission] + * [/obj/machinery/power/supermatter_crystal/var/gas_heat_modifier] + * [/obj/machinery/power/supermatter_crystal/var/gas_heat_resistance] + * [/obj/machinery/power/supermatter_crystal/var/gas_heat_power_generation] + * [/obj/machinery/power/supermatter_crystal/var/gas_powerloss_inhibition] + * + * Returns: null + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_gases() + gas_percentage = list() + gas_power_transmission = 0 + gas_heat_modifier = 0 + gas_heat_resistance = 0 + gas_heat_power_generation = 0 + gas_powerloss_inhibition = 0 + if(disable_gas) return - switch(type) - if(FLUX_ANOMALY) - var/explosive = has_changed_lifespan ? FLUX_NO_EXPLOSION : FLUX_LOW_EXPLOSIVE - new /obj/effect/anomaly/flux(local_turf, has_changed_lifespan ? rand(250, 350) : null, FALSE, explosive) - if(GRAVITATIONAL_ANOMALY) - new /obj/effect/anomaly/grav(local_turf, has_changed_lifespan ? rand(200, 300) : null, FALSE) - if(PYRO_ANOMALY) - new /obj/effect/anomaly/pyro(local_turf, has_changed_lifespan ? rand(150, 250) : null, FALSE) - if(HALLUCINATION_ANOMALY) - new /obj/effect/anomaly/hallucination(local_turf, has_changed_lifespan ? rand(150, 250) : null, FALSE) - if(VORTEX_ANOMALY) - new /obj/effect/anomaly/bhole(local_turf, 20, FALSE) - if(BIOSCRAMBLER_ANOMALY) - new /obj/effect/anomaly/bioscrambler(local_turf, null, FALSE) - if(DIMENSIONAL_ANOMALY) - new /obj/effect/anomaly/dimensional(local_turf, null, FALSE) + + var/total_moles = absorbed_gasmix.total_moles() + + for (var/gas_path in absorbed_gasmix.gases) + gas_percentage[gas_path] = absorbed_gasmix.gases[gas_path][MOLES] / total_moles + var/datum/sm_gas/sm_gas = GLOB.sm_gas_behavior[gas_path] + if(!sm_gas) + continue + gas_power_transmission += sm_gas.power_transmission * gas_percentage[gas_path] + gas_heat_modifier += sm_gas.heat_modifier * gas_percentage[gas_path] + gas_heat_resistance += sm_gas.heat_resistance * gas_percentage[gas_path] + gas_heat_power_generation += sm_gas.heat_power_generation * gas_percentage[gas_path] + gas_powerloss_inhibition += sm_gas.powerloss_inhibition * gas_percentage[gas_path] + + gas_heat_power_generation = clamp(gas_heat_power_generation, 0, 1) + gas_powerloss_inhibition = clamp(gas_powerloss_inhibition, 0, 1) + +/** + * Perform calculation for power lost and gained this tick. + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/internal_energy] + * [/obj/machinery/power/supermatter_crystal/var/external_power_trickle] + * [/obj/machinery/power/supermatter_crystal/var/external_power_immediate] + * + * Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_internal_energy() + if(disable_power_change) + return + var/list/additive_power = list() + + /// If we have a small amount of external_power_trickle we just round it up to 40. + additive_power[SM_POWER_EXTERNAL_TRICKLE] = external_power_trickle ? max(external_power_trickle/MATTER_POWER_CONVERSION, 40) : 0 + external_power_trickle -= min(additive_power[SM_POWER_EXTERNAL_TRICKLE], external_power_trickle) + additive_power[SM_POWER_EXTERNAL_IMMEDIATE] = external_power_immediate + external_power_immediate = 0 + additive_power[SM_POWER_HEAT] = gas_heat_power_generation * absorbed_gasmix.temperature / 6 + + // I'm sorry for this, but we need to calculate power lost immediately after power gain. + // Helps us prevent cases when someone dumps superhothotgas into the SM and shoots the power to the moon for one tick. + /// Power if we dont have decay. Used for powerloss calc. + var/momentary_power = internal_energy + for(var/powergain_type in additive_power) + momentary_power += additive_power[powergain_type] + if(internal_energy < powerloss_linear_threshold) // Negative numbers + additive_power[SM_POWER_POWERLOSS] = -1 * (momentary_power / POWERLOSS_CUBIC_DIVISOR) ** 3 + else + additive_power[SM_POWER_POWERLOSS] = -1 * (momentary_power * POWERLOSS_LINEAR_RATE + powerloss_linear_offset) + // Positive number + additive_power[SM_POWER_POWERLOSS_GAS] = -1 * gas_powerloss_inhibition * additive_power[SM_POWER_POWERLOSS] + additive_power[SM_POWER_POWERLOSS_SOOTHED] = -1 * min(1-gas_powerloss_inhibition , 0.2 * psy_coeff) * additive_power[SM_POWER_POWERLOSS] + + for(var/powergain_types in additive_power) + internal_energy += additive_power[powergain_types] + internal_energy = max(internal_energy, 0) + return additive_power + +/** + * Perform calculation for the main zap power multiplier. + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/zap_multiplier] + * + * Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_zap_multiplier() + var/list/additive_transmission = list() + additive_transmission[SM_ZAP_BASE] = 1 + additive_transmission[SM_ZAP_GAS] = gas_power_transmission + + zap_multiplier = 0 + for (var/transmission_types in additive_transmission) + zap_multiplier += additive_transmission[transmission_types] + zap_multiplier = max(zap_multiplier, 0) + return additive_transmission + +/** + * Perform calculation for the waste multiplier. + * This number affects the temperature, plasma, and oxygen of the waste gas. + * Multiplier is applied to energy for plasma and temperature but temperature for oxygen. + * + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/waste_multiplier] + * + * Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_waste_multiplier() + waste_multiplier = 0 + if(disable_gas) + return + /// Tell people the heat output in energy. More informative than telling them the heat multiplier. + var/additive_waste_multiplier = list() + additive_waste_multiplier[SM_WASTE_BASE] = 1 + additive_waste_multiplier[SM_WASTE_GAS] = gas_heat_modifier + additive_waste_multiplier[SM_WASTE_SOOTHED] = -0.2 * psy_coeff + + for (var/waste_type in additive_waste_multiplier) + waste_multiplier += additive_waste_multiplier[waste_type] + waste_multiplier = clamp(waste_multiplier, 0.5, INFINITY) + return additive_waste_multiplier + +/** + * Calculate at which temperature the sm starts taking damage. + * heat limit is given by: (T0C+40) * (1 + gas heat res + psy_coeff) + * + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/temp_limit] + * + * Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_temp_limit() + var/list/additive_temp_limit = list() + additive_temp_limit[SM_TEMP_LIMIT_BASE] = T0C + HEAT_PENALTY_THRESHOLD + additive_temp_limit[SM_TEMP_LIMIT_GAS] = gas_heat_resistance * (T0C + HEAT_PENALTY_THRESHOLD) + additive_temp_limit[SM_TEMP_LIMIT_SOOTHED] = psy_coeff * 45 + additive_temp_limit[SM_TEMP_LIMIT_LOW_MOLES] = clamp(2 - absorbed_gasmix.total_moles() / 100, 0, 1) * (T0C + HEAT_PENALTY_THRESHOLD) + + temp_limit = 0 + for (var/resistance_type in additive_temp_limit) + temp_limit += additive_temp_limit[resistance_type] + temp_limit = max(temp_limit, TCMB) + + return additive_temp_limit + +/** + * Perform calculation for the damage taken or healed. + * Description of each factors can be found in the defines. + * + * Updates: + * [/obj/machinery/power/supermatter_crystal/var/damage] + * + * Returns: The factors that have influenced the calculation. list[FACTOR_DEFINE] = number + */ +/obj/machinery/power/supermatter_crystal/proc/calculate_damage() + if(disable_damage) + return + /// Multiplier to our mole and power damage limits. + var/list/additive_damage = list() + var/mole_power_damage_limit = delamination_strategy.mole_power_damage_limit() + var/total_moles = absorbed_gasmix.total_moles() + additive_damage[SM_DAMAGE_HEAT] = max((absorbed_gasmix.temperature - temp_limit) * max(total_moles / 350, 0.25) / 600, 0) + additive_damage[SM_DAMAGE_POWER] = max((internal_energy - POWER_PENALTY_THRESHOLD * mole_power_damage_limit) / 2000, 0) + additive_damage[SM_DAMAGE_MOLES] = max((total_moles - MOLE_PENALTY_THRESHOLD * mole_power_damage_limit) / 320, 0) + if(total_moles) + additive_damage[SM_DAMAGE_HEAL_HEAT] = clamp((absorbed_gasmix.temperature - temp_limit) / 300, -2, 0) + + /// Damage from internal factors. + var/total_internal_damage = 0 + for (var/damage_type in additive_damage) + total_internal_damage += additive_damage[damage_type] + if(total_internal_damage > DAMAGE_HARDCAP) + // Scale each of the damages according to the hardcap. + // This means we lie a bit about each of the factors, but the final addition is honest. + for (var/damage_type in additive_damage) + // shorter way of writing damage = damage / total_internal_damage * damage_hardcap. + additive_damage[damage_type] *= DAMAGE_HARDCAP / total_internal_damage + total_internal_damage = DAMAGE_HARDCAP + + // We dont let external factors deal more damage than the emergency point. + // Evaluates the current damage and not the damage + internal damage though. + // Helps us decouple this and make it discrete. + additive_damage[SM_DAMAGE_EXTERNAL] = external_damage_immediate * clamp((emergency_point - damage) / emergency_point, 0, 1) + external_damage_immediate = 0 + + for(var/turf/open/space/turf_to_check in RANGE_TURFS(1, loc)) + if(LAZYLEN(turf_to_check.atmos_adjacent_turfs)) + additive_damage[SM_DAMAGE_SPACED] = clamp(internal_energy * 0.00125, 0, 10) + additive_damage[SM_DAMAGE_HEAL_HEAT] = 0 + break + + var/total_damage = 0 + for (var/damage_type in additive_damage) + total_damage += additive_damage[damage_type] + + damage_archived = damage + damage += total_damage + damage = max(damage, 0) + return additive_damage + +/** + * Sets the delam of our sm. + * + * Arguments: + * * priority: Truthy values means a forced delam. If current forced_delam is higher than priority we dont run. + * Set to a number higher than [SM_DELAM_PRIO_IN_GAME] to fully force an admin delam. + * * delam_path: Typepath of a [/datum/sm_delam]. [SM_DELAM_STRATEGY_PURGE] means reset and put prio back to zero. + * + * Returns: Not used for anything, just returns true on succesful set, manual and automatic. Helps admins check stuffs. + */ +/obj/machinery/power/supermatter_crystal/proc/set_delam(priority = SM_DELAM_PRIO_NONE, manual_delam_path = SM_DELAM_STRATEGY_PURGE) + if(priority < delam_priority) + return FALSE + var/datum/sm_delam/new_delam = null + + if(manual_delam_path == SM_DELAM_STRATEGY_PURGE) + for (var/delam_path in GLOB.sm_delam_list) + var/datum/sm_delam/delam = GLOB.sm_delam_list[delam_path] + if(!delam.can_select(src)) + continue + if(delam == delamination_strategy) + return FALSE + new_delam = delam + break + delam_priority = SM_DELAM_PRIO_NONE + else + new_delam = GLOB.sm_delam_list[manual_delam_path] + delam_priority = priority + + if(!new_delam) + return FALSE + delamination_strategy?.on_deselect(src) + delamination_strategy = new_delam + delamination_strategy.on_select(src) + return TRUE /obj/machinery/proc/supermatter_zap(atom/zapstart = src, range = 5, zap_str = 4000, zap_flags = ZAP_SUPERMATTER_FLAGS, list/targets_hit = list(), zap_cutoff = 1500, power_level = 0, zap_icon = DEFAULT_ZAP_ICON_STATE, color = null) if(QDELETED(zapstart)) @@ -564,60 +886,6 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) child_targets_hit = targets_hit.Copy() //Pass by ref begone supermatter_zap(target, new_range, zap_str, zap_flags, child_targets_hit, zap_cutoff, power_level, zap_icon, color) -/obj/machinery/power/supermatter_crystal/engine - is_main_engine = TRUE - -/obj/machinery/power/supermatter_crystal/shard - name = "supermatter shard" - desc = "A strangely translucent and iridescent crystal that looks like it used to be part of a larger structure." - base_icon_state = "darkmatter_shard" - icon_state = "darkmatter_shard" - anchored = FALSE - absorption_ratio = 0.125 - explosion_power = 12 - layer = ABOVE_MOB_LAYER - plane = GAME_PLANE_UPPER - moveable = TRUE - psyOverlay = /obj/overlay/psy/shard - -/obj/machinery/power/supermatter_crystal/shard/engine - name = "anchored supermatter shard" - is_main_engine = TRUE - anchored = TRUE - moveable = FALSE - -// When you wanna make a supermatter shard for the dramatic effect, but -// don't want it exploding suddenly -/obj/machinery/power/supermatter_crystal/shard/hugbox - name = "anchored supermatter shard" - takes_damage = FALSE - produces_gas = FALSE - power_changes = FALSE - processes = FALSE //SHUT IT DOWN - moveable = FALSE - anchored = TRUE - -/obj/machinery/power/supermatter_crystal/shard/hugbox/fakecrystal //Hugbox shard with crystal visuals, used in the Supermatter/Hyperfractal shuttle - name = "supermatter crystal" - base_icon_state = "darkmatter" - icon_state = "darkmatter" - -/obj/overlay/psy - icon = 'icons/obj/engine/supermatter.dmi' - icon_state = "psy" - layer = FLOAT_LAYER - 1 - -/obj/overlay/psy/shard - icon_state = "psy_shard" - -/atom/movable/supermatter_warp_effect - plane = GRAVITY_PULSE_PLANE - appearance_flags = PIXEL_SCALE // no tile bound so you can see it around corners and so - icon = 'icons/effects/light_overlays/light_352.dmi' - icon_state = "light" - pixel_x = -176 - pixel_y = -176 - #undef BIKE #undef COIL #undef ROD diff --git a/code/modules/power/supermatter/supermatter_delamination/_sm_delam.dm b/code/modules/power/supermatter/supermatter_delamination/_sm_delam.dm index f61672dbd92..0c6e0d73b60 100644 --- a/code/modules/power/supermatter/supermatter_delamination/_sm_delam.dm +++ b/code/modules/power/supermatter/supermatter_delamination/_sm_delam.dm @@ -67,8 +67,6 @@ GLOBAL_LIST_INIT(sm_delam_list, list( sm.lastwarning = REALTIMEOFDAY - (SUPERMATTER_WARNING_DELAY / 2) // Cut the time to next announcement in half. else // Taking damage, in warning sm.radio.talk_into(sm, "Danger! Crystal hyperstructure integrity faltering! Integrity: [sm.get_integrity_percent()]%", sm.warning_channel) - if(sm.damage_archived < sm.warning_point) - SEND_SIGNAL(sm, COMSIG_SUPERMATTER_DELAM_START_ALARM) SEND_SIGNAL(sm, COMSIG_SUPERMATTER_DELAM_ALARM) return TRUE @@ -95,11 +93,6 @@ GLOBAL_LIST_INIT(sm_delam_list, list( return list(mutable_appearance(sm.icon, "causality_field")) return list() -/// Modifies the damage dealt to the sm. -/// [/obj/machinery/power/supermatter_crystal/proc/deal_damage] -/datum/sm_delam/proc/damage_multiplier(obj/machinery/power/supermatter_crystal/sm) - return 1 - /// Returns a set of messages to be spouted during delams /// First message is start of count down, second message is quitting of count down (if sm healed), third is 5 second intervals /datum/sm_delam/proc/count_down_messages(obj/machinery/power/supermatter_crystal/sm) @@ -108,3 +101,7 @@ GLOBAL_LIST_INIT(sm_delam_list, list( messages += "Crystalline hyperstructure returning to safe operating parameters. Failsafe has been disengaged." messages += "remain before causality stabilization." return messages + +/// Adjusts the damage and mole limit at which we start taking damage. +/datum/sm_delam/proc/mole_power_damage_limit(obj/machinery/power/supermatter_crystal/sm) + return 1 diff --git a/code/modules/power/supermatter/supermatter_delamination/cascade_delam.dm b/code/modules/power/supermatter/supermatter_delamination/cascade_delam.dm index 68a827ddc7c..1a096a25707 100644 --- a/code/modules/power/supermatter/supermatter_delamination/cascade_delam.dm +++ b/code/modules/power/supermatter/supermatter_delamination/cascade_delam.dm @@ -7,7 +7,7 @@ if(total_moles < MOLE_PENALTY_THRESHOLD * sm.absorption_ratio) return FALSE for (var/gas_path in list(/datum/gas/antinoblium, /datum/gas/hypernoblium)) - var/percent = sm.absorbed_gasmix.gases[gas_path]?[MOLES] / total_moles + var/percent = sm.gas_percentage[gas_path] if(!percent || percent < 0.4) return FALSE return TRUE @@ -74,9 +74,6 @@ /datum/sm_delam/cascade/overlays(obj/machinery/power/supermatter_crystal/sm) return list() -/datum/sm_delam/cascade/damage_multiplier(obj/machinery/power/supermatter_crystal/sm) - return 0.25 - /datum/sm_delam/cascade/count_down_messages(obj/machinery/power/supermatter_crystal/sm) var/list/messages = list() messages += "CRYSTAL DELAMINATION IMMINENT. The supermatter has reached critical integrity failure. Harmonic frequency limits exceeded. Causality destabilization field could not be engaged." @@ -84,6 +81,9 @@ messages += "remain before resonance-induced stabilization." return messages +/datum/sm_delam/cascade/mole_power_damage_limit(obj/machinery/power/supermatter_crystal/sm) + return 0.25 + /datum/sm_delam/cascade/proc/announce_cascade(obj/machinery/power/supermatter_crystal/sm) if(QDELETED(sm)) return FALSE diff --git a/code/modules/power/supermatter/supermatter_delamination/common_delams.dm b/code/modules/power/supermatter/supermatter_delamination/common_delams.dm index c46aaf998fc..eeed35d6eb5 100644 --- a/code/modules/power/supermatter/supermatter_delamination/common_delams.dm +++ b/code/modules/power/supermatter/supermatter_delamination/common_delams.dm @@ -32,7 +32,7 @@ /datum/sm_delam/tesla /datum/sm_delam/tesla/can_select(obj/machinery/power/supermatter_crystal/sm) - return (sm.power > POWER_PENALTY_THRESHOLD) + return (sm.internal_energy > POWER_PENALTY_THRESHOLD) /datum/sm_delam/tesla/delam_progress(obj/machinery/power/supermatter_crystal/sm) . = ..() diff --git a/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm b/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm index 07b5142b93b..39b49657ef9 100644 --- a/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm +++ b/code/modules/power/supermatter/supermatter_delamination/delamination_effects.dm @@ -7,7 +7,7 @@ /// Just the mobs apparently. /datum/sm_delam/proc/effect_irradiate(obj/machinery/power/supermatter_crystal/sm) var/turf/sm_turf = get_turf(sm) - for (var/mob/living/victim in range(20, sm)) + for (var/mob/living/victim in range(DETONATION_RADIATION_RANGE, sm)) if(!is_valid_z_level(get_turf(victim), sm_turf)) continue if(victim.z == 0) @@ -70,7 +70,7 @@ /// Explodes /datum/sm_delam/proc/effect_explosion(obj/machinery/power/supermatter_crystal/sm) var/explosion_power = sm.explosion_power - var/power_scaling = sm.gasmix_power_ratio + var/power_scaling = sm.gas_heat_power_generation var/turf/sm_turf = get_turf(sm) //Dear mappers, balance the sm max explosion radius to 17.5, 37, 39, 41 explosion(origin = sm_turf, diff --git a/code/modules/power/supermatter/supermatter_extra_effects.dm b/code/modules/power/supermatter/supermatter_extra_effects.dm new file mode 100644 index 00000000000..62a66f11b8c --- /dev/null +++ b/code/modules/power/supermatter/supermatter_extra_effects.dm @@ -0,0 +1,183 @@ +// Any power past this number will be clamped down +#define MAX_ACCEPTED_POWER_OUTPUT 5000 + +// At the highest power output, assuming no integrity changes, the threshold will be 0. +#define THRESHOLD_EQUATION_SLOPE (-1 / MAX_ACCEPTED_POWER_OUTPUT) + +// The higher this number, the faster low integrity will drop threshold +// I would've named this "power", but y'know. :P +#define INTEGRITY_EXPONENTIAL_DEGREE 2 + +// At INTEGRITY_MIN_NUDGABLE_AMOUNT, the power will be treated as, at most, INTEGRITY_MAX_POWER_NUDGE. +// Any lower integrity will result in INTEGRITY_MAX_POWER_NUDGE. +#define INTEGRITY_MAX_POWER_NUDGE 1500 +#define INTEGRITY_MIN_NUDGABLE_AMOUNT 0.7 + +#define RADIATION_CHANCE_AT_FULL_INTEGRITY 0.03 +#define RADIATION_CHANCE_AT_ZERO_INTEGRITY 0.4 +#define CHANCE_EQUATION_SLOPE (RADIATION_CHANCE_AT_ZERO_INTEGRITY - RADIATION_CHANCE_AT_FULL_INTEGRITY) + +/obj/machinery/power/supermatter_crystal/proc/emit_radiation() + // As power goes up, rads go up. + // A standard N2 SM seems to produce a value of around 1,500. + var/power_factor = min(internal_energy, MAX_ACCEPTED_POWER_OUTPUT) + + var/integrity = 1 - CLAMP01(damage / explosion_point) + + // When integrity goes down, the threshold (from an observable point of view, rads) go up. + // However, the power factor must go up as well, otherwise turning off the emitters + // on a delaminating SM would stop radiation from escaping. + // To fix this, lower integrities raise the power factor to a minimum. + var/integrity_power_nudge = LERP(INTEGRITY_MAX_POWER_NUDGE, 0, CLAMP01((integrity - INTEGRITY_MIN_NUDGABLE_AMOUNT) / (1 - INTEGRITY_MIN_NUDGABLE_AMOUNT))) + + power_factor = max(power_factor, integrity_power_nudge) + + // At the "normal" N2 power output (with max integrity), this is 0.7, which is enough to be stopped + // by the walls or the radation shutters. + // As integrity does down, rads go up. + var/threshold + switch(integrity) + if(0) + threshold = power_factor ? 0 : 1 + if(1) + threshold = (THRESHOLD_EQUATION_SLOPE * power_factor + 1) + else + threshold = (THRESHOLD_EQUATION_SLOPE * power_factor + 1) ** ((1 / integrity) ** INTEGRITY_EXPONENTIAL_DEGREE) + + // Calculating chance is done entirely on integrity, so that actively delaminating SMs feel more dangerous + var/chance = (CHANCE_EQUATION_SLOPE * (1 - integrity)) + RADIATION_CHANCE_AT_FULL_INTEGRITY + + radiation_pulse( + src, + max_range = 8, + threshold = threshold, + chance = chance * 100, + ) + +/obj/machinery/power/supermatter_crystal/proc/processing_sound() + if(internal_energy) + soundloop.volume = clamp((50 + (internal_energy / 50)), 50, 100) + if(damage >= 300) + soundloop.mid_sounds = list('sound/machines/sm/loops/delamming.ogg' = 1) + else + soundloop.mid_sounds = list('sound/machines/sm/loops/calm.ogg' = 1) + + //We play delam/neutral sounds at a rate determined by power and damage + if(last_accent_sound >= world.time || !prob(20)) + return + var/aggression = min(((damage / 800) * (internal_energy / 2500)), 1.0) * 100 + if(damage >= 300) + playsound(src, SFX_SM_DELAM, max(50, aggression), FALSE, 40, 30, falloff_distance = 10) + else + playsound(src, SFX_SM_CALM, max(50, aggression), FALSE, 25, 25, falloff_distance = 10) + var/next_sound = round((100 - aggression) * 5) + last_accent_sound = world.time + max(SUPERMATTER_ACCENT_SOUND_MIN_COOLDOWN, next_sound) + +/obj/machinery/power/supermatter_crystal/proc/psychological_examination() + // Defaults to a value less than 1. Over time the psy_coeff goes to 0 if + // no supermatter soothers are nearby. + var/psy_coeff_diff = -0.05 + for(var/mob/living/carbon/human/seen_by_sm in view(src, SM_HALLUCINATION_RANGE(internal_energy))) + // Someone (generally a Psychologist), when looking at the SM within hallucination range makes it easier to manage. + if(HAS_TRAIT(seen_by_sm, TRAIT_SUPERMATTER_SOOTHER) || (seen_by_sm.mind && HAS_TRAIT(seen_by_sm.mind, TRAIT_SUPERMATTER_SOOTHER))) + psy_coeff_diff = 0.05 + visible_hallucination_pulse( + center = src, + radius = SM_HALLUCINATION_RANGE(internal_energy), + hallucination_duration = internal_energy * hallucination_power, + hallucination_max_duration = 400 SECONDS, + ) + psy_coeff = clamp(psy_coeff + psy_coeff_diff, 0, 1) + +/obj/machinery/power/supermatter_crystal/proc/handle_high_power() + if(internal_energy <= POWER_PENALTY_THRESHOLD && damage <= danger_point) //If the power is above 5000 or if the damage is above 550 + return + var/range = 4 + zap_cutoff = 1500 + var/total_moles = absorbed_gasmix.total_moles() + var/pressure = absorbed_gasmix.return_pressure() + var/temp = absorbed_gasmix.temperature + if(pressure > 0 && temp > 0) + //You may be able to freeze the zapstate of the engine with good planning, we'll see + zap_cutoff = clamp(3000 - (internal_energy * total_moles / 10) / temp, 350, 3000)//If the core is cold, it's easier to jump, ditto if there are a lot of mols + //We should always be able to zap our way out of the default enclosure + //See supermatter_zap() for more details + range = clamp(internal_energy / pressure * 10, 2, 7) + var/flags = ZAP_SUPERMATTER_FLAGS + var/zap_count = 0 + //Deal with power zaps + switch(internal_energy) + if(POWER_PENALTY_THRESHOLD to SEVERE_POWER_PENALTY_THRESHOLD) + zap_icon = DEFAULT_ZAP_ICON_STATE + zap_count = 2 + if(SEVERE_POWER_PENALTY_THRESHOLD to CRITICAL_POWER_PENALTY_THRESHOLD) + zap_icon = SLIGHTLY_CHARGED_ZAP_ICON_STATE + //Uncaps the zap damage, it's maxed by the input power + //Objects take damage now + flags |= (ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE) + zap_count = 3 + if(CRITICAL_POWER_PENALTY_THRESHOLD to INFINITY) + zap_icon = OVER_9000_ZAP_ICON_STATE + //It'll stun more now, and damage will hit harder, gloves are no garentee. + //Machines go boom + flags |= (ZAP_MOB_STUN | ZAP_MACHINE_EXPLOSIVE | ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE) + zap_count = 4 + //Now we deal with damage shit + if (damage > danger_point && prob(20)) + zap_count += 1 + + if(zap_count >= 1) + playsound(loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10) + for(var/i in 1 to zap_count) + supermatter_zap(src, range, clamp(internal_energy*2, 4000, 20000), flags, zap_cutoff = src.zap_cutoff, power_level = internal_energy, zap_icon = src.zap_icon) + + if(prob(5)) + supermatter_anomaly_gen(src, FLUX_ANOMALY, rand(5, 10)) + if(prob(5)) + supermatter_anomaly_gen(src, HALLUCINATION_ANOMALY, rand(5, 10)) + if(internal_energy > SEVERE_POWER_PENALTY_THRESHOLD && prob(5) || prob(1)) + supermatter_anomaly_gen(src, GRAVITATIONAL_ANOMALY, rand(5, 10)) + if((internal_energy > SEVERE_POWER_PENALTY_THRESHOLD && prob(2)) || (prob(0.3) && internal_energy > POWER_PENALTY_THRESHOLD)) + supermatter_anomaly_gen(src, PYRO_ANOMALY, rand(5, 10)) + +/obj/machinery/power/supermatter_crystal/proc/supermatter_pull(turf/center, pull_range = 3) + playsound(center, 'sound/weapons/marauder.ogg', 100, TRUE, extrarange = pull_range - world.view) + for(var/atom/movable/movable_atom in orange(pull_range,center)) + if((movable_atom.anchored || movable_atom.move_resist >= MOVE_FORCE_EXTREMELY_STRONG)) //move resist memes. + if(istype(movable_atom, /obj/structure/closet)) + var/obj/structure/closet/closet = movable_atom + closet.open(force = TRUE) + continue + if(ismob(movable_atom)) + var/mob/pulled_mob = movable_atom + if(pulled_mob.mob_negates_gravity()) + continue //You can't pull someone nailed to the deck + step_towards(movable_atom,center) + +/proc/supermatter_anomaly_gen(turf/anomalycenter, type = FLUX_ANOMALY, anomalyrange = 5, has_changed_lifespan = TRUE) + var/turf/local_turf = pick(orange(anomalyrange, anomalycenter)) + if(!local_turf) + return + switch(type) + if(FLUX_ANOMALY) + var/explosive = has_changed_lifespan ? FLUX_NO_EXPLOSION : FLUX_LOW_EXPLOSIVE + new /obj/effect/anomaly/flux(local_turf, has_changed_lifespan ? rand(250, 350) : null, FALSE, explosive) + if(GRAVITATIONAL_ANOMALY) + new /obj/effect/anomaly/grav(local_turf, has_changed_lifespan ? rand(200, 300) : null, FALSE) + if(PYRO_ANOMALY) + new /obj/effect/anomaly/pyro(local_turf, has_changed_lifespan ? rand(150, 250) : null, FALSE) + if(HALLUCINATION_ANOMALY) + new /obj/effect/anomaly/hallucination(local_turf, has_changed_lifespan ? rand(150, 250) : null, FALSE) + if(VORTEX_ANOMALY) + new /obj/effect/anomaly/bhole(local_turf, 20, FALSE) + if(BIOSCRAMBLER_ANOMALY) + new /obj/effect/anomaly/bioscrambler(local_turf, null, FALSE) + +#undef CHANCE_EQUATION_SLOPE +#undef INTEGRITY_EXPONENTIAL_DEGREE +#undef INTEGRITY_MAX_POWER_NUDGE +#undef INTEGRITY_MIN_NUDGABLE_AMOUNT +#undef MAX_ACCEPTED_POWER_OUTPUT +#undef RADIATION_CHANCE_AT_FULL_INTEGRITY +#undef RADIATION_CHANCE_AT_ZERO_INTEGRITY +#undef THRESHOLD_EQUATION_SLOPE diff --git a/code/modules/power/supermatter/supermatter_gas.dm b/code/modules/power/supermatter/supermatter_gas.dm index 23db8587e48..738715969bb 100644 --- a/code/modules/power/supermatter/supermatter_gas.dm +++ b/code/modules/power/supermatter/supermatter_gas.dm @@ -5,187 +5,225 @@ gas_list[sm_gas.gas_path] = sm_gas return gas_list +/// Return a list info of the SM gases. +/// Can only run after init_sm_gas +/proc/sm_gas_data() + var/list/data = list() + for (var/gas_path in GLOB.sm_gas_behavior) + var/datum/sm_gas/sm_gas = GLOB.sm_gas_behavior[gas_path] + var/list/singular_gas_data = list() + singular_gas_data["desc"] = sm_gas.desc + + // Positive is true if more of the amount is a good thing. + var/list/numeric_data = list() + if(sm_gas.power_transmission) + numeric_data += list(list( + "name" = "Power Transmission", + "amount" = sm_gas.power_transmission, + "positive" = TRUE, + )) + if(sm_gas.heat_modifier) + numeric_data += list(list( + "name" = "Waste Multiplier", + "amount" = sm_gas.heat_modifier, + "positive" = FALSE, + )) + if(sm_gas.heat_resistance) + numeric_data += list(list( + "name" = "Heat Resistance", + "amount" = sm_gas.heat_resistance, + "positive" = TRUE, + )) + if(sm_gas.heat_power_generation) + numeric_data += list(list( + "name" = "Heat Power Gain", + "amount" = sm_gas.heat_power_generation, + "positive" = TRUE, + )) + if(sm_gas.powerloss_inhibition) + numeric_data += list(list( + "name" = "Power Decay Negation", + "amount" = sm_gas.powerloss_inhibition, + "positive" = TRUE, + )) + singular_gas_data["numeric_data"] = numeric_data + data[gas_path] = singular_gas_data + return data + /// Assoc of sm_gas_behavior[/datum/gas (path)] = datum/sm_gas (instance) GLOBAL_LIST_INIT(sm_gas_behavior, init_sm_gas()) /// Contains effects of gases when absorbed by the sm. +/// If the gas has no effects you do not need to add another sm_gas subtype, +/// We already guard for nulls in [/obj/machinery/power/supermatter_crystal/proc/calculate_gases] /datum/sm_gas /// Path of the [/datum/gas] involved with this interaction. var/gas_path /// Influences zap power without interfering with the crystal's own energy. - var/transmit_modifier = 0 + var/power_transmission = 0 /// How much more waste heat and gas the SM generates. - var/heat_penalty = 0 + var/heat_modifier = 0 /// How extra hot the SM can run before taking damage var/heat_resistance = 0 /// Lets the sm generate extra power from heat. Yeah... - var/powermix = 0 + var/heat_power_generation = 0 /// How much powerloss do we get rid of. var/powerloss_inhibition = 0 + /// Give a short description of the gas if needed. If the gas have extra effects describe it here. + var/desc -/datum/sm_gas/proc/extra_effects(obj/machinery/power/supermatter_crystal/sm, datum/gas_mixture/env) +/datum/sm_gas/proc/extra_effects(obj/machinery/power/supermatter_crystal/sm) return /datum/sm_gas/oxygen gas_path = /datum/gas/oxygen - heat_penalty = 1 - transmit_modifier = 1.5 - powermix = 1 + power_transmission = 0.15 + heat_power_generation = 1 /datum/sm_gas/nitrogen gas_path = /datum/gas/nitrogen - heat_penalty = -1.5 - powermix = -1 + heat_modifier = -2.5 + heat_power_generation = -1 /datum/sm_gas/carbon_dioxide gas_path = /datum/gas/carbon_dioxide - heat_penalty = 2 - powermix = 1 + heat_modifier = 1 + heat_power_generation = 1 powerloss_inhibition = 1 + desc = "When absorbed by the Supermatter and exposed to oxygen, Pluoxium will be generated." /// Can be on Oxygen or CO2, but better lump it here since CO2 is rarer. -/datum/sm_gas/carbon_dioxide/extra_effects(obj/machinery/power/supermatter_crystal/sm, datum/gas_mixture/env) - if(!(sm.gas_percentage[/datum/gas/carbon_dioxide] && sm.gas_percentage[/datum/gas/oxygen])) +/datum/sm_gas/carbon_dioxide/extra_effects(obj/machinery/power/supermatter_crystal/sm) + if(!sm.gas_percentage[/datum/gas/carbon_dioxide] || !sm.gas_percentage[/datum/gas/oxygen]) return - var/co2_pp = env.return_pressure() * sm.gas_percentage[/datum/gas/carbon_dioxide] - /// Our consumption ratio, not the actual ratio in SM we already have that. - /// This var is a fucking lie, we only consume half of it. - var/co2_ratio = (co2_pp - CO2_CONSUMPTION_PP) / (co2_pp + CO2_PRESSURE_SCALING) - co2_ratio = clamp(co2_ratio, 0, 1) + var/co2_pp = sm.absorbed_gasmix.return_pressure() * sm.gas_percentage[/datum/gas/carbon_dioxide] + var/co2_ratio = clamp((1/2 * (co2_pp - CO2_CONSUMPTION_PP) / (co2_pp + CO2_PRESSURE_SCALING)), 0, 1) var/consumed_co2 = sm.absorbed_gasmix.gases[/datum/gas/carbon_dioxide][MOLES] * co2_ratio consumed_co2 = min( consumed_co2, - sm.absorbed_gasmix.gases[/datum/gas/carbon_dioxide][MOLES] * INVERSE(0.5), - sm.absorbed_gasmix.gases[/datum/gas/oxygen][MOLES] * INVERSE(0.5) + sm.absorbed_gasmix.gases[/datum/gas/carbon_dioxide][MOLES], + sm.absorbed_gasmix.gases[/datum/gas/oxygen][MOLES] ) if(!consumed_co2) return - sm.absorbed_gasmix.gases[/datum/gas/carbon_dioxide][MOLES] -= consumed_co2 * 0.5 - sm.absorbed_gasmix.gases[/datum/gas/oxygen][MOLES] -= consumed_co2 * 0.5 + sm.absorbed_gasmix.gases[/datum/gas/carbon_dioxide][MOLES] -= consumed_co2 + sm.absorbed_gasmix.gases[/datum/gas/oxygen][MOLES] -= consumed_co2 ASSERT_GAS(/datum/gas/pluoxium, sm.absorbed_gasmix) - sm.absorbed_gasmix.gases[/datum/gas/pluoxium][MOLES] += consumed_co2 * 0.25 + sm.absorbed_gasmix.gases[/datum/gas/pluoxium][MOLES] += consumed_co2 /datum/sm_gas/plasma gas_path = /datum/gas/plasma - heat_penalty = 15 - transmit_modifier = 4 - powermix = 1 + heat_modifier = 14 + power_transmission = 0.4 + heat_power_generation = 1 /datum/sm_gas/water_vapor gas_path = /datum/gas/water_vapor - heat_penalty = 12 - transmit_modifier = -2.5 - powermix = 1 + heat_modifier = 11 + power_transmission = -0.25 + heat_power_generation = 1 /datum/sm_gas/hypernoblium gas_path = /datum/gas/hypernoblium - heat_penalty = -13 - transmit_modifier = 3 - powermix = -1 + heat_modifier = -14 + power_transmission = 0.3 + heat_power_generation = -1 /datum/sm_gas/nitrous_oxide gas_path = /datum/gas/nitrous_oxide - heat_resistance = 6 - -/datum/sm_gas/nitrium - gas_path = /datum/gas/nitrium + heat_resistance = 5 /datum/sm_gas/tritium gas_path = /datum/gas/tritium - heat_penalty = 10 - transmit_modifier = 30 - powermix = 1 + heat_modifier = 9 + power_transmission = 3 + heat_power_generation = 1 /datum/sm_gas/bz gas_path = /datum/gas/bz - heat_penalty = 5 - transmit_modifier = -2 - powermix = 1 + heat_modifier = 4 + power_transmission = -0.2 + heat_power_generation = 1 + desc = "Will emit nuclear particles at compositions above 40%" /// Start to emit radballs at a maximum of 30% chance per tick -/datum/sm_gas/bz/extra_effects(obj/machinery/power/supermatter_crystal/sm, datum/gas_mixture/env) - if(sm.gas_percentage[/datum/gas/bz] >= 0.4 && prob(30 * sm.gas_percentage[/datum/gas/bz])) +/datum/sm_gas/bz/extra_effects(obj/machinery/power/supermatter_crystal/sm) + if(sm.gas_percentage[/datum/gas/bz] > 0.4 && prob(30 * sm.gas_percentage[/datum/gas/bz])) sm.fire_nuclear_particle() /datum/sm_gas/pluoxium gas_path = /datum/gas/pluoxium - heat_penalty = -0.5 - transmit_modifier = -5 - powermix = 1 + heat_modifier = -1.5 + power_transmission = -0.5 + heat_power_generation = 1 /datum/sm_gas/miasma gas_path = /datum/gas/miasma - powermix = 0.5 + heat_power_generation = 0.5 + desc = "Will be consumed by the Supermatter to generate power." ///Miasma is really just microscopic particulate. It gets consumed like anything else that touches the crystal. -/datum/sm_gas/miasma/extra_effects(obj/machinery/power/supermatter_crystal/sm, datum/gas_mixture/env) +/datum/sm_gas/miasma/extra_effects(obj/machinery/power/supermatter_crystal/sm) if(!sm.gas_percentage[/datum/gas/miasma]) return - var/miasma_pp = env.return_pressure() * sm.gas_percentage[/datum/gas/miasma] - /// Our consumption ratio, not the actual ratio in SM we already have that. - var/miasma_ratio = ((miasma_pp - MIASMA_CONSUMPTION_PP) / (miasma_pp + MIASMA_PRESSURE_SCALING)) * (1 + (sm.gasmix_power_ratio * MIASMA_GASMIX_SCALING)) - miasma_ratio = clamp(miasma_ratio, 0, 1) + var/miasma_pp = sm.absorbed_gasmix.return_pressure() * sm.gas_percentage[/datum/gas/miasma] + var/miasma_ratio = clamp(((miasma_pp - MIASMA_CONSUMPTION_PP) / (miasma_pp + MIASMA_PRESSURE_SCALING)) * (1 + (sm.gas_heat_power_generation * MIASMA_GASMIX_SCALING)), 0, 1) var/consumed_miasma = sm.absorbed_gasmix.gases[/datum/gas/miasma][MOLES] * miasma_ratio if(!consumed_miasma) return sm.absorbed_gasmix.gases[/datum/gas/miasma][MOLES] -= consumed_miasma - sm.matter_power += consumed_miasma * MIASMA_POWER_GAIN + sm.external_power_trickle += consumed_miasma * MIASMA_POWER_GAIN /datum/sm_gas/freon gas_path = /datum/gas/freon - heat_penalty = -10 - transmit_modifier = -30 - powermix = 1 + heat_modifier = -9 + power_transmission = -3 + heat_power_generation = 1 /datum/sm_gas/hydrogen gas_path = /datum/gas/hydrogen - heat_penalty = 10 - transmit_modifier = 25 - heat_resistance = 2 - powermix = 1 + heat_modifier = 9 + power_transmission = 2.5 + heat_resistance = 1 + heat_power_generation = 1 /datum/sm_gas/healium gas_path = /datum/gas/healium - heat_penalty = 4 - transmit_modifier = 2.4 - powermix = 1 + heat_modifier = 3 + power_transmission = 0.24 + heat_power_generation = 1 /datum/sm_gas/proto_nitrate gas_path = /datum/gas/proto_nitrate - heat_penalty = -3 - transmit_modifier = 15 - heat_resistance = 5 - powermix = 1 + heat_modifier = -4 + power_transmission = 1.5 + heat_resistance = 4 + heat_power_generation = 1 /datum/sm_gas/zauker gas_path = /datum/gas/zauker - heat_penalty = 8 - transmit_modifier = 20 - powermix = 1 + heat_modifier = 7 + power_transmission = 2 + heat_power_generation = 1 + desc = "Will generate electrical zaps." -/datum/sm_gas/zauker/extra_effects(obj/machinery/power/supermatter_crystal/sm, datum/gas_mixture/env) +/datum/sm_gas/zauker/extra_effects(obj/machinery/power/supermatter_crystal/sm) if(!prob(sm.gas_percentage[/datum/gas/zauker])) return playsound(sm.loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10) sm.supermatter_zap( - sm, - range = 6, - zap_str = clamp(sm.power * 2, 4000, 20000), - zap_flags = ZAP_MOB_STUN, - zap_cutoff = sm.zap_cutoff, - power_level = sm.power, + sm, + range = 6, + zap_str = clamp(sm.internal_energy * 2, 4000, 20000), + zap_flags = ZAP_MOB_STUN, + zap_cutoff = sm.zap_cutoff, + power_level = sm.internal_energy, zap_icon = sm.zap_icon ) -/datum/sm_gas/halon - gas_path = /datum/gas/halon - -/datum/sm_gas/helium - gas_path = /datum/gas/helium - /datum/sm_gas/antinoblium gas_path = /datum/gas/antinoblium - transmit_modifier = -5 - heat_penalty = 15 - powermix = 1 + heat_modifier = 14 + power_transmission = -0.5 + heat_power_generation = 1 diff --git a/code/modules/power/supermatter/supermatter_hit_procs.dm b/code/modules/power/supermatter/supermatter_hit_procs.dm index 9a269fc9965..c99279013ed 100644 --- a/code/modules/power/supermatter/supermatter_hit_procs.dm +++ b/code/modules/power/supermatter/supermatter_hit_procs.dm @@ -15,31 +15,32 @@ kiss_power = 20000 if(!istype(local_turf)) return FALSE - if(!istype(projectile.firer, /obj/machinery/power/emitter) && power_changes) + if(!istype(projectile.firer, /obj/machinery/power/emitter)) investigate_log("has been hit by [projectile] fired by [key_name(projectile.firer)]", INVESTIGATE_ENGINE) if(projectile.armor_flag != BULLET || kiss_power) if(kiss_power) - psyCoeff = 1 - psy_overlay = TRUE - if(power_changes) //This needs to be here I swear - power += projectile.damage * bullet_energy + kiss_power - if(!has_been_powered) - var/fired_from_str = projectile.fired_from ? " with [projectile.fired_from]" : "" - investigate_log( - projectile.firer \ - ? "has been powered for the first time by [key_name(projectile.firer)][fired_from_str]." \ - : "has been powered for the first time.", - INVESTIGATE_ENGINE - ) - message_admins( - projectile.firer \ - ? "[src] [ADMIN_JMP(src)] has been powered for the first time by [ADMIN_FULLMONTY(projectile.firer)][fired_from_str]." \ - : "[src] [ADMIN_JMP(src)] has been powered for the first time." - ) - has_been_powered = TRUE - else if(takes_damage) - damage += (projectile.damage * bullet_energy) * clamp((emergency_point - damage) / emergency_point, 0, 1) - if(damage > danger_point) + psy_coeff = 1 + external_power_immediate += projectile.damage * bullet_energy + kiss_power + if(!has_been_powered) + var/fired_from_str = projectile.fired_from ? " with [projectile.fired_from]" : "" + investigate_log( + projectile.firer \ + ? "has been powered for the first time by [key_name(projectile.firer)][fired_from_str]." \ + : "has been powered for the first time.", + INVESTIGATE_ENGINE + ) + message_admins( + projectile.firer \ + ? "[src] [ADMIN_JMP(src)] has been powered for the first time by [ADMIN_FULLMONTY(projectile.firer)][fired_from_str]." \ + : "[src] [ADMIN_JMP(src)] has been powered for the first time." + ) + has_been_powered = TRUE + else + external_damage_immediate += projectile.damage * bullet_energy + // Stop taking damage at emergency point, yell to players at danger point. + // This isn't clean and we are repeating [/obj/machinery/power/supermatter_crystal/proc/calculate_damage], sorry for this. + var/damage_to_be = damage + external_damage_immediate * clamp((emergency_point - damage) / emergency_point, 0, 1) + if(damage_to_be > danger_point) visible_message(span_notice("[src] compresses under stress, resisting further impacts!")) return BULLET_ACT_HIT @@ -78,7 +79,7 @@ if (scalpel.usesLeft) to_chat(user, span_danger("You extract a sliver from \the [src]. \The [src] begins to react violently!")) new /obj/item/nuke_core/supermatter_sliver(src.drop_location()) - matter_power += 800 + external_power_trickle += 800 scalpel.usesLeft-- if (!scalpel.usesLeft) to_chat(user, span_notice("A tiny piece of \the [scalpel] falls off, rendering it useless!")) @@ -104,8 +105,8 @@ investigate_log("[key_name(user)] attached [destabilizing_crystal] to a supermatter crystal.", INVESTIGATE_ENGINE) to_chat(user, span_danger("\The [destabilizing_crystal] snaps onto \the [src].")) set_delam(SM_DELAM_PRIO_IN_GAME, /datum/sm_delam/cascade) - damage += 100 - matter_power += 500 + external_damage_immediate += 100 + external_power_trickle += 500 qdel(destabilizing_crystal) return @@ -120,8 +121,5 @@ default_unfasten_wrench(user, tool) /obj/machinery/power/supermatter_crystal/proc/consume_callback(matter_increase, damage_increase) - if(matter_increase && power_changes) - matter_power += matter_increase - if(damage_increase && takes_damage) - damage += damage_increase - damage = max(damage, 0) + external_power_trickle += matter_increase + external_damage_immediate += damage_increase diff --git a/code/modules/power/supermatter/supermatter_process.dm b/code/modules/power/supermatter/supermatter_process.dm deleted file mode 100644 index 4a360557b7a..00000000000 --- a/code/modules/power/supermatter/supermatter_process.dm +++ /dev/null @@ -1,355 +0,0 @@ -/obj/machinery/power/supermatter_crystal/process_atmos() - if(!processes) //Just fuck me up bro - return - var/turf/local_turf = loc - - if(isnull(local_turf))// We have a null turf...something is wrong, stop processing this entity. - return PROCESS_KILL - - if(!istype(local_turf))//We are in a crate or somewhere that isn't turf, if we return to turf resume processing but for now. - return //Yeah just stop. - - if(isclosedturf(local_turf)) - var/turf/did_it_melt = local_turf.Melt() - if(!isclosedturf(did_it_melt)) //In case some joker finds way to place these on indestructible walls - visible_message(span_warning("[src] melts through [local_turf]!")) - return - - handle_crystal_sounds() - - //Ok, get the air from the turf - var/datum/gas_mixture/env = local_turf.return_air() - environment_total_moles = env.total_moles() - if(produces_gas) - //Remove gas from surrounding area - absorbed_gasmix = env.remove_ratio(absorption_ratio) - else - // Pass all the gas related code an empty gas container - absorbed_gasmix = new() - - overlays -= psyOverlay - if(psy_overlay) - overlays -= psyOverlay - if(psyCoeff > 0) - psyOverlay.alpha = psyCoeff * 255 - overlays += psyOverlay - else - psy_overlay = FALSE - damage_archived = damage - if(!absorbed_gasmix || !absorbed_gasmix.total_moles() || isspaceturf(local_turf)) //we're in space or there is no gas to process - if(takes_damage) - damage += max((power / 1000) * DAMAGE_INCREASE_MULTIPLIER, 0.1) // always does at least some damage - if(!istype(env, /datum/gas_mixture/immutable) && produces_gas && power) //There is no gas to process, but we are not in a space turf. Lets make them. - //Power * 0.55 * a value between 1 and 0.8 - var/device_energy = power * REACTION_POWER_MODIFIER * (1 - (psyCoeff * 0.2)) - //Can't do stuff if it's null, so lets make a new gasmix. - absorbed_gasmix = new() - //Since there is no gas to process, we will produce as if heat penalty is 1 and temperature at TCMB. - absorbed_gasmix.assert_gases(/datum/gas/plasma, /datum/gas/oxygen) - absorbed_gasmix.temperature = ((device_energy) / THERMAL_RELEASE_MODIFIER) - absorbed_gasmix.temperature = max(TCMB, min(absorbed_gasmix.temperature, 2500)) - absorbed_gasmix.gases[/datum/gas/plasma][MOLES] = max((device_energy) / PLASMA_RELEASE_MODIFIER, 0) - absorbed_gasmix.gases[/datum/gas/oxygen][MOLES] = max(((device_energy + TCMB) - T0C) / OXYGEN_RELEASE_MODIFIER, 0) - absorbed_gasmix.garbage_collect() - env.merge(absorbed_gasmix) - air_update_turf(FALSE, FALSE) - else - combined_gas = absorbed_gasmix.total_moles() - gas_percentage = list() - - power_transmission_bonus = 0 - dynamic_heat_modifier = 0 - dynamic_heat_resistance = 0 - gasmix_power_ratio = 0 - powerloss_dynamic_scaling = 0 - - for (var/gas_path in absorbed_gasmix.gases) - var/datum/sm_gas/sm_gas = GLOB.sm_gas_behavior[gas_path] - gas_percentage[gas_path] = absorbed_gasmix.gases[gas_path][MOLES] / combined_gas - power_transmission_bonus += sm_gas.transmit_modifier * gas_percentage[gas_path] - dynamic_heat_modifier += sm_gas.heat_penalty * gas_percentage[gas_path] - dynamic_heat_resistance += sm_gas.heat_resistance * gas_percentage[gas_path] - gasmix_power_ratio += sm_gas.powermix * gas_percentage[gas_path] - powerloss_dynamic_scaling += sm_gas.powerloss_inhibition * gas_percentage[gas_path] - - gasmix_power_ratio = clamp(gasmix_power_ratio, 0, 1) - dynamic_heat_modifier = max(dynamic_heat_modifier, 0.5) - dynamic_heat_resistance = max(dynamic_heat_resistance, 1) - - // Extra effects should always fire after the compositions are all finished - // Some extra effects like [/datum/sm_gas/carbon_dioxide/extra_effects] needs more than one gas. - for (var/gas_path in absorbed_gasmix.gases) - var/datum/sm_gas/sm_gas = GLOB.sm_gas_behavior[gas_path] - sm_gas.extra_effects(src, env) - - if(takes_damage) - //causing damage - deal_damage(absorbed_gasmix) - - //main power calculations proc - power_calculations(env, absorbed_gasmix) - //irradiate at this point - emit_radiation() - //handles temperature increase and gases made by the crystal - temperature_gas_production(env, absorbed_gasmix) - - //handles hallucinations and the presence of a psychiatrist - psychological_examination() - - //Transitions between one function and another, one we use for the fast inital startup, the other is used to prevent errors with fusion temperatures. - //Use of the second function improves the power gain imparted by using co2 - if(power_changes) - ///The power that is getting lost this tick. - var/power_loss = power < powerloss_linear_threshold ? ((power / POWERLOSS_CUBIC_DIVISOR) ** 3) : (power * POWERLOSS_LINEAR_RATE + powerloss_linear_offset) - power_loss *= powerloss_inhibitor * (1 - (PSYCHOLOGIST_POWERLOSS_REDUCTION * psyCoeff)) - power = max(power - power_loss, 0) - //After this point power is lowered - //This wraps around to the begining of the function - //Handle high power zaps/anomaly generation - handle_high_power(absorbed_gasmix) - - if(prob(15)) - supermatter_pull(loc, min(power/850, 3))//850, 1700, 2550 - - if(damage == 0) // Clear any in game forced delams if on full health. - set_delam(SM_DELAM_PRIO_IN_GAME, SM_DELAM_STRATEGY_PURGE) - else - set_delam() // This one cant clear any forced delams. - delamination_strategy.delam_progress(src) - - if(damage > explosion_point && !final_countdown) - count_down() - - return TRUE - -/obj/machinery/power/supermatter_crystal/proc/handle_crystal_sounds() - //We vary volume by power, and handle OH FUCK FUSION IN COOLING LOOP noises. - if(power) - soundloop.volume = clamp((50 + (power / 50)), 50, 100) - if(damage >= 300) - soundloop.mid_sounds = list('sound/machines/sm/loops/delamming.ogg' = 1) - else - soundloop.mid_sounds = list('sound/machines/sm/loops/calm.ogg' = 1) - - //We play delam/neutral sounds at a rate determined by power and damage - if(last_accent_sound >= world.time || !prob(20)) - return - var/aggression = min(((damage / 800) * (power / 2500)), 1.0) * 100 - if(damage >= 300) - playsound(src, SFX_SM_DELAM, max(50, aggression), FALSE, 40, 30, falloff_distance = 10) - else - playsound(src, SFX_SM_CALM, max(50, aggression), FALSE, 25, 25, falloff_distance = 10) - var/next_sound = round((100 - aggression) * 5) - last_accent_sound = world.time + max(SUPERMATTER_ACCENT_SOUND_MIN_COOLDOWN, next_sound) - -/obj/machinery/power/supermatter_crystal/proc/deal_damage(datum/gas_mixture/removed) - var/has_holes = FALSE - //Check for holes in the SM inner chamber - for(var/turf/open/space/turf_to_check in RANGE_TURFS(1, loc)) - if(LAZYLEN(turf_to_check.atmos_adjacent_turfs)) - damage += clamp((power * 0.005) * DAMAGE_INCREASE_MULTIPLIER, 0, MAX_SPACE_EXPOSURE_DAMAGE) - power += 250 - has_holes = TRUE - break - - var/delam_damage_multipler = delamination_strategy.damage_multiplier(src) - //more moles of gases are harder to heat than fewer, so let's scale heat damage around them - mole_heat_penalty = max(combined_gas / MOLE_HEAT_PENALTY, 0.25) - - //Due to DAMAGE_INCREASE_MULTIPLIER, we only deal one 4th of the damage the statements otherwise would cause - //((((some value between 0.5 and 1 * temp - ((273.15 + 40) * some values between 1 and 10)) * some number between 0.25 and knock your socks off / 150) * 0.25 - //Heat and mols account for each other, a lot of hot mols are more damaging then a few - //Mols start to have a positive effect on damage after 350 - damage = max(damage + (max(clamp(removed.total_moles() / 200, 0.5, 1) * removed.temperature - ((T0C + HEAT_PENALTY_THRESHOLD)*dynamic_heat_resistance), 0) * mole_heat_penalty / 150 ) * DAMAGE_INCREASE_MULTIPLIER, 0) - //Power only starts affecting damage when it is above 5000 (1250 when a cascade is occurring) - damage = max(damage + (max(power - (POWER_PENALTY_THRESHOLD * delam_damage_multipler), 0)/500) * DAMAGE_INCREASE_MULTIPLIER, 0) - //Molar count only starts affecting damage when it is above 1800 (450 when a cascade is occurring) - damage = max(damage + (max(combined_gas - (MOLE_PENALTY_THRESHOLD * delam_damage_multipler), 0)/80) * DAMAGE_INCREASE_MULTIPLIER, 0) - - //There might be a way to integrate healing and hurting via heat - //healing damage - if(combined_gas < MOLE_PENALTY_THRESHOLD && !has_holes) - //Only has a net positive effect when the temp is below 313.15, heals up to 2 damage. Psycologists increase this temp min by up to 45 - damage = max(damage + (min(removed.temperature - ((T0C + HEAT_PENALTY_THRESHOLD) + (45 * psyCoeff)), 0) / 150 ), 0) - - //caps damage rate - //Takes the lower number between archived damage + (1.8) and damage - //This means we can only deal 1.8 damage per function call - damage = min(damage_archived + (DAMAGE_HARDCAP * explosion_point),damage) - -/obj/machinery/power/supermatter_crystal/proc/power_calculations(datum/gas_mixture/env, datum/gas_mixture/removed) - //Ranges from 0 to 1(1-(value between 0 and 1 * ranges from 1 to 1.5(mol / 500))) - //We take the mol count, and scale it to be our inhibitor - powerloss_inhibitor = clamp(1-powerloss_dynamic_scaling, 0, 1) - - //Releases stored power into the general pool - //We get this by consuming shit or being scalpeled - if(matter_power && power_changes) - //We base our removed power off one 10th of the matter_power. - var/removed_matter = max(matter_power/MATTER_POWER_CONVERSION, 40) - //Adds at least 40 power - power = max(power + removed_matter, 0) - //Removes at least 40 matter power - matter_power = max(matter_power - removed_matter, 0) - - var/temp_factor = 50 - if(gasmix_power_ratio > 0.8) - //with a perfect gas mix, make the power more based on heat - icon_state = "[base_icon_state]_glow" - else - //in normal mode, power is less effected by heat - temp_factor = 30 - icon_state = base_icon_state - - //if there is more pluox and n2 then anything else, we receive no power increase from heat - if(power_changes) - power = max((removed.temperature * temp_factor / T0C) * gasmix_power_ratio + power, 0) - - //Zaps around 2.5 seconds at 1500 MeV, limited to 0.5 from 4000 MeV and up - if(power && (last_power_zap + 4 SECONDS - (power * 0.001)) < world.time) - playsound(src, 'sound/weapons/emitter2.ogg', 70, TRUE) - var/power_multiplier = max(0, 1 + power_transmission_bonus / 10) - var/pressure_multiplier = max((1 / ((env.return_pressure() ** pressure_bonus_curve_angle) + 1) * pressure_bonus_derived_steepness) + pressure_bonus_derived_constant, 1) - hue_angle_shift = clamp(903 * log(10, (power + 8000)) - 3590, -50, 240) - var/zap_color = color_matrix_rotate_hue(hue_angle_shift) - supermatter_zap( - zapstart = src, - range = 3, - zap_str = 2.5 * power * power_multiplier * pressure_multiplier, - zap_flags = ZAP_SUPERMATTER_FLAGS, - zap_cutoff = 300, - power_level = power, - color = zap_color, - ) - last_power_zap = world.time - -/obj/machinery/power/supermatter_crystal/proc/temperature_gas_production(datum/gas_mixture/env, datum/gas_mixture/removed) - //Power * 0.55 * a value between 1 and 0.8 - var/device_energy = power * REACTION_POWER_MODIFIER * (1 - (psyCoeff * 0.2)) - - //To figure out how much temperature to add each tick, consider that at one atmosphere's worth - //of pure oxygen, with all four lasers firing at standard energy and no N2 present, at room temperature - //that the device energy is around 2140. At that stage, we don't want too much heat to be put out - //Since the core is effectively "cold" - - //Also keep in mind we are only adding this temperature to (efficiency)% of the one tile the rock - //is on. An increase of 4*C @ 25% efficiency here results in an increase of 1*C / (#tilesincore) overall. - //Power * 0.55 * (some value between 1.5 and 23) / 5 - removed.temperature += ((device_energy * dynamic_heat_modifier) / THERMAL_RELEASE_MODIFIER) - //We can only emit so much heat, that being 57500 - removed.temperature = max(TCMB, min(removed.temperature, 2500 * dynamic_heat_modifier)) - - //Calculate how much gas to release - //Varies based on power and gas content - - absorbed_gasmix.assert_gases(/datum/gas/plasma, /datum/gas/oxygen) - removed.gases[/datum/gas/plasma][MOLES] += max((device_energy * dynamic_heat_modifier) / PLASMA_RELEASE_MODIFIER, 0) - //Varies based on power, gas content, and heat - removed.gases[/datum/gas/oxygen][MOLES] += max(((device_energy + removed.temperature * dynamic_heat_modifier) - T0C) / OXYGEN_RELEASE_MODIFIER, 0) - - if(produces_gas) - removed.garbage_collect() - env.merge(removed) - air_update_turf(FALSE, FALSE) - -/obj/machinery/power/supermatter_crystal/proc/psychological_examination() - // Defaults to a value less than 1. Over time the psyCoeff goes to 0 if - // no supermatter soothers are nearby. - var/psy_coeff_diff = -0.05 - for(var/mob/living/carbon/human/seen_by_sm in view(src, HALLUCINATION_RANGE(power))) - // Someone (generally a Psychologist), when looking at the SM within hallucination range makes it easier to manage. - if(HAS_TRAIT(seen_by_sm, TRAIT_SUPERMATTER_SOOTHER) || (seen_by_sm.mind && HAS_TRAIT(seen_by_sm.mind, TRAIT_SUPERMATTER_SOOTHER))) - psy_coeff_diff = 0.05 - psy_overlay = TRUE - - visible_hallucination_pulse( - center = src, - radius = HALLUCINATION_RANGE(power), - hallucination_duration = power * hallucination_power, - hallucination_max_duration = 400 SECONDS, - ) - psyCoeff = clamp(psyCoeff + psy_coeff_diff, 0, 1) - -/obj/machinery/power/supermatter_crystal/proc/handle_high_power(datum/gas_mixture/removed) - if(power <= POWER_PENALTY_THRESHOLD && damage <= danger_point) //If the power is above 5000 or if the damage is above 550 - return - var/range = 4 - zap_cutoff = 1500 - if(removed && removed.return_pressure() > 0 && removed.return_temperature() > 0) - //You may be able to freeze the zapstate of the engine with good planning, we'll see - zap_cutoff = clamp(3000 - (power * (removed.total_moles()) / 10) / removed.return_temperature(), 350, 3000)//If the core is cold, it's easier to jump, ditto if there are a lot of mols - //We should always be able to zap our way out of the default enclosure - //See supermatter_zap() for more details - range = clamp(power / removed.return_pressure() * 10, 2, 7) - var/flags = ZAP_SUPERMATTER_FLAGS - var/zap_count = 0 - //Deal with power zaps - switch(power) - if(POWER_PENALTY_THRESHOLD to SEVERE_POWER_PENALTY_THRESHOLD) - zap_icon = DEFAULT_ZAP_ICON_STATE - zap_count = 2 - if(SEVERE_POWER_PENALTY_THRESHOLD to CRITICAL_POWER_PENALTY_THRESHOLD) - zap_icon = SLIGHTLY_CHARGED_ZAP_ICON_STATE - //Uncaps the zap damage, it's maxed by the input power - //Objects take damage now - flags |= (ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE) - zap_count = 3 - if(CRITICAL_POWER_PENALTY_THRESHOLD to INFINITY) - zap_icon = OVER_9000_ZAP_ICON_STATE - //It'll stun more now, and damage will hit harder, gloves are no garentee. - //Machines go boom - flags |= (ZAP_MOB_STUN | ZAP_MACHINE_EXPLOSIVE | ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE) - zap_count = 4 - //Now we deal with damage shit - if (damage > danger_point && prob(20)) - zap_count += 1 - - if(zap_count >= 1) - playsound(loc, 'sound/weapons/emitter2.ogg', 100, TRUE, extrarange = 10) - for(var/i in 1 to zap_count) - supermatter_zap(src, range, clamp(power*2, 4000, 20000), flags, zap_cutoff = src.zap_cutoff, power_level = power, zap_icon = src.zap_icon) - - if(prob(5)) - supermatter_anomaly_gen(src, FLUX_ANOMALY, rand(5, 10)) - if(prob(5)) - supermatter_anomaly_gen(src, HALLUCINATION_ANOMALY, rand(5, 10)) - if(power > SEVERE_POWER_PENALTY_THRESHOLD && prob(5) || prob(1)) - supermatter_anomaly_gen(src, GRAVITATIONAL_ANOMALY, rand(5, 10)) - if((power > SEVERE_POWER_PENALTY_THRESHOLD && prob(2)) || (prob(0.3) && power > POWER_PENALTY_THRESHOLD)) - supermatter_anomaly_gen(src, PYRO_ANOMALY, rand(5, 10)) - -/** - * Sets the delam of our sm. - * - * Arguments: - * * priority: Truthy values means a forced delam. If current forced_delam is higher than priority we dont run. - * Set to a number higher than [SM_DELAM_PRIO_IN_GAME] to fully force an admin delam. - * * delam_path: Typepath of a [/datum/sm_delam]. [SM_DELAM_STRATEGY_PURGE] means reset and put prio back to zero. - * - * Returns: Not used for anything, just returns true on succesful set, manual and automatic. Helps admins check stuffs. - */ -/obj/machinery/power/supermatter_crystal/proc/set_delam(priority = SM_DELAM_PRIO_NONE, manual_delam_path = SM_DELAM_STRATEGY_PURGE) - if(priority < delam_priority) - return FALSE - var/datum/sm_delam/new_delam = null - - if(manual_delam_path == SM_DELAM_STRATEGY_PURGE) - for (var/delam_path in GLOB.sm_delam_list) - var/datum/sm_delam/delam = GLOB.sm_delam_list[delam_path] - if(!delam.can_select(src)) - continue - if(delam == delamination_strategy) - return FALSE - new_delam = delam - break - delam_priority = SM_DELAM_PRIO_NONE - else - new_delam = GLOB.sm_delam_list[manual_delam_path] - delam_priority = priority - - if(!new_delam) - return FALSE - delamination_strategy?.on_deselect(src) - delamination_strategy = new_delam - delamination_strategy.on_select(src) - return TRUE diff --git a/code/modules/power/supermatter/supermatter_radiation.dm b/code/modules/power/supermatter/supermatter_radiation.dm deleted file mode 100644 index 90e4aef001d..00000000000 --- a/code/modules/power/supermatter/supermatter_radiation.dm +++ /dev/null @@ -1,64 +0,0 @@ -// Any power past this number will be clamped down -#define MAX_ACCEPTED_POWER_OUTPUT 5000 - -// At the highest power output, assuming no integrity changes, the threshold will be 0. -#define THRESHOLD_EQUATION_SLOPE (-1 / MAX_ACCEPTED_POWER_OUTPUT) - -// The higher this number, the faster low integrity will drop threshold -// I would've named this "power", but y'know. :P -#define INTEGRITY_EXPONENTIAL_DEGREE 2 - -// At INTEGRITY_MIN_NUDGABLE_AMOUNT, the power will be treated as, at most, INTEGRITY_MAX_POWER_NUDGE. -// Any lower integrity will result in INTEGRITY_MAX_POWER_NUDGE. -#define INTEGRITY_MAX_POWER_NUDGE 1500 -#define INTEGRITY_MIN_NUDGABLE_AMOUNT 0.7 - -#define RADIATION_CHANCE_AT_FULL_INTEGRITY 0.03 -#define RADIATION_CHANCE_AT_ZERO_INTEGRITY 0.4 -#define CHANCE_EQUATION_SLOPE (RADIATION_CHANCE_AT_ZERO_INTEGRITY - RADIATION_CHANCE_AT_FULL_INTEGRITY) - -/obj/machinery/power/supermatter_crystal/proc/emit_radiation() - // As power goes up, rads go up. - // A standard N2 SM seems to produce a value of around 1,500. - var/power_factor = min(power, MAX_ACCEPTED_POWER_OUTPUT) - - var/integrity = 1 - CLAMP01(damage / explosion_point) - - // When integrity goes down, the threshold (from an observable point of view, rads) go up. - // However, the power factor must go up as well, otherwise turning off the emitters - // on a delaminating SM would stop radiation from escaping. - // To fix this, lower integrities raise the power factor to a minimum. - var/integrity_power_nudge = LERP(INTEGRITY_MAX_POWER_NUDGE, 0, CLAMP01((integrity - INTEGRITY_MIN_NUDGABLE_AMOUNT) / (1 - INTEGRITY_MIN_NUDGABLE_AMOUNT))) - - power_factor = max(power_factor, integrity_power_nudge) - - // At the "normal" N2 power output (with max integrity), this is 0.7, which is enough to be stopped - // by the walls or the radation shutters. - // As integrity does down, rads go up. - var/threshold - switch(integrity) - if(0) - threshold = power_factor ? 0 : 1 - if(1) - threshold = (THRESHOLD_EQUATION_SLOPE * power_factor + 1) - else - threshold = (THRESHOLD_EQUATION_SLOPE * power_factor + 1) ** ((1 / integrity) ** INTEGRITY_EXPONENTIAL_DEGREE) - - // Calculating chance is done entirely on integrity, so that actively delaminating SMs feel more dangerous - var/chance = (CHANCE_EQUATION_SLOPE * (1 - integrity)) + RADIATION_CHANCE_AT_FULL_INTEGRITY - - radiation_pulse( - src, - max_range = 8, - threshold = threshold, - chance = chance * 100, - ) - -#undef CHANCE_EQUATION_SLOPE -#undef INTEGRITY_EXPONENTIAL_DEGREE -#undef INTEGRITY_MAX_POWER_NUDGE -#undef INTEGRITY_MIN_NUDGABLE_AMOUNT -#undef MAX_ACCEPTED_POWER_OUTPUT -#undef RADIATION_CHANCE_AT_FULL_INTEGRITY -#undef RADIATION_CHANCE_AT_ZERO_INTEGRITY -#undef THRESHOLD_EQUATION_SLOPE diff --git a/code/modules/power/supermatter/supermatter_variants.dm b/code/modules/power/supermatter/supermatter_variants.dm new file mode 100644 index 00000000000..bcdf5ea802c --- /dev/null +++ b/code/modules/power/supermatter/supermatter_variants.dm @@ -0,0 +1,48 @@ +/// Normal SM with it's processing disabled. +/obj/machinery/power/supermatter_crystal/hugbox + disable_damage = TRUE + disable_gas = TRUE + disable_power_change = TRUE + disable_process = TRUE + +/// Normal SM designated as main engine. +/obj/machinery/power/supermatter_crystal/engine + is_main_engine = TRUE + +/// Shard SM. +/obj/machinery/power/supermatter_crystal/shard + name = "supermatter shard" + desc = "A strangely translucent and iridescent crystal that looks like it used to be part of a larger structure." + base_icon_state = "sm_shard" + icon_state = "sm_shard" + anchored = FALSE + absorption_ratio = 0.125 + explosion_power = 12 + layer = ABOVE_MOB_LAYER + plane = GAME_PLANE_UPPER + moveable = TRUE + +/// Shard SM with it's processing disabled. +/obj/machinery/power/supermatter_crystal/shard/hugbox + name = "anchored supermatter shard" + disable_damage = TRUE + disable_gas = TRUE + disable_power_change = TRUE + disable_process = TRUE + moveable = FALSE + anchored = TRUE + +/// Shard SM designated as the main engine. +/obj/machinery/power/supermatter_crystal/shard/engine + name = "anchored supermatter shard" + is_main_engine = TRUE + anchored = TRUE + moveable = FALSE + +/atom/movable/supermatter_warp_effect + plane = GRAVITY_PULSE_PLANE + appearance_flags = PIXEL_SCALE // no tile bound so you can see it around corners and so + icon = 'icons/effects/light_overlays/light_352.dmi' + icon_state = "light" + pixel_x = -176 + pixel_y = -176 diff --git a/icons/obj/engine/supermatter.dmi b/icons/obj/engine/supermatter.dmi index dd006d56de868fa89d31ab0b9c220a28d1e6533b..874773ef0859b195090b500014dc360c90a396b1 100644 GIT binary patch literal 19705 zcmV)%K#jkNP)006)U1^@s6gF9$<0002cdQ@0+L}hbh za%pgMX>V=-0C=2@kwFfEFc3x8^c0P33uzIf8#hK`SjZJfl|m;eYT7EPr?;{Z3>bI+ zxAQVDnGo-?LtfzhF-H%f4O}mHi_@5*po7XvUzQ09q<}`+_s<3y%dPNnVcG=>D$vFk zhJsr2R=o4lzY4{jk2GEr8m|Z&Qc-87nhP}YMqW@ETClIv=ko*4YGD;a8z6wmx_ zxB~0S8iblzHdLjAkuT5aX59al{|r~c#}g=c!lfzC+WD&Hs1RqxzQ5`VN3w@^*xvj_ z002wmNkl?y9Kku5Ili zuA*zN^xj29Y;;KPjS><uYU62Q7?9kLb^?~uu;mPluGGYRewTX z>{zr6S-qp~rk}eh{X(gh^5ODw9;~#8r__hi+sbxBL^AR>{ea}m@N4Pkca%y=x8p?h z?K)?Yffjt1W-3vToKJMk$!LP5NWnA{Un~|I6x+ALo6A%C8$Ql6BTns*&@-zndrz&dzih zyO<|`NQLmNDPuHDnPWsU_&tD_@BnLcRPdujGWgFx zSfB+vf2w?1-p_?)_NH>V^6_;UfNvY^{b5WV1Y^oz7((twu+@YeKUU7AeUXp=<1Jwj z8a2sv_T!nW{rU|@z?s7lIA8?)5^q65ToB^V3tYwf<>O59(jWu*m_$BD(QTHKc&=0K zPG4m0KOFvpN5YSiHT@zjz1ycCh$gjxowP0oim zg$py9(n4l@gI_lo0>UVTQU&+|(PKYBRG84uy1U8cb)upGz$7Q*`A=tWvGg5Dw{3+r z)L$fv;XT-|{|bq*qWoQ+=b#G!=tQ?!lZ;dE%~)wZa|Fy(;Vr>d_=S-EhWC+p`$ve6 z7W!Mz7PQ8rRf4V*79S%4%Rs9}6D^D|@Vem<`W=Ya>Ke64Q0Vj$g|2ZPBhue1IrL?(AbO~JvB zA-O1iUeKupp!6_li_v0)Ilv4{5P>kn57zj52oQmY?;e36w?_Ynwg5OT7DOwEEl}7@ z(eG~&l|bYn<1Sc2LJ*%Fj{X
VBEo{ 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 literal 38939 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 lMN7* { )); return gas && gas.color; }; + +export const getGasFromId = (gasId) => { + const gasSearchString = String(gasId).toLowerCase(); + // prettier-ignore + const gas = GASES.find((gas) => ( + gas.id === gasSearchString + || gas.name.toLowerCase() === gasSearchString + )); + return gas; +}; + +// Paths need to be exact matches so we dont need to lowercase stuffs. +export const getGasFromPath = (gasPath) => { + return GASES.find((gas) => gasPath === gas.path); +}; diff --git a/tgui/packages/tgui/interfaces/NtosSupermatter.tsx b/tgui/packages/tgui/interfaces/NtosSupermatter.tsx new file mode 100644 index 00000000000..4b89123a21e --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosSupermatter.tsx @@ -0,0 +1,74 @@ +import { useBackend, useLocalState } from '../backend'; +import { NtosWindow } from '../layouts'; +import { SupermatterContent, SupermatterData } from './Supermatter'; +import { Button, ProgressBar, Section, Table } from '../components'; + +type NtosSupermatterData = SupermatterData & { focus_uid?: number }; + +export const NtosSupermatter = (props, context) => { + const { act, data } = useBackend(context); + const { sm_data, gas_metadata, focus_uid } = data; + const [activeUID, setActiveUID] = useLocalState(context, 'activeUID', 0); + const activeSM = sm_data.find((sm) => sm.uid === activeUID); + return ( + + + {activeSM ? ( + setActiveUID(0)}> + Back + + } + /> + ) : ( +

act('PRG_refresh')} + /> + }> + + {sm_data.map((sm) => ( + + {sm.uid + '. ' + sm.area_name} + + Integrity: + + + + + +
+
+ )} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosSupermatterMonitor.tsx b/tgui/packages/tgui/interfaces/NtosSupermatterMonitor.tsx deleted file mode 100644 index c7c40bc6307..00000000000 --- a/tgui/packages/tgui/interfaces/NtosSupermatterMonitor.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { NtosWindow } from '../layouts'; -import { SupermatterMonitorContent } from './SupermatterMonitor'; - -export const NtosSupermatterMonitor = () => { - return ( - - - - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/Supermatter.tsx b/tgui/packages/tgui/interfaces/Supermatter.tsx new file mode 100644 index 00000000000..4c66b5975c5 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Supermatter.tsx @@ -0,0 +1,391 @@ +import { filter, sortBy } from 'common/collections'; +import { flow } from 'common/fp'; +import { toFixed } from 'common/math'; +import { BooleanLike } from 'common/react'; +import { InfernoNode } from 'inferno'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, LabeledList, ProgressBar, Section, Stack } from '../components'; +import { getGasFromPath } from '../constants'; +import { Window } from '../layouts'; + +const logScale = (value) => Math.log2(16 + Math.max(0, value)) - 4; + +type SMGasMetadata = { + [key: string]: { + desc?: string; + numeric_data: { + name: string; + amount: number; + positive: BooleanLike; + }[]; + }; +}; + +type SupermatterProps = { + sectionButton?: InfernoNode; + uid: number; + area_name: string; + integrity: number; + integrity_factors: { name: string; amount: number }[]; + internal_energy: number; + internal_energy_factors: { name: string; amount: number }[]; + zap_multiplier: number; + zap_multiplier_factors: { name: string; amount: number }[]; + temp_limit: number; + temp_limit_factors: { name: string; amount: number }[]; + waste_multiplier: number; + waste_multiplier_factors: { name: string; amount: number }[]; + absorbed_ratio: number; + gas_composition: { [gas_path: string]: number }; + gas_temperature: number; + gas_total_moles: number; + gas_metadata: SMGasMetadata; +}; + +// LabeledList but stack and with a chevron dropdown. +type SupermatterEntryProps = { + title: string; + content: InfernoNode; + detail?: InfernoNode; + alwaysShowChevron?: boolean; +}; +const SupermatterEntry = (props: SupermatterEntryProps, context) => { + const { title, content, detail, alwaysShowChevron } = props; + if (!alwaysShowChevron && !detail) { + return ( + + + + {title + ':'} + + {content} + + + ); + } + const [activeDetail, setActiveDetail] = useLocalState(context, title, false); + return ( + <> + + + + {title + ':'} + + {content} + + + }> + + {gas_composition.map(([gas_path, amount]) => ( + + {toFixed(amount * 100, 2) + '%'} + + } + detail={ + gas_metadata[gas_path] ? ( + <> + {gas_metadata[gas_path].desc &&
} + {gas_metadata[gas_path].numeric_data.length ? ( + <> + + At 100% Composition gives: + + + {gas_metadata[gas_path].numeric_data.map( + (effect) => + effect.amount !== 0 && ( + 0 + ? 'green' + : 'red' + : effect.amount < 0 + ? 'green' + : 'red' + }> + {effect.amount > 0 + ? '+' + effect.amount * 100 + '%' + : effect.amount * 100 + '%'} + + ) + )} + + + ) : ( + 'Has no composition effects' + )} + + ) : ( + 'Has no effects' + ) + } + /> + ))} +
+ +
+
+ ); +}; + +export type SupermatterData = { + sm_data: Omit[]; + gas_metadata: SMGasMetadata; +}; + +export const Supermatter = (props, context) => { + const { act, data } = useBackend(context); + const { sm_data, gas_metadata } = data; + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/SupermatterMonitor.js b/tgui/packages/tgui/interfaces/SupermatterMonitor.js deleted file mode 100644 index 6115c31a886..00000000000 --- a/tgui/packages/tgui/interfaces/SupermatterMonitor.js +++ /dev/null @@ -1,195 +0,0 @@ -import { sortBy } from 'common/collections'; -import { flow } from 'common/fp'; -import { toFixed } from 'common/math'; -import { useBackend } from '../backend'; -import { Button, LabeledList, ProgressBar, Section, Stack, Table } from '../components'; -import { getGasColor, getGasLabel } from '../constants'; -import { Window } from '../layouts'; - -const logScale = (value) => Math.log2(16 + Math.max(0, value)) - 4; - -export const SupermatterMonitor = () => { - return ( - - - - - - ); -}; - -export const SupermatterMonitorContent = (props, context) => { - const { act, data } = useBackend(context); - const { - active, - singlecrystal, - SM_uid, - SM_area_name, - SM_integrity, - SM_power, - SM_ambienttemp, - SM_ambientpressure, - SM_moles, - SM_bad_moles_amount, - } = data; - if (!active) { - return ; - } - const gases = flow([ - (gases) => gases.filter((gas) => gas.amount >= 0.01), - sortBy((gas) => -gas.amount), - ])(data.gases || []); - const gasMaxAmount = Math.max(1, ...gases.map((gas) => gas.amount)); - return ( -
act('PRG_clear')} - /> - ) - }> - - -
- - - - - - - {toFixed(SM_power) + ' MeV/cm3'} - - - - - {toFixed(SM_ambienttemp) + ' K'} - - - - - {toFixed(SM_moles) + ' moles'} - - - - - {toFixed(SM_ambientpressure) + ' kPa'} - - - -
-
- -
- - {gases.map((gas) => ( - - - {toFixed(gas.amount, 2) + '%'} - - - ))} - -
-
-
-
- ); -}; - -const SupermatterList = (props, context) => { - const { act, data } = useBackend(context); - const { supermatters = [] } = data; - return ( -
act('PRG_refresh')} - /> - }> - - {supermatters.map((sm) => ( - - {sm.uid + '. ' + sm.area_name} - - Integrity: - - - - - -
-
- ); -};