From c26fefb25d384a8065c45ca32ffd39962a99d3f2 Mon Sep 17 00:00:00 2001 From: Will <7099514+Willburd@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:06:44 -0400 Subject: [PATCH] Explosives science experiments (#19359) * yay explosions * i have no idea what i am doing with tgui * experiment stuff done * time title * tabs * . * searh handling * - * make it a bit easier to understand * use correct time * tuned --------- Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- code/__defines/dcs/signals/signals_object.dm | 2 + code/__defines/experisci.dm | 1 + code/game/machinery/doppler_array.dm | 43 +++++++++ .../experiment/handlers/experiment_handler.dm | 27 +++--- .../experiment/instances/ordinance.dm | 11 +++ .../experisci/experiment/types/ordinance.dm | 30 +++++++ .../tg/machinery/destructive_analyzer.dm | 1 + .../tg/techwebs/nodes/security_nodes.dm | 4 +- .../DopplerArray/DopplerExplosions.tsx | 28 ++++++ .../interfaces/DopplerArray/DopplerSearch.tsx | 40 +++++++++ .../interfaces/DopplerArray/DopplerTabs.tsx | 23 +++++ .../tgui/interfaces/DopplerArray/constants.ts | 1 + .../tgui/interfaces/DopplerArray/functions.ts | 10 +++ .../tgui/interfaces/DopplerArray/index.tsx | 90 +++++++++++++++++++ .../tgui/interfaces/DopplerArray/types.ts | 24 +++++ vorestation.dme | 2 + 16 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 code/modules/research/tg/experisci/experiment/instances/ordinance.dm create mode 100644 code/modules/research/tg/experisci/experiment/types/ordinance.dm create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/DopplerExplosions.tsx create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/DopplerSearch.tsx create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/DopplerTabs.tsx create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/constants.ts create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/functions.ts create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/index.tsx create mode 100644 tgui/packages/tgui/interfaces/DopplerArray/types.ts diff --git a/code/__defines/dcs/signals/signals_object.dm b/code/__defines/dcs/signals/signals_object.dm index d435b9b082..dfd22635af 100644 --- a/code/__defines/dcs/signals/signals_object.dm +++ b/code/__defines/dcs/signals/signals_object.dm @@ -32,6 +32,8 @@ #define COMSIG_MACHINERY_SET_OCCUPANT "machinery_set_occupant" ///from /obj/machinery/rnd/destructive_analyzer/proc/destroy_item(gain_research_points = FALSE): Runs when the destructive scanner scans a group of objects. (list/scanned_atoms) #define COMSIG_MACHINERY_DESTRUCTIVE_SCAN "machinery_destructive_scan" +///from /obj/machinery/doppler_array/proc/sense_explosion(): Runs when an explosion is detected. (turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken) +#define COMSIG_MACHINERY_EXPLOSION_DETECTED "machinery_explosion_detected" ///from /obj/machinery/computer/arcade/victory_tickets(tickets, sound = TRUE) #define COMSIG_ARCADE_VICTORY "arcade_victory" ///from /obj/machinery/computer/telescience/proc/doteleport(mob/user): (list/atom/movable/teleported_things, turf/target_turf, sending ) diff --git a/code/__defines/experisci.dm b/code/__defines/experisci.dm index a7d3075ab2..9c79560c34 100644 --- a/code/__defines/experisci.dm +++ b/code/__defines/experisci.dm @@ -16,6 +16,7 @@ #define EXPERIMENT_TAG_BASE "Base" #define EXPERIMENT_TAG_SCAN "Scan" #define EXPERIMENT_TAG_PHYSICAL "Physical Experiment" +#define EXPERIMENT_TAG_ORDINANCE "Explosives Experiment" /// Macro for defining a progress stage #define EXPERIMENT_PROGRESS(type, desc, values...) list(list(type, desc, values)) diff --git a/code/game/machinery/doppler_array.dm b/code/game/machinery/doppler_array.dm index b95ff4aac5..3c7fdb4f65 100644 --- a/code/game/machinery/doppler_array.dm +++ b/code/game/machinery/doppler_array.dm @@ -8,14 +8,38 @@ icon_state = "doppler" circuit = /obj/item/circuitboard/doppler_array + var/list/detected_explosions = list() + /obj/machinery/doppler_array/Initialize(mapload) + //Explosive analysis + var/static/list/explosive_signals = list( + COMSIG_MACHINERY_EXPLOSION_DETECTED = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_ordinance_experiment), + ) + AddComponent(/datum/component/experiment_handler, \ + config_mode = EXPERIMENT_CONFIG_ALTCLICK, \ + allowed_experiments = list(/datum/experiment/ordnance),\ + config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE|EXPERIMENT_CONFIG_SILENT_FAIL,\ + experiment_signals = explosive_signals, \ + ) . = ..() RegisterSignal(SSdcs, COMSIG_GLOB_EXPLOSION, PROC_REF(sense_explosion)) + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, ROUNDSTART_TRAIT) /obj/machinery/doppler_array/Destroy() UnregisterSignal(SSdcs, COMSIG_GLOB_EXPLOSION) . = ..() +/obj/machinery/doppler_array/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "DopplerArray", name) + ui.open() + +/obj/machinery/doppler_array/tgui_static_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/list/data = list() + data["explosions"] = length(detected_explosions) ? detected_explosions : null; + return data + /obj/machinery/doppler_array/proc/sense_explosion(datum/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken) SIGNAL_HANDLER @@ -31,6 +55,21 @@ if(our_turf.Distance(epicenter) > 100) return atom_say("Explosive disturbance detected - Epicenter at: grid ([x0],[y0],[z0]). Epicenter radius: [devastation_range]. Outer radius: [heavy_impact_range]. Shockwave radius: [light_impact_range]. Temporal displacement of tachyons: [seconds_taken] seconds.") + SEND_SIGNAL(src, COMSIG_MACHINERY_EXPLOSION_DETECTED, epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken) + detected_explosions += list( + list( + "index" = length(detected_explosions), + "time" = stationtime2text(), + "x" = x0, + "y" = y0, + "z" = z0, + "devastation_range" = devastation_range, + "heavy_impact_range" = heavy_impact_range, + "light_impact_range" = light_impact_range, + "seconds_taken" = seconds_taken, + ) + ) + update_static_data_for_all_viewers() /obj/machinery/doppler_array/power_change() ..() @@ -47,3 +86,7 @@ return if(default_part_replacement(user, W)) return + +/obj/machinery/doppler_array/attack_hand(mob/user) + . = ..() + tgui_interact(user) diff --git a/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm b/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm index 2fd9be27cb..e83fb5666a 100644 --- a/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm +++ b/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm @@ -134,22 +134,23 @@ to_chat(user, span_notice("[target] is not related to your currently selected experiment.")) /** - * Hooks on destructive scans to try and run an experiment (When using a handheld handler) + * Hooks on destructive scans to try and run a destructive analyzer experiment. */ -/datum/component/experiment_handler/proc/try_run_destructive_experiment(datum/source, atom/scan_target) +/datum/component/experiment_handler/proc/try_run_destructive_experiment(obj/source, atom/scan_target) SIGNAL_HANDLER - var/atom/movable/our_scanner = parent - if (selected_experiment == null && !(config_flags & EXPERIMENT_CONFIG_ALWAYS_ACTIVE) ) - if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL)) - playsound(our_scanner, 'sound/machines/buzz-sigh.ogg', 25) - to_chat(our_scanner, span_notice("No experiment selected!")) - return + if(action_experiment(source, scan_target)) - playsound(our_scanner, 'sound/machines/ping.ogg', 25) - to_chat(our_scanner, span_notice("The scan succeeds.")) - else if(!(config_flags & EXPERIMENT_CONFIG_SILENT_FAIL)) - playsound(src, 'sound/machines/buzz-sigh.ogg', 25) - our_scanner.atom_say("The scan did not result in anything.") + playsound(source, 'sound/machines/ping.ogg', 25) + source.atom_say("Destructive analysis complete.") + +/** + * Hooks on doppler array scans to try and run a explosive experiment. + */ +/datum/component/experiment_handler/proc/try_run_ordinance_experiment(obj/source, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken) + SIGNAL_HANDLER + + if(action_experiment(source, epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken)) + playsound(source, 'sound/machines/ping.ogg', 25) /// Hooks on a successful autopsy experiment /datum/component/experiment_handler/proc/try_run_autopsy_experiment(obj/source, mob/living/target) diff --git a/code/modules/research/tg/experisci/experiment/instances/ordinance.dm b/code/modules/research/tg/experisci/experiment/instances/ordinance.dm new file mode 100644 index 0000000000..a9a2bfae85 --- /dev/null +++ b/code/modules/research/tg/experisci/experiment/instances/ordinance.dm @@ -0,0 +1,11 @@ +/datum/experiment/ordnance/lowyieldbomb + name = "Explosive Sensor Test" + description = "Detonate a simple explosive and record observations using a doppler array." + required_devastation_range = 1 + required_heavy_impact_range = 1 + +/datum/experiment/ordnance/highyieldbomb + name = "High Yield Explosives Test" + description = "Detonate a much bigger explosive and record observations using a doppler array." + required_devastation_range = 3 + required_heavy_impact_range = 5 diff --git a/code/modules/research/tg/experisci/experiment/types/ordinance.dm b/code/modules/research/tg/experisci/experiment/types/ordinance.dm new file mode 100644 index 0000000000..08a720edd0 --- /dev/null +++ b/code/modules/research/tg/experisci/experiment/types/ordinance.dm @@ -0,0 +1,30 @@ +/datum/experiment/ordnance + name = "Explosives Research" + description = "An experiment conducted in the phoronics subdepartment." + exp_tag = EXPERIMENT_TAG_ORDINANCE + performance_hint = "Perform research using explosive devices." + allowed_experimentors = list(/obj/machinery/doppler_array) + var/required_devastation_range = 0 + var/required_heavy_impact_range = 0 + var/required_light_impact_range = 0 + var/explosion_criteria_met = FALSE + +/datum/experiment/ordnance/is_complete() + return explosion_criteria_met + +/datum/experiment/ordnance/check_progress() + var/status_message = "Detonate an explosive of sufficient size and detect it with a doppler array." + . += EXPERIMENT_PROG_BOOL(status_message, is_complete()) + +/datum/experiment/ordnance/actionable(datum/component/experiment_handler/experiment_handler) + return !is_complete() + +/datum/experiment/ordnance/perform_experiment_actions(datum/component/experiment_handler/experiment_handler, turf/epicenter, devastation_range, heavy_impact_range, light_impact_range, seconds_taken) + if(devastation_range < required_devastation_range) + return FALSE + if(heavy_impact_range < required_heavy_impact_range) + return FALSE + if(light_impact_range < required_light_impact_range) + return FALSE + explosion_criteria_met = TRUE + return TRUE diff --git a/code/modules/research/tg/machinery/destructive_analyzer.dm b/code/modules/research/tg/machinery/destructive_analyzer.dm index c8d51f44f2..9d6372740d 100644 --- a/code/modules/research/tg/machinery/destructive_analyzer.dm +++ b/code/modules/research/tg/machinery/destructive_analyzer.dm @@ -38,6 +38,7 @@ It is used to destroy hand-held objects and advance technological research. Used ) . = ..() default_apply_parts() + ADD_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER, ROUNDSTART_TRAIT) /obj/machinery/rnd/destructive_analyzer/Destroy() rmat = null diff --git a/code/modules/research/tg/techwebs/nodes/security_nodes.dm b/code/modules/research/tg/techwebs/nodes/security_nodes.dm index ce9d8213a7..c89f39bedc 100644 --- a/code/modules/research/tg/techwebs/nodes/security_nodes.dm +++ b/code/modules/research/tg/techwebs/nodes/security_nodes.dm @@ -283,7 +283,7 @@ "large_Grenade", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS) - // required_experiments = list(/datum/experiment/ordnance/explosive/lowyieldbomb) + required_experiments = list(/datum/experiment/ordnance/lowyieldbomb) announce_channels = list(CHANNEL_SECURITY, CHANNEL_MEDICAL) /datum/techweb_node/exotic_ammo @@ -308,7 +308,7 @@ // "techshotshell", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS) - // discount_experiments = list(/datum/experiment/ordnance/explosive/highyieldbomb = TECHWEB_TIER_4_POINTS) + discount_experiments = list(/datum/experiment/ordnance/highyieldbomb = TECHWEB_TIER_4_POINTS) announce_channels = list(CHANNEL_SECURITY) /datum/techweb_node/electric_weapons diff --git a/tgui/packages/tgui/interfaces/DopplerArray/DopplerExplosions.tsx b/tgui/packages/tgui/interfaces/DopplerArray/DopplerExplosions.tsx new file mode 100644 index 0000000000..97d5a3c4e3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/DopplerExplosions.tsx @@ -0,0 +1,28 @@ +import { LabeledList, Section } from 'tgui-core/components'; +import type { Explosion } from './types'; + +export const DopplerExplosions = (props: { explosions: Explosion[] }) => { + const { explosions } = props; + + return explosions.map((exp) => ( +
+ + + {exp.x}.{exp.y}.{exp.z} + + + {exp.devastation_range} + + + {exp.heavy_impact_range} + + + {exp.light_impact_range} + + + {exp.seconds_taken} + + +
+ )); +}; diff --git a/tgui/packages/tgui/interfaces/DopplerArray/DopplerSearch.tsx b/tgui/packages/tgui/interfaces/DopplerArray/DopplerSearch.tsx new file mode 100644 index 0000000000..fa7c1e5805 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/DopplerSearch.tsx @@ -0,0 +1,40 @@ +import { Button, Collapsible, Input, Stack } from 'tgui-core/components'; +import { capitalize } from 'tgui-core/string'; +import type { SearchFields } from './types'; + +export const DopplerSearch = (props: { + searchFields: SearchFields; + setSearchFields: React.Dispatch>; + searchText: string; + setSearchText: React.Dispatch>; +}) => { + const { searchFields, setSearchFields, searchText, setSearchText } = props; + + return ( + + + {Object.keys(searchFields).map((field) => ( + + + setSearchFields({ + ...searchFields, + [field]: !searchFields[field], + }) + } + > + {capitalize(field.replace(/_/g, ' '))} + + + ))} + + setSearchText(value)} + fluid + /> + + ); +}; diff --git a/tgui/packages/tgui/interfaces/DopplerArray/DopplerTabs.tsx b/tgui/packages/tgui/interfaces/DopplerArray/DopplerTabs.tsx new file mode 100644 index 0000000000..c6406dbcfd --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/DopplerTabs.tsx @@ -0,0 +1,23 @@ +import { Tabs } from 'tgui-core/components'; +import { explosionTypes } from './constants'; + +export const DopplerTabs = (props: { + activeTab: string; + setActiveTab: React.Dispatch>; +}) => { + const { activeTab, setActiveTab } = props; + + return ( + + {['All', ...explosionTypes].map((sev) => ( + setActiveTab(sev)} + > + {sev} + + ))} + + ); +}; diff --git a/tgui/packages/tgui/interfaces/DopplerArray/constants.ts b/tgui/packages/tgui/interfaces/DopplerArray/constants.ts new file mode 100644 index 0000000000..8266001b0a --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/constants.ts @@ -0,0 +1 @@ +export const explosionTypes = ['Small', 'Medium', 'Large']; diff --git a/tgui/packages/tgui/interfaces/DopplerArray/functions.ts b/tgui/packages/tgui/interfaces/DopplerArray/functions.ts new file mode 100644 index 0000000000..475c7b06c7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/functions.ts @@ -0,0 +1,10 @@ +import { explosionTypes } from './constants'; +import type { Explosion } from './types'; + +export function getSeverity(exp: Explosion) { + if (exp.devastation_range >= 3 || exp.heavy_impact_range >= 5) + return explosionTypes[2]; + if (exp.devastation_range >= 2 || exp.heavy_impact_range >= 3) + return explosionTypes[1]; + return explosionTypes[0]; +} diff --git a/tgui/packages/tgui/interfaces/DopplerArray/index.tsx b/tgui/packages/tgui/interfaces/DopplerArray/index.tsx new file mode 100644 index 0000000000..493844c1e7 --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/index.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Box, NoticeBox, Stack } from 'tgui-core/components'; +import { createSearch } from 'tgui-core/string'; +import { DopplerExplosions } from './DopplerExplosions'; +import { DopplerSearch } from './DopplerSearch'; +import { DopplerTabs } from './DopplerTabs'; +import { getSeverity } from './functions'; +import type { Data, Explosion } from './types'; + +export const DopplerArray = (props) => { + const { data } = useBackend(); + const [searchFields, setSearchFields] = useState({ + time: true, + coordinates: true, + inner_radius: true, + outer_radius: true, + shockwave: true, + tachyon: true, + }); + const [searchText, setSearchText] = useState(''); + const [activeTab, setActiveTab] = useState('All'); + + const { explosions } = data; + + const searcher = createSearch(searchText, (exp) => { + const parts: string[] = []; + + if (searchFields.time) parts.push(exp.time); + if (searchFields.coordinates) parts.push(`${exp.x}.${exp.y}.${exp.z}`); + if (searchFields.inner_radius) parts.push(exp.devastation_range.toString()); + if (searchFields.outer_radius) + parts.push(exp.heavy_impact_range.toString()); + if (searchFields.shockwave) parts.push(exp.light_impact_range.toString()); + if (searchFields.tachyon) parts.push(exp.seconds_taken.toString()); + + return parts.join(' '); + }); + + const grouped = explosions?.reduce>( + (acc, exp) => { + const severity = getSeverity(exp); + + if (!acc[severity]) { + acc[severity] = []; + } + + acc[severity].push(exp); + return acc; + }, + {}, + ); + + const visibleExplosions = + activeTab === 'All' ? explosions : grouped?.[activeTab]; + + const filteredExplosions = (visibleExplosions ?? []).filter(searcher); + + return ( + + + + + + + + + + + {filteredExplosions.length ? ( + + ) : ( + + + No recorded explosions. + + + )} + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/DopplerArray/types.ts b/tgui/packages/tgui/interfaces/DopplerArray/types.ts new file mode 100644 index 0000000000..6895ca94ff --- /dev/null +++ b/tgui/packages/tgui/interfaces/DopplerArray/types.ts @@ -0,0 +1,24 @@ +export type Data = { + explosions: Explosion[]; +}; + +export type Explosion = { + index: number; + time: string; + x: number; + y: number; + z: number; + devastation_range: number; + heavy_impact_range: number; + light_impact_range: number; + seconds_taken: number; +}; + +export type SearchFields = { + time: boolean; + coordinates: boolean; + inner_radius: boolean; + outer_radius: boolean; + shockwave: boolean; + tachyon: boolean; +}; diff --git a/vorestation.dme b/vorestation.dme index dcb23422ce..9cb0798cf9 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -4557,10 +4557,12 @@ #include "code\modules\research\tg\experisci\experiment\instances\destructive.dm" #include "code\modules\research\tg\experisci\experiment\instances\machines.dm" #include "code\modules\research\tg\experisci\experiment\instances\misc.dm" +#include "code\modules\research\tg\experisci\experiment\instances\ordinance.dm" #include "code\modules\research\tg\experisci\experiment\instances\people.dm" #include "code\modules\research\tg\experisci\experiment\instances\physical.dm" #include "code\modules\research\tg\experisci\experiment\instances\teleporting.dm" #include "code\modules\research\tg\experisci\experiment\types\experiment.dm" +#include "code\modules\research\tg\experisci\experiment\types\ordinance.dm" #include "code\modules\research\tg\experisci\experiment\types\physical_experiment.dm" #include "code\modules\research\tg\experisci\experiment\types\random_scanning.dm" #include "code\modules\research\tg\experisci\experiment\types\scanning.dm"