Beaker panel TGUI / At least makes it functional (#93398)

## About The Pull Request

Makes Beaker Panel a TGUI so it actually works

<img width="787" height="295" alt="image"
src="https://github.com/user-attachments/assets/343056d2-f059-473d-a1eb-5604d2c3a38e"
/>

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

🆑 Melbert
admin: Spawn-reagent-container works again, and it's a TGUI
/🆑
This commit is contained in:
MrMelbert
2025-10-14 13:20:15 -05:00
committed by GitHub
parent 39de5b2b9d
commit 65bd5e581a
5 changed files with 406 additions and 317 deletions
-3
View File
@@ -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()
+84 -311
View File
@@ -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 = {"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<link rel='stylesheet' type='text/css' href='[SSassets.transport.get_asset_url("common.css")]'>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/js/select2.full.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.7/css/select2.min.css">
<script type="text/javascript" src="https://kit.fontawesome.com/8d67455b41.js"></script>
<style>
.select2-search { color: #40628a; background-color: #272727; }
.select2-results { color: #40628a; background-color: #272727; }
.select2-selection { border-radius: 0px !important; }
ul {
list-style-type: none; /* Remove bullets */
padding: 0; /* Remove padding */
margin: 0; /* Remove margins */
}
ul li {
margin-top: -1px; /* Prevent double borders */
padding: 12px; /* Add some padding */
color: #ffffff;
text-decoration: none;
background: #40628a;
border: 1px solid #161616;
margin: 0 2px 0 0;
cursor:default;
}
.remove-reagent {
background-color: #d03000;
}
.container-control {
width: 48%;
float: left;
padding-right: 10px;
}
.reagent > div, .reagent-div {
float: right;
width: 200px;
}
input.reagent {
width: 50%;
}
.grenade-data {
display: inline-block;
}
</style>
<script>
window.onload=function(){
var reagents = [reagentsforbeakers()];
var containers = [beakersforbeakers()];
$('select\[name="containertype"\]').select2({
data: containers,
escapeMarkup: noEscape,
templateResult: formatContainer,
templateSelection: textSelection,
width: "300px"
});
$('.select-new-reagent').select2({
data: reagents,
escapeMarkup: noEscape,
templateResult: formatReagent,
templateSelection: textSelection
});
$('.remove-reagent').click(function() { $(this).parents('li').remove(); });
$('#spawn-grenade').click(function() {
var containers = $('div.container-control').map(function() {
var type = $(this).children('select\[name=containertype\]').select2("data")\[0\].id;
var reagents = $(this).find("li.reagent").map(function() {
return { "reagent": $(this).data("type"), "volume": $(this).find('input').val()};
}).get();
return {"container": type, "reagents": reagents };
}).get();
var grenadeType = $('#grenade-type').val()
var grenadeData = {};
$('.grenade-data.'+grenadeType).find(':input').each(function() {
var ret = {};
grenadeData\[$(this).attr('name')\] = $(this).val();
});
$.ajax({
url: '',
data: {
"_src_": "holder",
"admin_token": "[RawHrefToken()]",
"beakerpanel": "spawngrenade",
"containers": JSON.stringify(containers),
"grenadetype": grenadeType,
"grenadedata": JSON.stringify(grenadeData)
}
});
});
$('.spawn-container').click(function() {
var container = $(this).parents('div.container-control')\[0\];
var type = $(container).children('select\[name=containertype\]').select2("data")\[0\].id;
var reagents = $(container).find("li.reagent").map(function() {
return { "reagent": $(this).data("type"), "volume": $(this).find('input').val()};
}).get();
$.ajax({
url: '',
data: {
"_src_": "holder",
"admin_token": "[RawHrefToken()]",
"beakerpanel": "spawncontainer",
"container": JSON.stringify({"container": type, "reagents": reagents }),
}
});
});
$('.add-reagent').click(function() {
var select = $(this).parents('li').children('select').select2("data")\[0\];
var amount = $(this).parent().children('input').val();
addReagent($(this).parents('ul'), select.id, select.text, amount)
})
$('.export-reagents').click(function() {
var container = $(this).parents('div.container-control')\[0\];
var ret = \[\];
var reagents = $(container).find("li.reagent").each(function() {
var reagentname = $(this).contents().filter(function(){ return this.nodeType == 3; })\[0\].nodeValue.toLowerCase().replace(/\\W/g, '');
ret.push(reagentname+"="+$(this).find('input').val());
});
prompt("Copy this value", ret.join(';'));
});
$('.import-reagents').click(function() {
var macro = prompt("Enter a chemistry macro", "");
var parts = macro.split(';');
var container = $(this).parents('div.container-control')\[0\];
var ul = $(container).find("ul");
$(parts).each(function() {
var reagentArr = this.split('=');
var thisReagent = $(reagents).filter(function() { return this.text.toLowerCase().replace(/\\W/g, '') == reagentArr\[0\] })\[0\];
addReagent(ul, thisReagent.id, thisReagent.text, reagentArr\[1\]);
});
});
$('#grenade-type').change(function() {
$('.grenade-data').hide();
$('.grenade-data.'+$(this).val()).show();
})
function addReagent(ul, reagentType, reagentName, amount)
{
$('<li class="reagent" data-type="'+reagentType+'">'+reagentName+'<div><input class="reagent" value="'+amount+'" />&nbsp;&nbsp;<button class="remove-reagent"><i class="far fa-trash-alt"></i>&nbsp;Remove</button></div></li>').insertBefore($(ul).children('li').last());
$(ul).children('li').last().prev().find('button').click(function() { $(this).parents('li').remove(); });
}
function textSelection(selection)
{
return selection.text;
}
function noEscape(markup)
{
return markup;
}
function formatReagent(result)
{
return '<span>'+result.text+'</span><br/><span><small>'+result.id+'</small></span>';
}
function formatContainer(result)
{
return '<span>'+result.text+" ("+result.volume+'u)</span><br/><span><small>'+result.id+'</small></span>';
}
}
</script>
</head>
<body scroll=auto>
<div class='uiWrapper'>
<div class='uiTitleWrapper'><div class='uiTitle'><tt>Beaker panel</tt></div></div>
<div class='uiContent'>
<div class="width: 100%">
<button id="spawn-grenade">
<i class="fas fa-bomb"></i>&nbsp;Spawn grenade
</button>
<label for="grenade-type">Grenade type: </label>
<select id="grenade-type">
<option value="normal">Normal</option>
</select>
<div class="grenade-data normal">
</div>
<br />
<small>note: beakers recommended, other containers may have issues</small>
</div>
"}
for (var/i in 1 to 2 )
dat += {"
<div class="container-control">
<h4>
Container [i]:
</h4>
<br />
<label for="beaker[i]type">Container type</label>
<select name="containertype" id="beaker[i]type"></select>
<br />
<br />
<div>
<button class="spawn-container">
<i class="fas fa-cog"></i>&nbsp;Spawn
</button>
&nbsp;&nbsp;&nbsp;
<button class="import-reagents">
<i class="fas fa-file-import"></i>&nbsp;Import
</button>
&nbsp;&nbsp;&nbsp;
<button class="export-reagents">
<i class="fas fa-file-export"></i>&nbsp;Export
</button>
</div>
<ul>
<li>
<select class="select-new-reagent"></select><div class="reagent-div"><input style="width: 50%" type="text" name="newreagent" value="40" />&nbsp;&nbsp;<button class="add-reagent">
<i class="fas fa-plus"></i>&nbsp;Add
</button>
</div>
</li>
</ul>
</div>
"}
dat += {"
</div>
</div>
</body>
</html>
"}
user << browse(dat, "window=beakerpanel;size=1100x720")
var/datum/beaker_panel/panel = new
panel.ui_interact(user.mob)