From 65bd5e581a47593a48ee05026b7cfa96fef89707 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Tue, 14 Oct 2025 13:20:15 -0500
Subject: [PATCH] Beaker panel TGUI / At least makes it functional (#93398)
## About The Pull Request
Makes Beaker Panel a TGUI so it actually works
There's a lot more I want to do to this UI, namely UX and more features
(like changing grenade types) but I figured I should PR it now that it's
bare minimum functional so I don't get eaten by scope creep
Fixes #89954
## Changelog
:cl: Melbert
admin: Spawn-reagent-container works again, and it's a TGUI
/:cl:
---
code/modules/admin/topic.dm | 3 -
code/modules/admin/verbs/beakerpanel.dm | 395 ++++--------------
.../antagonists/blob/blobstrains/_reagent.dm | 2 -
.../reagents/drinks/drink_reagents.dm | 2 +-
tgui/packages/tgui/interfaces/BeakerPanel.tsx | 321 ++++++++++++++
5 files changed, 406 insertions(+), 317 deletions(-)
create mode 100644 tgui/packages/tgui/interfaces/BeakerPanel.tsx
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 054a507b6a2..90eaecb2124 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1272,9 +1272,6 @@
var/ban_id = href_list["unbanlog"]
ban_log(ban_id)
- else if(href_list["beakerpanel"])
- beaker_panel_act(href_list)
-
else if(href_list["reloadpolls"])
GLOB.polls.Cut()
GLOB.poll_options.Cut()
diff --git a/code/modules/admin/verbs/beakerpanel.dm b/code/modules/admin/verbs/beakerpanel.dm
index 7088fba92f1..94dcb7fb171 100644
--- a/code/modules/admin/verbs/beakerpanel.dm
+++ b/code/modules/admin/verbs/beakerpanel.dm
@@ -1,320 +1,93 @@
-/proc/reagentsforbeakers()
- . = list()
- for(var/t in subtypesof(/datum/reagent))
- var/datum/reagent/R = t
- . += list(list("id" = t, "text" = initial(R.name)))
+/datum/beaker_panel
- . = json_encode(.)
+/datum/beaker_panel/ui_state(mob/user)
+ return ADMIN_STATE(R_ADMIN)
-/proc/beakersforbeakers()
- . = list()
- for(var/t in subtypesof(/obj/item/reagent_containers))
- var/obj/item/reagent_containers/C = t
- . += list(list("id" = t, "text" = initial(C.name), "volume" = initial(C.volume)))
+/datum/beaker_panel/ui_close()
+ qdel(src)
- . = json_encode(.)
+/datum/beaker_panel/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "BeakerPanel")
+ ui.open()
-/datum/admins/proc/beaker_panel_act(list/href_list)
- switch (href_list["beakerpanel"])
- if ("spawncontainer")
- var/containerdata = json_decode(href_list["container"])
- var/obj/item/reagent_containers/container = beaker_panel_create_container(containerdata, get_turf(usr))
- usr.log_message("spawned a [container] containing [pretty_string_from_reagent_list(container.reagents.reagent_list)]", LOG_GAME)
- if ("spawngrenade")
- var/obj/item/grenade/chem_grenade/grenade = new(get_turf(usr))
- var/containersdata = json_decode(href_list["containers"])
- var/reagent_string
- for (var/i in 1 to 2)
- grenade.beakers += beaker_panel_create_container(containersdata[i], grenade)
- reagent_string += " ([grenade.beakers[i].name] [i] : " + pretty_string_from_reagent_list(grenade.beakers[i].reagents.reagent_list) + ");"
- grenade.stage_change(GRENADE_READY)
- var/grenadedata = json_decode(href_list["grenadedata"])
- switch (href_list["grenadetype"])
- if ("normal") // Regular cable coil-timed grenade
- var/det_time = text2num(grenadedata["grenade-timer"])
- if (det_time)
- grenade.det_time = det_time
- usr.log_message("spawned a [grenade] containing: [reagent_string]", LOG_GAME)
+/datum/beaker_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
-/datum/admins/proc/beaker_panel_prep_assembly(obj/item/assembly/towrap, grenade)
- var/obj/item/assembly/igniter/igniter = new
- igniter.secured = FALSE
- var/obj/item/assembly_holder/assholder = new(grenade)
- towrap.forceMove(assholder)
- igniter.forceMove(assholder)
- assholder.assemble(igniter, towrap, usr)
- assholder.master = grenade
- return assholder
+ var/mob/user = ui.user
+ switch(action)
+ if("spawn")
+ var/obj/created = spawn_container_from_data(user, params["spawn_info"])
+ user.log_message("spawned a [created] containing [pretty_string_from_reagent_list(created.reagents.reagent_list)]", LOG_ADMIN)
+ return TRUE
+ if("spawngrenade")
+ var/obj/item/grenade/chem_grenade/grenade = spawn_grenade_from_data(user, params["spawn_info"], params["grenade_info"])
+ var/log_string = list()
+ for(var/obj/beaker as anything in grenade.beakers)
+ log_string += pretty_string_from_reagent_list(beaker.reagents.reagent_list)
+ user.log_message("spawned a [grenade] containing [english_list(log_string)]", LOG_ADMIN)
+ return TRUE
-/datum/admins/proc/beaker_panel_create_container(list/containerdata, location)
- var/containertype = text2path(containerdata["container"])
- var/obj/item/reagent_containers/container = new containertype(location)
- var/datum/reagents/reagents = container.reagents
- for(var/datum/reagent/R in reagents.reagent_list) // clear the container of reagents
- reagents.remove_reagent(R.type,R.volume)
- for (var/list/item in containerdata["reagents"])
- var/datum/reagent/reagenttype = text2path(item["reagent"])
- var/amount = text2num(item["volume"])
- if ((reagents.total_volume + amount) > reagents.maximum_volume)
- reagents.maximum_volume = reagents.total_volume + amount
- reagents.add_reagent(reagenttype, amount)
+/datum/beaker_panel/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["reagents"] = list()
+ data["containers"] = list()
+
+ for(var/datum/reagent/reagent_type as anything in subtypesof(/datum/reagent))
+ if(!reagent_type::name)
+ continue
+ data["reagents"] += list(list("id" = reagent_type, "text" = reagent_type::name))
+
+ for(var/obj/item/reagent_containers/container_type as anything in subtypesof(/obj/item/reagent_containers))
+ if(!container_type::name)
+ continue
+ data["containers"] += list(list("id" = container_type, "text" = container_type::name, "volume" = container_type::volume))
+
+ return data
+
+/datum/beaker_panel/proc/spawn_container_from_data(mob/user, list/spawn_info)
+ var/container_type = text2path(spawn_info["container"])
+ var/list/container_reagents = list()
+ for(var/reagent_string, reagent_amount in spawn_info["reagents"])
+ container_reagents[text2path(reagent_string)] = text2num(reagent_amount)
+
+ return spawn_container(user, container_type, container_reagents)
+
+/datum/beaker_panel/proc/spawn_container(mob/user, container_type, list/container_reagents)
+ var/obj/item/reagent_containers/container = new container_type(user.drop_location())
+ container.reagents.maximum_volume = INFINITY
+ container.reagents.clear_reagents()
+ container.reagents.add_reagent_list(container_reagents)
+ container.reagents.maximum_volume = max(container.reagents.total_volume, initial(container.volume))
return container
+/datum/beaker_panel/proc/spawn_grenade_from_data(mob/user, list/all_spawn_info, list/grenade_info)
+ var/list/containers = list()
+ for(var/list/container_info as anything in all_spawn_info)
+ containers += spawn_container_from_data(user, container_info)
+
+ return spawn_grenade(user, containers, grenade_info)
+
+/datum/beaker_panel/proc/spawn_grenade(mob/user, list/beakers, list/grenade_info)
+ var/obj/item/grenade/chem_grenade/grenade = new(user.drop_location())
+ grenade.beakers = beakers
+ grenade.stage_change(GRENADE_READY)
+
+ for(var/obj/beaker as anything in grenade.beakers)
+ beaker.forceMove(grenade)
+
+ switch(grenade_info["detonation_type"])
+ if("normal")
+ var/det_time = text2num(grenade_info["detonation_timer"]) * 1 SECONDS
+ if(det_time)
+ grenade.det_time = det_time
+
+ return grenade
+
ADMIN_VERB(beaker_panel, R_SPAWN, "Spawn Reagent Container", "Spawn a reagent container.", ADMIN_CATEGORY_EVENTS)
- var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/namespaced/common)
- asset_datum.send(user)
- //Could somebody tell me why this isn't using the browser datum, given that it copypastes all of browser datum's html
- // fuck if I know, but im not touching it
- var/dat = {"
-
-
-