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"