From 5d0145fceefb2078e9cdac630166d326c421c8ff Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:38:38 +0100 Subject: [PATCH] [MIRROR] New Experimentor UI (#26324) * New Experimentor UI (#81157) ![Demo](https://github.com/tgstation/tgstation/assets/137328283/de57a003-62ce-4296-a09d-1b5edecdc139) ## About The Pull Request This project rewrites experimentor UI from browser to TGUI. Refactored `ejectItem`, moved experiment handling logic out of `ui_act` and reordered proc calls, thus fixing a null reference runtime during attempt to unlock any techweb node. In addition, removed `checkCircumstances` due to being unused. ## Why It's Good For The Game The New UI is more responsive and displays much more information in a more organized fashion. Also, it removes `updateUsrDialog` and helps to bring https://hackmd.io/XLt5MoRvRxuhFbwtk4VAUA to a closure. ## Changelog :cl: refactor: refactored experimentor UI to TGUI. /:cl: * New Experimentor UI --------- Co-authored-by: Interception&? <137328283+intercepti0n@users.noreply.github.com> --- code/modules/research/experimentor.dm | 200 +++++++------- .../tgui/interfaces/Experimentator.tsx | 246 ++++++++++++++++++ 2 files changed, 341 insertions(+), 105 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/Experimentator.tsx diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm index 404a953595d..fff52087a35 100644 --- a/code/modules/research/experimentor.dm +++ b/code/modules/research/experimentor.dm @@ -26,7 +26,7 @@ density = TRUE use_power = IDLE_POWER_USE circuit = /obj/item/circuitboard/machine/experimentor - interaction_flags_machine = INTERACT_MACHINE_WIRES_IF_OPEN|INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_OPEN_SILICON|INTERACT_MACHINE_SET_MACHINE + interaction_flags_machine = INTERACT_MACHINE_WIRES_IF_OPEN|INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_OPEN_SILICON var/recentlyExperimented = 0 /// Weakref to the first ian we can find at init var/datum/weakref/tracked_ian_ref @@ -117,14 +117,6 @@ if(in_range(user, src) || isobserver(user)) . += span_notice("The status display reads: Malfunction probability reduced by [malfunction_probability_coeff]%.
Cooldown interval between experiments at [resetTime*0.1] seconds.") -/obj/machinery/rnd/experimentor/proc/checkCircumstances(obj/item/O) - //snowflake check to only take "made" bombs - if(istype(O, /obj/item/transfer_valve)) - var/obj/item/transfer_valve/T = O - if(!T.tank_one || !T.tank_two || !T.attached_device) - return FALSE - return TRUE - /obj/machinery/rnd/experimentor/attackby(obj/item/weapon, mob/living/user, params) if(user.combat_mode) return ..() @@ -142,117 +134,115 @@ ejectItem() return ..(O) -/obj/machinery/rnd/experimentor/ui_interact(mob/user) - var/list/dat = list("
") - if(loaded_item) - dat += "Loaded Item: [loaded_item]" +/obj/machinery/rnd/experimentor/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new (user, src, "Experimentator") + ui.open() - dat += "
Available tests:" - dat += "Poke" - dat += "Irradiate" - dat += "Gas" - dat += "Burn" - dat += "Freeze" - dat += "Destroy
" - if(istype(loaded_item,/obj/item/relic)) - dat += "Discover" - dat += "Eject" - var/list/listin = techweb_item_unlock_check(src) - if(listin) - var/list/output = list("Research Boost Data:") - var/list/res = list("Already researched:") - for(var/node_id in listin) - var/datum/techweb_node/N = SSresearch.techweb_node_by_id(node_id) - var/str = "[N.display_name]: [listin[N]] points." - var/datum/techweb/science_web = locate(/datum/techweb/science) in SSresearch.techwebs - if(science_web.researched_nodes[N.id]) - res += str - if(science_web.visible_nodes[N.id]) //JOY OF DISCOVERY! - output += str - output += res - dat += output - else - dat += "Nothing loaded." - dat += "Refresh" - dat += "Close
" - var/datum/browser/popup = new(user, "experimentor","Experimentor", 700, 400, src) - popup.set_content(dat.Join("
")) - popup.open() - onclose(user, "experimentor") +/obj/machinery/rnd/experimentor/ui_data(mob/user) + var/list/data = list() -/obj/machinery/rnd/experimentor/Topic(href, href_list) - if(..()) + data["hasItem"] = !!loaded_item + data["isOnCooldown"] = recentlyExperimented + data["isServerConnected"] = !!stored_research + + if(!isnull(loaded_item)) + var/list/item_data = list() + + item_data["name"] = loaded_item.name + item_data["icon"] = icon2base64(getFlatIcon(loaded_item, no_anim = TRUE)) + item_data["isRelic"] = istype(loaded_item, /obj/item/relic) + + item_data["associatedNodes"] = list() + var/list/unlockable_nodes = techweb_item_unlock_check(loaded_item) + for(var/node_id in unlockable_nodes) + var/datum/techweb_node/node = SSresearch.techweb_node_by_id(node_id) + + item_data["associatedNodes"] += list(list( + "name" = node.display_name, + "isUnlocked" = !(node_id in stored_research.hidden_nodes), + )) + + data["loadedItem"] = item_data + + return data + +/obj/machinery/rnd/experimentor/ui_act(action, list/params) + . = ..() + if(.) return - usr.set_machine(src) - var/scantype = href_list["function"] - var/obj/item/process = locate(href_list["item"]) in src + switch(action) + if("eject") + ejectItem() + return TRUE - if(href_list["close"]) - usr << browse(null, "window=experimentor") + if("experiment") + var/reaction = text2num(params["id"]) + if(isnull(reaction)) + return + + try_perform_experiment(reaction) + return TRUE + +/obj/machinery/rnd/experimentor/proc/ejectItem(delete = FALSE) + if(isnull(loaded_item)) return - else if(scantype == "eject") - ejectItem() - else if(scantype == "refresh") - updateUsrDialog() - else - if(recentlyExperimented) - to_chat(usr, span_warning("[src] has been used too recently!")) - else if(!loaded_item) - to_chat(usr, span_warning("[src] is not currently loaded!")) - else if(!process || process != loaded_item) //Interface exploit protection (such as hrefs or swapping items with interface set to old item) - to_chat(usr, span_danger("Interface failure detected in [src]. Please try again.")) - else - var/dotype - if(text2num(scantype) == SCANTYPE_DISCOVER) - dotype = SCANTYPE_DISCOVER - else - dotype = matchReaction(process,scantype) - experiment(dotype,process) - use_power(750) - if(dotype != FAIL) - var/list/nodes = techweb_item_unlock_check(process) - var/picked = pick_weight(nodes) //This should work. - stored_research.unhide_node(SSresearch.techweb_node_by_id(picked)) - updateUsrDialog() -/obj/machinery/rnd/experimentor/proc/matchReaction(matching,reaction) - var/obj/item/D = matching - if(D) - var/list/item_reactions = item_reactions() - if(item_reactions.Find("[D.type]")) - var/tor = item_reactions["[D.type]"] - if(tor == text2num(reaction)) - return tor - else - return FAIL - else - return FAIL - else + if(delete) + QDEL_NULL(loaded_item) + return + + var/atom/drop_atom = get_step(src, EAST) || drop_location() + if(cloneMode) + visible_message(span_notice("A duplicate of \the [loaded_item] pops out!")) + new loaded_item.type(drop_atom) + cloneMode = FALSE + return + + loaded_item.forceMove(drop_atom) + loaded_item = null + +/obj/machinery/rnd/experimentor/proc/match_reaction(obj/item/matching, target_reaction) + PRIVATE_PROC(TRUE) + if(isnull(matching) || isnull(target_reaction)) return FAIL -/obj/machinery/rnd/experimentor/proc/ejectItem(delete=FALSE) - if(loaded_item) - if(cloneMode) - visible_message(span_notice("A duplicate [loaded_item] pops out!")) - var/type_to_make = loaded_item.type - new type_to_make(get_turf(pick(oview(1,src)))) - cloneMode = FALSE - return - var/turf/dropturf = get_turf(pick(view(1,src))) - if(!dropturf) //Failsafe to prevent the object being lost in the void forever. - dropturf = drop_location() - loaded_item.forceMove(dropturf) - if(delete) - qdel(loaded_item) - loaded_item = null + var/list/item_reactions = item_reactions() + if("[matching.type]" in item_reactions) + var/associated_reaction = item_reactions["[matching.type]"] + if(associated_reaction == target_reaction) + return associated_reaction + + return FAIL + +/obj/machinery/rnd/experimentor/proc/try_perform_experiment(reaction) + PRIVATE_PROC(TRUE) + if(isnull(stored_research)) + return + + if(recentlyExperimented) + return + + if(isnull(loaded_item)) + return + + if(reaction != SCANTYPE_DISCOVER) + reaction = match_reaction(loaded_item, reaction) + + if(reaction != FAIL) + var/picked_node_id = pick(techweb_item_unlock_check(loaded_item)) + stored_research.unhide_node(SSresearch.techweb_node_by_id(picked_node_id)) + + experiment(reaction, loaded_item) + use_power(750) /obj/machinery/rnd/experimentor/proc/throwSmoke(turf/where) var/datum/effect_system/fluid_spread/smoke/smoke = new smoke.set_up(0, holder = src, location = where) smoke.start() - /obj/machinery/rnd/experimentor/proc/experiment(exp,obj/item/exp_on) recentlyExperimented = 1 icon_state = "[base_icon_state]_wloop" diff --git a/tgui/packages/tgui/interfaces/Experimentator.tsx b/tgui/packages/tgui/interfaces/Experimentator.tsx new file mode 100644 index 00000000000..d2fd0b23b26 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Experimentator.tsx @@ -0,0 +1,246 @@ +import { BooleanLike } from 'common/react'; +import { toTitleCase } from 'common/string'; + +import { useBackend } from '../backend'; +import { + Button, + Icon, + Image, + LabeledList, + NoticeBox, + Section, + Stack, +} from '../components'; +import { Window } from '../layouts'; + +type Data = { + hasItem: BooleanLike; + isOnCooldown: BooleanLike; + isServerConnected: BooleanLike; + loadedItem: Item; +}; + +type Item = { + name: string; + icon: string; + isRelic: BooleanLike; + associatedNodes: Node[]; +}; + +type Node = { + name: string; + isUnlocked: BooleanLike; +}; + +export const Experimentator = (props: any) => { + const { act, data } = useBackend(); + const { hasItem, isOnCooldown, isServerConnected, loadedItem } = data; + + return ( + + + {isServerConnected ? ( + hasItem && loadedItem ? ( + act('eject')} + onExperiment={(id) => act('experiment', { id: id })} + /> + ) : ( + + No item present in experimentation chamber. Please insert one. + + ) + ) : ( + + Not connected to a server. Please sync one using a multitool. + + )} + + + ); +}; + +enum Experiment { + Poke = 'Poke', + Irradiate = 'Irradiate', + Gas = 'Gas', + Heat = 'Heat', + Cold = 'Freeze', + Obliterate = 'Obliterate', +} + +const EXPERIMENT2ICON = { + Poke: 'hand', + Irradiate: 'radiation', + Gas: 'cloud', + Heat: 'fire', + Cold: 'snowflake', + Obliterate: 'trash', +}; + +type ExperimentScreenProps = { + item: Item; + isOnCooldown: BooleanLike; + onEject: () => void; + onExperiment: (id: number) => void; +}; + +const ExperimentScreen = (props: ExperimentScreenProps) => { + const { item, isOnCooldown, onEject, onExperiment } = props; + const { name, icon, isRelic, associatedNodes } = item; + + return ( + + + + + + + + + + + + + + + + ); +}; + +type ItemPreviewProps = { + name: string; + icon: string; + onEject: () => void; +}; + +const ItemPreview = (props: ItemPreviewProps) => { + const { name, icon, onEject } = props; + + return ( + + + + + + + + + ); +};