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>
This commit is contained in:
Will
2026-04-02 01:06:44 +02:00
committed by GitHub
co-authored by Kashargul
parent 105d7647b1
commit c26fefb25d
16 changed files with 322 additions and 15 deletions
@@ -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 )
+1
View File
@@ -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))
+43
View File
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) => (
<Section key={exp.index} title={exp.time}>
<LabeledList>
<LabeledList.Item label="Coordinates">
{exp.x}.{exp.y}.{exp.z}
</LabeledList.Item>
<LabeledList.Item label="Inner Radius">
{exp.devastation_range}
</LabeledList.Item>
<LabeledList.Item label="Outer Radius">
{exp.heavy_impact_range}
</LabeledList.Item>
<LabeledList.Item label="Shockwave Radius">
{exp.light_impact_range}
</LabeledList.Item>
<LabeledList.Item label="Tachyon Displacement">
{exp.seconds_taken}
</LabeledList.Item>
</LabeledList>
</Section>
));
};
@@ -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<React.SetStateAction<SearchFields>>;
searchText: string;
setSearchText: React.Dispatch<React.SetStateAction<string>>;
}) => {
const { searchFields, setSearchFields, searchText, setSearchText } = props;
return (
<Collapsible color="transparent" title="Search Filter">
<Stack wrap="wrap">
{Object.keys(searchFields).map((field) => (
<Stack.Item key={field}>
<Button.Checkbox
checked={searchFields[field]}
onClick={() =>
setSearchFields({
...searchFields,
[field]: !searchFields[field],
})
}
>
{capitalize(field.replace(/_/g, ' '))}
</Button.Checkbox>
</Stack.Item>
))}
</Stack>
<Input
placeholder="Search explosions..."
value={searchText}
onChange={(value) => setSearchText(value)}
fluid
/>
</Collapsible>
);
};
@@ -0,0 +1,23 @@
import { Tabs } from 'tgui-core/components';
import { explosionTypes } from './constants';
export const DopplerTabs = (props: {
activeTab: string;
setActiveTab: React.Dispatch<React.SetStateAction<string>>;
}) => {
const { activeTab, setActiveTab } = props;
return (
<Tabs>
{['All', ...explosionTypes].map((sev) => (
<Tabs.Tab
key={sev}
selected={activeTab === sev}
onClick={() => setActiveTab(sev)}
>
{sev}
</Tabs.Tab>
))}
</Tabs>
);
};
@@ -0,0 +1 @@
export const explosionTypes = ['Small', 'Medium', 'Large'];
@@ -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];
}
@@ -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<Data>();
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<Explosion>(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<Record<string, Explosion[]>>(
(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 (
<Window width={350} height={500}>
<Window.Content scrollable>
<Stack vertical>
<Stack.Item>
<DopplerTabs activeTab={activeTab} setActiveTab={setActiveTab} />
</Stack.Item>
<Stack.Item>
<DopplerSearch
searchFields={searchFields}
setSearchFields={setSearchFields}
searchText={searchText}
setSearchText={setSearchText}
/>
</Stack.Item>
<Stack.Item grow>
{filteredExplosions.length ? (
<DopplerExplosions explosions={filteredExplosions} />
) : (
<NoticeBox>
<Box inline verticalAlign="middle">
No recorded explosions.
</Box>
</NoticeBox>
)}
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -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;
};
+2
View File
@@ -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"