diff --git a/bun.lock b/bun.lock index 3a30c7ff72..4ee0e74f55 100644 --- a/bun.lock +++ b/bun.lock @@ -9,23 +9,23 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@2.2.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.5", "@biomejs/cli-darwin-x64": "2.2.5", "@biomejs/cli-linux-arm64": "2.2.5", "@biomejs/cli-linux-arm64-musl": "2.2.5", "@biomejs/cli-linux-x64": "2.2.5", "@biomejs/cli-linux-x64-musl": "2.2.5", "@biomejs/cli-win32-arm64": "2.2.5", "@biomejs/cli-win32-x64": "2.2.5" }, "bin": { "biome": "bin/biome" } }, "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw=="], + "@biomejs/biome": ["@biomejs/biome@2.2.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.6", "@biomejs/cli-darwin-x64": "2.2.6", "@biomejs/cli-linux-arm64": "2.2.6", "@biomejs/cli-linux-arm64-musl": "2.2.6", "@biomejs/cli-linux-x64": "2.2.6", "@biomejs/cli-linux-x64-musl": "2.2.6", "@biomejs/cli-win32-arm64": "2.2.6", "@biomejs/cli-win32-x64": "2.2.6" }, "bin": { "biome": "bin/biome" } }, "sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ=="], "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], } diff --git a/code/__defines/dcs/signals.dm b/code/__defines/dcs/signals.dm index 5ad7a43718..df9b2bc4f5 100644 --- a/code/__defines/dcs/signals.dm +++ b/code/__defines/dcs/signals.dm @@ -194,6 +194,7 @@ #define COMSIG_CLICK_CTRL "ctrl_click" ///from base of atom/AltClick(): (/mob) #define COMSIG_CLICK_ALT "alt_click" + #define COMPONENT_CANCEL_CLICK_ALT (1<<0) ///from base of atom/CtrlShiftClick(/mob) #define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click" ///from base of atom/MouseDrop(): (/atom/over, /mob/user) diff --git a/code/__defines/dcs/signals/signals_object.dm b/code/__defines/dcs/signals/signals_object.dm index cf18d404da..0c4cb414f1 100644 --- a/code/__defines/dcs/signals/signals_object.dm +++ b/code/__defines/dcs/signals/signals_object.dm @@ -1,2 +1,4 @@ ///from /obj/machinery/computer/arcade/prizevend(mob/user, prizes = 1) #define COMSIG_ARCADE_PRIZEVEND "arcade_prizevend" +///from /obj/machinery/rnd/destructive_analyzer/proc/destroy_item_individual(gain_research_points = FALSE): Runs when the destructive scanner scans a group of objects. (obj/item) +#define COMSIG_MACHINERY_DESTRUCTIVE_SCAN "machinery_destructive_scan" diff --git a/code/_helpers/global_lists.dm b/code/_helpers/global_lists.dm index 96f6c6b140..c0c62dc3c9 100644 --- a/code/_helpers/global_lists.dm +++ b/code/_helpers/global_lists.dm @@ -396,6 +396,20 @@ GLOBAL_LIST_INIT(item_digestion_blacklist, list( /obj/item/mmi/digital/robot, /obj/item/rig/protean)) +///A list of stuff we do NOT want being deconstructed. Either due to how critical it is (ID/nuke disk) or buggy (holders and paicards) +GLOBAL_LIST_INIT(item_deconstruction_blacklist, list( + /obj/item/card/id, + /obj/item/areaeditor/blueprints, + /obj/item/disk/nuclear, + /obj/item/perfect_tele_beacon, + /obj/item/organ/internal/brain, + /obj/item/mmi, + /obj/item/rig/protean, + /obj/item/holder, + /obj/item/paicard, + /obj/item/stack/material/cyborg, + /obj/item/storage)) + ///A list of chemicals that are banned from being obtainable through means that generate chemicals. These chemicals are either lame, annoying, pref-breaking, or OP (This list does NOT include reactions) GLOBAL_LIST_INIT(obtainable_chemical_blacklist, list( REAGENT_ID_ADMINORDRAZINE, diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index a2ddd19f91..6d7685a79c 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -303,8 +303,8 @@ // if(!user.can_interact_with(src)) // return FALSE - // if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT) - // return TRUE + if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT) + return TRUE if(HAS_TRAIT(src, TRAIT_ALT_CLICK_BLOCKER) && !isobserver(user)) return TRUE diff --git a/code/controllers/subsystems/research.dm b/code/controllers/subsystems/research.dm index 467304bd78..9cb5b705d2 100644 --- a/code/controllers/subsystems/research.dm +++ b/code/controllers/subsystems/research.dm @@ -41,7 +41,17 @@ SUBSYSTEM_DEF(research) var/list/techweb_nodes_experimental = list() ///path = list(point type = value) var/list/techweb_point_items = list( - // /obj/item/assembly/signaler/anomaly = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS) + /obj/item/research_sample/common = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS*0.5), + /obj/item/research_sample/uncommon = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_2_POINTS*0.5), + /obj/item/research_sample/rare = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS*0.5), + /obj/item/research_sample/bluespace = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_4_POINTS*0.5) + ) + ///Allows repeated deconstruction of these items for points. These items MUST be in techweb_point_items as well. + var/list/techweb_repeatable_items = list( + /obj/item/research_sample/common, + /obj/item/research_sample/uncommon, + /obj/item/research_sample/rare, + /obj/item/research_sample/bluespace ) var/list/errored_datums = list() ///Associated list of all point types that techwebs will have and their respective 'abbreviated' name. diff --git a/code/game/objects/items/weapons/circuitboards/machinery/research.dm b/code/game/objects/items/weapons/circuitboards/machinery/research.dm index 20c310e539..26a08f6962 100644 --- a/code/game/objects/items/weapons/circuitboards/machinery/research.dm +++ b/code/game/objects/items/weapons/circuitboards/machinery/research.dm @@ -111,3 +111,13 @@ req_components = list( /obj/item/stock_parts/manipulator = 1, /obj/item/stock_parts/console_screen = 1) + +/obj/item/circuitboard/destructive_analyzer + name = T_BOARD("destructive analyzer") + build_path = /obj/machinery/rnd/destructive_analyzer + board_type = new /datum/frame/frame_types/machine + origin_tech = list(TECH_MAGNET = 2, TECH_ENGINEERING = 2, TECH_DATA = 2) + req_components = list( + /obj/item/stock_parts/scanning_module = 1, + /obj/item/stock_parts/manipulator = 1, + /obj/item/stock_parts/micro_laser = 1) diff --git a/code/modules/research/tg/designs/boards/science.dm b/code/modules/research/tg/designs/boards/science.dm index a040ab9b11..fbf9bb1adf 100644 --- a/code/modules/research/tg/designs/boards/science.dm +++ b/code/modules/research/tg/designs/boards/science.dm @@ -196,3 +196,12 @@ RND_CATEGORY_MACHINE ) departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING + +/datum/design_techweb/board/destructive_analyzer + name = "destructive analyzer" + id = "destructive_analyzer" + build_path = /obj/item/circuitboard/destructive_analyzer + category = list( + RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_RESEARCH + ) + departmental_flags = DEPARTMENT_BITFLAG_SCIENCE 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 ddf2d3c192..74f5253e6e 100644 --- a/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm +++ b/code/modules/research/tg/experisci/experiment/handlers/experiment_handler.dm @@ -139,7 +139,7 @@ /datum/component/experiment_handler/proc/try_run_destructive_experiment(datum/source, list/scanned_atoms) SIGNAL_HANDLER var/atom/movable/our_scanner = parent - if (selected_experiment == null) + 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!")) diff --git a/code/modules/research/tg/experisci/experiment/instances/misc.dm b/code/modules/research/tg/experisci/experiment/instances/misc.dm index e62aec8227..91f1f62961 100644 --- a/code/modules/research/tg/experisci/experiment/instances/misc.dm +++ b/code/modules/research/tg/experisci/experiment/instances/misc.dm @@ -1,3 +1,17 @@ + +///Example of a destructive experiment. +/datum/experiment/scanning/random/artifact_destruction + name = "Artifact Analysis" + description = "Destroy some artifacts" + possible_types = list(/obj/item/research_sample/common) + total_requirement = 3 + traits = EXPERIMENT_TRAIT_DESTRUCTIVE + exp_tag = "Physical Experiment" + +/datum/experiment/scanning/random/artifact_destruction/serialize_progress_stage(atom/target, list/seen_instances) + return EXPERIMENT_PROG_INT("Destroy artifacts.", \ + traits & EXPERIMENT_TRAIT_DESTRUCTIVE ? scanned[target] : seen_instances.len, required_atoms[target]) + /datum/experiment/scanning/random/janitor_trash name = "Station Hygiene Inspection" description = "To learn how to clean, we must first learn what it is to have filth. We need you to scan some filth around the station." diff --git a/code/modules/research/tg/experisci/experiment/types/scanning.dm b/code/modules/research/tg/experisci/experiment/types/scanning.dm index 186f400c3e..b7cbd5309c 100644 --- a/code/modules/research/tg/experisci/experiment/types/scanning.dm +++ b/code/modules/research/tg/experisci/experiment/types/scanning.dm @@ -10,7 +10,7 @@ name = "Scanning Experiment" description = "Base experiment for scanning atoms" exp_tag = "Scan" - allowed_experimentors = list(/obj/item/experi_scanner) + allowed_experimentors = list(/obj/item/experi_scanner, /obj/machinery/rnd/destructive_analyzer) performance_hint = "Perform scanning experiments using a handheld experi-scanner." /// The typepaths and number of atoms that must be scanned var/list/required_atoms = list() diff --git a/code/modules/research/tg/machinery/destructive_analyzer.dm b/code/modules/research/tg/machinery/destructive_analyzer.dm new file mode 100644 index 0000000000..d568198044 --- /dev/null +++ b/code/modules/research/tg/machinery/destructive_analyzer.dm @@ -0,0 +1,319 @@ +///The 'ID' for deconstructing items for Research points instead of nodes. +#define DESTRUCTIVE_ANALYZER_DESTROY_POINTS "research_points" + +/* +Destructive Analyzer + +It is used to destroy hand-held objects and advance technological research. Used to perform /datum/experiment/physical/destructive_analysis experiments. +*/ + +/obj/machinery/rnd/destructive_analyzer + name = "destructive analyzer" + icon_state = "d_analyzer" + var/decon_mod = 0 + circuit = /obj/item/circuitboard/destructive_analyzer + use_power = USE_POWER_IDLE + idle_power_usage = 30 + active_power_usage = 2500 + var/rped_recycler_ready = TRUE + var/datum/component/remote_materials/rmat + +/obj/machinery/rnd/destructive_analyzer/Initialize(mapload) + rmat = AddComponent( + /datum/component/remote_materials, \ + mapload, \ + mat_container_flags = MATCONTAINER_NO_INSERT \ + ) + + //Destructive analysis + var/static/list/destructive_signals = list( + COMSIG_MACHINERY_DESTRUCTIVE_SCAN = TYPE_PROC_REF(/datum/component/experiment_handler, try_run_destructive_experiment), + ) + + AddComponent(/datum/component/experiment_handler, \ + allowed_experiments = list(/datum/experiment/scanning),\ + config_flags = EXPERIMENT_CONFIG_ALWAYS_ACTIVE|EXPERIMENT_CONFIG_SILENT_FAIL,\ + experiment_signals = destructive_signals, \ + ) + . = ..() + default_apply_parts() + +/obj/machinery/rnd/destructive_analyzer/Destroy() + rmat = null + . = ..() + +/obj/machinery/rnd/destructive_analyzer/RefreshParts() + var/T = 0 + for(var/obj/item/stock_parts/S in component_parts) + T += S.rating + T *= 0.1 + decon_mod = clamp(T, 0, 1) + +/obj/machinery/rnd/destructive_analyzer/update_icon() + var/current_item = loaded_item?.resolve() + if(panel_open) + icon_state = "d_analyzer_t" + else if(current_item) + icon_state = "d_analyzer_l" + else + icon_state = "d_analyzer" + +/obj/machinery/rnd/destructive_analyzer/attackby(var/obj/item/O as obj, var/mob/user as mob) + if(busy) + to_chat(user, span_notice("\The [src] is busy right now.")) + return + if(default_deconstruction_screwdriver(user, O)) + return + if(default_deconstruction_crowbar(user, O)) + return + if(default_part_replacement(user, O)) + return + if(!panel_open) + var/current_item = loaded_item?.resolve() + if(current_item) + to_chat(user, span_notice("There is something already loaded into \the [src].")) + else + if(isrobot(user)) //Don't put your module items in there! + return + if(is_type_in_list(O, GLOB.item_deconstruction_blacklist)) + to_chat(user, span_notice("The machine rejects \the [O]!")) + return + if((O.item_flags & DROPDEL) || (O.item_flags & NOSTRIP)) + to_chat(user, span_notice("The machine rejects \the [O]!")) + return + if(O.tethered_host_item) + to_chat(user, span_notice("The machine rejects \the [O]!")) + return + if(LAZYLEN(O.contents)) + to_chat(user, span_notice("The machine rejects \the [O]! You need to clear it of all items first!")) + return + busy = TRUE + loaded_item = WEAKREF(O) + user.drop_item() + O.forceMove(src) + SStgui.update_uis(src) + to_chat(user, span_notice("You add \the [O] to \the [src].")) + flick("d_analyzer_la", src) + addtimer(CALLBACK(src, PROC_REF(analyze_finish)), 1 SECONDS, TIMER_DELETE_ME) + return TRUE + // Handle signal to remote_materials so we can link the DA to the silo + . = ..() + +/obj/machinery/rnd/destructive_analyzer/proc/analyze_finish() + SHOULD_NOT_OVERRIDE(TRUE) + PRIVATE_PROC(TRUE) + update_icon() + reset_busy() + +/////////////////////////////////////////////////////////////////////////////////////////////////////// +// RPED recycling +/////////////////////////////////////////////////////////////////////////////////////////////////////// +/obj/machinery/rnd/destructive_analyzer/MouseDrop_T(atom/dropping, mob/living/user) + if(istype(dropping, /obj/item/storage/part_replacer)) + var/obj/item/storage/part_replacer/replacer = dropping + replacer.hide_from(user) + if(!rped_recycler_ready) + to_chat(user, span_notice("\The [src]'s stock parts recycler isn't ready yet.")) + return FALSE + + // We want the lowest-part tier rating in the RPED so we only recycle the lowest-tier parts. + var/lowest_rating = INFINITY + for(var/obj/item/B in replacer.contents) + if(B.rped_rating() < lowest_rating) + lowest_rating = B.rped_rating() + if(lowest_rating == INFINITY) + atom_say("Mass part deconstruction attempt canceled - no valid parts for recycling detected.") + return FALSE + // Sending salvaged materials to the silo + var/datum/component/material_container/materials = get_silo_material_container_datum(TRUE) + if(!materials) + return FALSE + for(var/obj/item/B in replacer.contents) + if(B.rped_rating() > lowest_rating) + continue + materials.insert_item(B, decon_mod, src) + // Feedback + playsound(get_turf(src), 'sound/machines/click.ogg', 50, 1) + rped_recycler_ready = FALSE + addtimer(CALLBACK(src, PROC_REF(rped_ready)), 5 SECONDS, TIMER_DELETE_ME) + to_chat(user, span_notice("You deconstruct all the parts of rating [lowest_rating] in [replacer] with [src].")) + return TRUE + . = ..() + +/obj/machinery/rnd/destructive_analyzer/proc/rped_ready() + PRIVATE_PROC(TRUE) + SHOULD_NOT_OVERRIDE(TRUE) + + rped_recycler_ready = TRUE + playsound(get_turf(src), 'sound/machines/chime.ogg', 50, 1) + +/obj/machinery/rnd/destructive_analyzer/proc/get_silo_material_container_datum(verbose) + var/datum/component/material_container/materials = rmat.mat_container + if(!materials) + if(verbose) + atom_say("No access to material storage, please contact the quartermaster.") + return null + if(rmat.on_hold()) + if(verbose) + atom_say("Mineral access is on hold, please contact the quartermaster.") + return null + return materials + +/////////////////////////////////////////////////////////////////////////////////////////////////////// +// Handling deconstruction +/////////////////////////////////////////////////////////////////////////////////////////////////////// + +/obj/machinery/rnd/destructive_analyzer/attack_hand(mob/user as mob) + tgui_interact(user) + +/obj/machinery/rnd/destructive_analyzer/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "DestructiveAnalyzer") + ui.open() + +/obj/machinery/rnd/destructive_analyzer/tgui_data(mob/user) + var/list/data = list() + data["server_connected"] = !!stored_research + data["node_data"] = null + var/obj/item/current_item = loaded_item?.resolve() + if(current_item) + data["item_icon"] = icon2base64(getFlatIcon(image(icon = current_item.icon, icon_state = current_item.icon_state), no_anim = TRUE)) + data["indestructible"] = is_type_in_list(current_item, GLOB.item_deconstruction_blacklist) + data["loaded_item"] = current_item + data["already_deconstructed"] = !!stored_research.deconstructed_items[current_item.type] + var/list/points = techweb_item_point_check(current_item) + data["recoverable_points"] = techweb_point_display_generic(points) + + var/list/boostable_nodes = techweb_item_unlock_check(current_item) + for(var/id in boostable_nodes) + var/datum/techweb_node/unlockable_node = SSresearch.techweb_node_by_id(id) + var/list/node_data = list() + node_data["node_name"] = unlockable_node.display_name + node_data["node_id"] = unlockable_node.id + node_data["node_hidden"] = !!stored_research.hidden_nodes[unlockable_node.id] + data["node_data"] += list(node_data) + else + data["loaded_item"] = null + return data + +/obj/machinery/rnd/destructive_analyzer/tgui_static_data(mob/user) + var/list/data = list() + data["research_point_id"] = DESTRUCTIVE_ANALYZER_DESTROY_POINTS + return data + +/obj/machinery/rnd/destructive_analyzer/tgui_act(action, params, datum/tgui/ui) + if(..()) + return TRUE + + var/mob/user = usr + var/current_item = loaded_item?.resolve() + switch(action) + if("eject_item") + if(busy) + balloon_alert(user, "already busy!") + return TRUE + if(current_item) + unload_item() + return TRUE + if("deconstruct") + if(!user_try_decon_id(params["deconstruct_id"])) + balloon_alert(user, "analysis failed!") + return TRUE + +///Drops the loaded item where it can and nulls it. +/obj/machinery/rnd/destructive_analyzer/proc/unload_item() + var/obj/item/current_item = loaded_item?.resolve() + if(!current_item) + loaded_item = null + return FALSE + //playsound(loc, 'sound/machines/terminal/terminal_insert_disc.ogg', 30, FALSE) + current_item.forceMove(drop_location()) + loaded_item = null + update_icon() + return TRUE + +/** + * Destroys an item by going through all its contents (including itself) and calling destroy_item_individual + * Args: + * gain_research_points - Whether deconstructing each individual item should check for research points to boost. + */ +/obj/machinery/rnd/destructive_analyzer/proc/destroy_item(gain_research_points = FALSE) + var/obj/item/current_item = loaded_item?.resolve() + if(!current_item || QDELETED(src)) + return FALSE + //flick("[base_icon_state]_process", src) + busy = TRUE + addtimer(CALLBACK(src, PROC_REF(reset_busy)), 2.4 SECONDS) + use_power(active_power_usage) + //Failsafe. + for(var/atom/movable/AM in current_item.contents) + AM.forceMove(get_turf(src)) + playsound(src, 'sound/machines/destructive_analyzer.ogg', 50, 1) + SEND_SIGNAL(src, COMSIG_MACHINERY_DESTRUCTIVE_SCAN, current_item) + destroy_item_individual(current_item, gain_research_points) + loaded_item = null + update_icon() + return TRUE + +/** + * Destroys the individual provided item + * Args: + * thing - The thing being destroyed. Generally an object, but it can be a mob too, such as intellicards and pAIs. + * gain_research_points - Whether deconstructing this should give research points to the stored techweb, if applicable. + */ +/obj/machinery/rnd/destructive_analyzer/proc/destroy_item_individual(obj/item/thing, gain_research_points = FALSE) + if(isliving(thing)) + var/mob/living/mob_thing = thing + var/turf/turf_to_dump_to = get_turf(src) + log_and_message_admins("made an attempt to kill [mob_thing] in a destructive analyzer was made at [ADMIN_VERBOSEJMP(turf_to_dump_to)]") + visible_message(span_warning("A loud buzz sounds out from \the [src] as it rejects and spits out \the [mob_thing]!")) + mob_thing.forceMove(turf_to_dump_to) + return + //Safety. + if(is_type_in_list(thing, GLOB.item_deconstruction_blacklist)) + var/turf/turf_to_dump_to = get_turf(src) + log_and_message_admins("made an attempt to destroy [thing] in a destructive analyzer was made at [ADMIN_VERBOSEJMP(turf_to_dump_to)]") + visible_message(span_warning("A loud buzz sounds out from \the [src] as it rejects and spits out \the [thing]!")) + thing.forceMove(turf_to_dump_to) + return + var/list/point_value = techweb_item_point_check(thing) + //If it has a point value and we haven't deconstructed it OR we've deconstructed it but it's a repeatable. + if(point_value && (!stored_research.deconstructed_items[thing.type] || (stored_research.deconstructed_items[thing.type] && (thing.type in SSresearch.techweb_repeatable_items)))) + stored_research.deconstructed_items[thing.type] = TRUE + stored_research.add_point_list(point_value) + + //Finally, let's add it to the material silo, if applicable. + var/datum/component/material_container/materials = get_silo_material_container_datum(FALSE) + materials.insert_item(thing, decon_mod, src, FALSE) + qdel(thing) + +/** + * Attempts to destroy the loaded item using a provided research id. + * Args: + * id - The techweb ID node that we're meant to unlock if applicable. + */ +/obj/machinery/rnd/destructive_analyzer/proc/user_try_decon_id(id) + var/obj/item/current_item = loaded_item?.resolve() + if(!istype(current_item)) + return FALSE + if(LAZYLEN(current_item.contents)) + visible_message(span_notice("A warning blares from \the [src]: The [current_item] still has items inside it!")) + return FALSE + if(isnull(id)) + return FALSE + + if(id == DESTRUCTIVE_ANALYZER_DESTROY_POINTS) + if(!destroy_item(gain_research_points = TRUE)) + return FALSE + return TRUE + + var/datum/techweb_node/node_to_discover = SSresearch.techweb_node_by_id(id) + if(!istype(node_to_discover)) + return FALSE + if(!destroy_item()) + return FALSE + stored_research.unhide_node(node_to_discover) + return TRUE + +#undef DESTRUCTIVE_ANALYZER_DESTROY_POINTS diff --git a/code/modules/research/tg/rdmachines.dm b/code/modules/research/tg/rdmachines.dm index 4b25c8cbce..433c69d31b 100644 --- a/code/modules/research/tg/rdmachines.dm +++ b/code/modules/research/tg/rdmachines.dm @@ -14,7 +14,7 @@ ///Ref to global science techweb. var/datum/techweb/stored_research ///The item loaded inside the machine, used by experimentors and destructive analyzers only. - var/obj/item/loaded_item + var/datum/weakref/loaded_item /obj/machinery/rnd/Initialize(mapload) . = ..() @@ -59,6 +59,8 @@ return ..() /obj/machinery/rnd/dismantle() - if(loaded_item) - loaded_item.forceMove(drop_location()) + var/obj/item/our_item = loaded_item?.resolve() + if(our_item) + our_item.forceMove(drop_location()) + loaded_item = null . = ..() diff --git a/code/modules/research/tg/techwebs/nodes/research_nodes.dm b/code/modules/research/tg/techwebs/nodes/research_nodes.dm index 129b7c0cc9..cd63ee0af8 100644 --- a/code/modules/research/tg/techwebs/nodes/research_nodes.dm +++ b/code/modules/research/tg/techwebs/nodes/research_nodes.dm @@ -12,7 +12,7 @@ // "rdservercontrol", // "doppler_array", // "experimentor", - // "destructive_analyzer", + "destructive_analyzer", // "destructive_scanner", // "laptop", // "portadrive_basic", diff --git a/package.json b/package.json index 7909974e77..3c200374fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "@biomejs/biome": "^2.2.5", + "@biomejs/biome": "^2.2.6", "prettier": "^3.6.2" }, "scripts": { diff --git a/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx b/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx new file mode 100644 index 0000000000..691820535a --- /dev/null +++ b/tgui/packages/tgui/interfaces/DestructiveAnalyzer.tsx @@ -0,0 +1,159 @@ +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Box, + Button, + Image, + NoticeBox, + Section, + Stack, +} from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; + +type Data = { + server_connected: BooleanLike; + loaded_item: string; + item_icon: string; + indestructible: BooleanLike; + already_deconstructed: BooleanLike; + recoverable_points: string; + node_data: NodeData[] | null; + research_point_id: string; +}; + +type NodeData = { + node_name: string; + node_id: string; + node_hidden: BooleanLike; +}; + +export const DestructiveAnalyzer = () => { + const { act, data } = useBackend(); + const { + server_connected, + indestructible, + loaded_item, + item_icon, + already_deconstructed, + recoverable_points, + research_point_id, + node_data, + } = data; + if (!server_connected) { + return ( + + + + Not connected to a server. Please sync one using a multitool. + + + + ); + } + if (!loaded_item) { + return ( + + + + No item loaded!
+ Put any item inside to see what it's capable of! +
+
+
+ ); + } + return ( + + + + +
act('eject_item')} + /> + } + > + +
+
+ +
+ + {!!indestructible && ( + + + This item can't be deconstructed! + + + )} + {!indestructible && !!recoverable_points && ( + <> + + + Research points from deconstruction + + + + {recoverable_points} + + + )} + + + {!indestructible && ( + + + act('deconstruct', { + deconstruct_id: research_point_id, + }) + } + > + Deconstruct + + + )} + {node_data?.map((node) => ( + + + act('deconstruct', { deconstruct_id: node.node_id }) + } + > + {node.node_name} + + + ))} + + + +
+
+
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/ResearchConsole/index.tsx b/tgui/packages/tgui/interfaces/ResearchConsole/index.tsx index c2a0ad95bf..f37a98f675 100644 --- a/tgui/packages/tgui/interfaces/ResearchConsole/index.tsx +++ b/tgui/packages/tgui/interfaces/ResearchConsole/index.tsx @@ -13,7 +13,7 @@ import { import { ConstructorEnum, type Data, Tab } from './data'; import { Constructor } from './pages/Constructor'; import { DesignList } from './pages/DesignList'; -import { DestructiveAnalyzer } from './pages/DestructiveAnalyzer'; +import { DestructiveAnalyzer } from '../DestructiveAnalyzer'; import { LockScreen } from './pages/LockScreen'; import { Misc } from './pages/Misc'; import { ResearchList } from './pages/ResearchList'; diff --git a/tgui/packages/tgui/interfaces/ResearchConsole/pages/DestructiveAnalyzer.tsx b/tgui/packages/tgui/interfaces/ResearchConsole/pages/DestructiveAnalyzer.tsx deleted file mode 100644 index 42ef1d0a1f..0000000000 --- a/tgui/packages/tgui/interfaces/ResearchConsole/pages/DestructiveAnalyzer.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useBackend } from 'tgui/backend'; -import { Box, Button, Icon, Section, Stack, Table } from 'tgui-core/components'; -import { classes } from 'tgui-core/react'; - -import type { Data, LinkedDestroyer } from '../data'; - -export const DestructiveAnalyzer = (props) => { - const { act, data } = useBackend(); - - let inner; - - if (!data.linked_destroy) { - inner = No Destructive Analyzer linked.; - } else if (!data.linked_destroy.loaded_item) { - inner = No item loaded.; - } else { - inner = ; - } - - return ( -
- {inner} -
- ); -}; - -const DestroyButton = (props: { destroyer: LinkedDestroyer }) => { - const { act } = useBackend(); - const { destroyer } = props; - - const [buttonPressed, setButtonPressed] = useState(false); - - return ( - - -
act('eject_item')}>Eject} - fontSize={1.2} - mt={2} - ml={1} - backgroundColor="#0a0a0a" - height="95%" - style={{ borderRadius: '5px' }} - > - - {destroyer.loaded_item} - - - - Tech Levels - Name - - Level - - - Current - - - {destroyer.origin_tech.map((tech) => ( - - {tech.name} - {tech.level} - - {tech.current} - {tech.level >= tech.current ? ( - - ) : null} - - - ))} -
-
-
- - - - {/* https://codepen.io/ovr/pen/yLXBbB */} - - - - -
- ); -}; - -export const AnimatedArrows = (props: { on: boolean } & any) => { - const { on, ...rest } = props; - - const [activeArrow, setActiveArrow] = useState(0); - - // Lower to make it animate faster - const SPEED = 200; - - useEffect(() => { - const id = setInterval(() => { - setActiveArrow((arrow) => (arrow + 1) % 3); - }, SPEED); - return () => clearInterval(id); - }, []); - - return ( - - - - - - ); -}; diff --git a/vorestation.dme b/vorestation.dme index eab17764fb..5f000107ce 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -4343,6 +4343,7 @@ #include "code\modules\research\tg\machinery\_production.dm" #include "code\modules\research\tg\machinery\circuit_imprinter.dm" #include "code\modules\research\tg\machinery\departmental_protolathe.dm" +#include "code\modules\research\tg\machinery\destructive_analyzer.dm" #include "code\modules\research\tg\machinery\mech_fabricator.dm" #include "code\modules\research\tg\machinery\mech_prosfab.dm" #include "code\modules\research\tg\machinery\protolathe.dm"