From d2b92afe55934c4c624eb8f373aaa5c3aee4fd69 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Tue, 2 Sep 2025 20:57:41 -0500
Subject: [PATCH] Plant Analyzer UX Revamp (#92667)
## About The Pull Request
1. Chems are no longer hidden in an icon's tooltip. There is a separate
tab for chems. Right clicking to do a chem scan will pull up the chem
tab, but you can also just use the two buttons at the top - Closes
#91172
2. You no longer need to repeatedly scan a tray to update the UI.
Standing adjacent to a scanned tray will update it regularly.
3. Units have been redefined from pure seconds to minutes/seconds where
applicable.
4. The report has been swapped around, with tray on top and plant on
bottom.
5. The nutrient bar is now colored based on contents of the tray. It no
longer has a tooltip listing the tray's contents, that's now in the
chemicals tab.
6. Auto-grow, pollinating, and yield modifier icons have been brought
center below the tray, instead of in the top right section. They now
change color to make it more apparent at a glance.
7. Traits have been moved from icons to a collapsible list at the bottom
of the UI, and are now printed entirely. They still have tooltips
explaining what they do.
8. Grafted gene was moved center below the plant, and is now printed
entirely.
9. Mutations have been moved to a collapsible list center below the
plant.
10. Plants now indicate if they are dead in the UI.
11. Scanning produce now lists all the chemicals inside it on top of all
the chemicals it produces. - Closes #89443, Closes #90793
12. Restores lost behavior of seed unique data - Closes #90706
Also
13. Added Hydroponics to Runtime station
14. Added screentip for dumping reagents from hydro-trays
## Why It's Good For The Game
While I was initially supportive of the move to a plant analyzer UI it
was undeniable it was a huge UX stepdown from printing results to chat.
This PR aims to bring back some of those lost UX features by making more
information visible at a glance (instead of requiring memorization of
mystery meat icons or hovering over many elements to find information
hidden in tooltips)
I know it's a little bit of a mess with the colors and layout, I planned
on doing a whole custom css and layout and such but I kinda just wanted
to get it out.
## Changelog
:cl: Melbert
qol: Plant analyzer: Scanning a tray will regularly update the UI with
the tray's information while you stand adjacent to it, no longer
requiring you re-scan the tray to update it.
qol: Plant analyzer: UI is now split between chems and stats - RMB will
open directly to the chem tab.
qol: Plant analyzer: UI received several UX changes - listing out more
information at a glance, rather than behind tooltips or icons
fix: Plant analyzer: Chem scanning plants now shows the plant's current
chemical contents once again (on top of their genes)
fix: Plant analyzer: Unique seed data (kudzu mutation, replica pod dna)
is shown again
qol: Added hydroponics to runtime station
qol: Added screentip to hydroponic trays for clearing reagents
/:cl:
---
_maps/map_files/debug/runtimestation.dmm | 197 ++++---
.../items/devices/scanners/plant_analyzer.dm | 81 ++-
.../events/space_vines/vine_mutations.dm | 19 +
code/modules/hydroponics/grown/replicapod.dm | 7 +-
code/modules/hydroponics/grown/weeds/kudzu.dm | 13 +-
code/modules/hydroponics/hydroponics.dm | 6 +-
code/modules/hydroponics/seeds.dm | 6 +-
.../tgui/interfaces/PlantAnalyzer/Graft.tsx | 5 +
.../tgui/interfaces/PlantAnalyzer/Seed.tsx | 495 ++++++++++++++----
.../tgui/interfaces/PlantAnalyzer/Tray.tsx | 181 +++++--
.../tgui/interfaces/PlantAnalyzer/index.tsx | 60 ++-
.../tgui/interfaces/PlantAnalyzer/types.ts | 56 +-
12 files changed, 857 insertions(+), 269 deletions(-)
diff --git a/_maps/map_files/debug/runtimestation.dmm b/_maps/map_files/debug/runtimestation.dmm
index 0dbf5d65af1..1d0269d8cb1 100644
--- a/_maps/map_files/debug/runtimestation.dmm
+++ b/_maps/map_files/debug/runtimestation.dmm
@@ -1222,6 +1222,11 @@
"kS" = (
/turf/closed/wall/r_wall,
/area/station/medical/chemistry)
+"kY" = (
+/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
+/obj/structure/cable,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"lc" = (
/turf/open/floor/engine,
/area/station/hallway/secondary/entry)
@@ -1444,6 +1449,13 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/turf/open/floor/plating,
/area/station/engineering/main)
+"tj" = (
+/obj/structure/cable,
+/obj/machinery/light/directional/east,
+/obj/item/gun/energy/floragun,
+/obj/item/geneshears,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"tn" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
@@ -1559,6 +1571,12 @@
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
/turf/open/floor/iron/white/corner,
/area/station/medical/medbay)
+"vK" = (
+/obj/machinery/door/airlock,
+/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
+/obj/structure/cable,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"vQ" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
@@ -1638,6 +1656,12 @@
/obj/structure/transit_tube/station/dispenser/reverse/flipped,
/turf/open/floor/iron,
/area/station/engineering/gravity_generator)
+"xv" = (
+/obj/machinery/vending/hydroseeds{
+ all_products_free = 1
+ },
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"xD" = (
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4,
/obj/effect/turf_decal/tile/blue/half/contrasted,
@@ -1728,6 +1752,10 @@
/obj/machinery/airalarm/directional/north,
/turf/open/floor/iron,
/area/station/science)
+"zU" = (
+/obj/machinery/hydroponics/constructable/fullupgrade,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"zY" = (
/obj/effect/turf_decal/stripes/line,
/obj/machinery/atmospherics/pipe/smart/manifold4w/scrubbers/hidden/layer2,
@@ -1736,6 +1764,7 @@
/area/station/commons/storage/primary)
"Aj" = (
/obj/structure/cable,
+/obj/machinery/light/directional/east,
/turf/open/floor/iron,
/area/station/cargo/bitrunning/den)
"An" = (
@@ -1792,6 +1821,11 @@
/obj/item/gun/ballistic/automatic/pistol,
/turf/open/floor/iron,
/area/station/command/bridge)
+"BK" = (
+/obj/item/wrench,
+/obj/item/plant_analyzer,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"BM" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/turf/open/floor/iron,
@@ -1841,6 +1875,17 @@
/obj/machinery/component_printer,
/turf/open/floor/iron/dark,
/area/station/science/explab)
+"Dd" = (
+/obj/item/shovel/spade{
+ pixel_y = 4;
+ pixel_x = 3
+ },
+/obj/item/secateurs,
+/obj/item/cultivator{
+ pixel_x = -6
+ },
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"Df" = (
/obj/machinery/light/directional/east,
/obj/item/integrated_circuit/loaded/hello_world,
@@ -1984,7 +2029,7 @@
/area/station/engineering/atmos)
"GN" = (
/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
- dir = 4
+ dir = 1
},
/obj/machinery/light/directional/south,
/turf/open/floor/iron,
@@ -2098,6 +2143,9 @@
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark,
/area/station/engineering/gravity_generator)
+"Ju" = (
+/turf/closed/wall/r_wall,
+/area/station/service/hydroponics)
"JJ" = (
/obj/machinery/navbeacon{
codes_txt = "patrol;next_patrol=4-Southeast";
@@ -2133,6 +2181,12 @@
/obj/machinery/light/directional/south,
/turf/open/floor/plating,
/area/station/engineering/atmos)
+"KW" = (
+/obj/structure/sink/directional/south,
+/obj/item/storage/bag/plants,
+/obj/item/reagent_containers/cup/watering_can/advanced,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"Lb" = (
/obj/effect/turf_decal/stripes/line{
dir = 1
@@ -2193,11 +2247,6 @@
/obj/structure/cable,
/turf/open/floor/iron,
/area/station/hallway/primary/central)
-"Ml" = (
-/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
-/obj/machinery/light/directional/south,
-/turf/open/floor/iron,
-/area/station/hallway/secondary/entry)
"My" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/structure/cable,
@@ -2210,6 +2259,12 @@
},
/turf/open/floor/iron,
/area/station/cargo/miningoffice)
+"MH" = (
+/obj/machinery/atmospherics/components/unary/vent_pump/on/layer4{
+ dir = 4
+ },
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"MM" = (
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
/obj/effect/turf_decal/stripes/line{
@@ -2222,6 +2277,12 @@
/obj/effect/turf_decal/tile/neutral/fourcorners,
/turf/open/floor/iron/dark,
/area/station/engineering/gravity_generator)
+"MT" = (
+/obj/machinery/vending/hydronutrients{
+ all_products_free = 1
+ },
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"Nb" = (
/obj/machinery/bci_implanter,
/turf/open/floor/iron/dark,
@@ -2275,6 +2336,10 @@
/obj/item/stock_parts/power_store/cell/bluespace,
/turf/open/floor/iron/dark,
/area/station/science/explab)
+"Oy" = (
+/obj/machinery/seed_extractor,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"OF" = (
/obj/structure/cable,
/obj/machinery/atmospherics/pipe/smart/manifold4w/supply/hidden/layer4,
@@ -2483,7 +2548,6 @@
/area/station/medical/medbay)
"SC" = (
/obj/structure/table/reinforced,
-/obj/machinery/light/directional/south,
/obj/item/bitrunning_debug,
/turf/open/floor/iron,
/area/station/cargo/bitrunning/den)
@@ -2573,6 +2637,13 @@
/obj/structure/cable,
/turf/open/floor/iron,
/area/station/engineering/main)
+"VF" = (
+/obj/machinery/hydroponics/constructable/fullupgrade,
+/obj/machinery/power/apc/auto_name/directional/south,
+/obj/structure/cable,
+/obj/machinery/airalarm/directional/east,
+/turf/open/floor/iron,
+/area/station/service/hydroponics)
"We" = (
/obj/machinery/newscaster/directional/west,
/turf/open/floor/iron,
@@ -2672,6 +2743,10 @@
"Yd" = (
/turf/open/floor/iron,
/area/station/cargo/bitrunning/den)
+"Yg" = (
+/obj/machinery/smartfridge/food,
+/turf/open/floor/plating,
+/area/station/service/hydroponics)
"Ym" = (
/obj/structure/table,
/obj/item/storage/toolbox/electrical,
@@ -3356,12 +3431,12 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
+Ju
+Ju
+Ju
+Ju
+Ju
+Ju
aa
aa
aa
@@ -3448,12 +3523,12 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
+Ju
+MT
+xv
+BK
+zU
+Ju
aa
aa
aa
@@ -3540,12 +3615,12 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
+Ju
+KW
+MH
+Dd
+zU
+Ju
aa
aa
aa
@@ -3632,12 +3707,12 @@ aa
aa
aa
aa
-aa
-aa
-aa
-aa
-aa
-aa
+Ju
+Oy
+kY
+tj
+VF
+Ju
aa
aa
aa
@@ -3724,12 +3799,12 @@ Tj
Tj
Tj
Tj
-dY
-en
-dY
-dY
-aa
-aa
+Ju
+Yg
+vK
+Ju
+Ju
+Ju
aa
aa
aa
@@ -3818,7 +3893,7 @@ lc
lc
wB
eh
-eh
+ax
dY
aa
aa
@@ -3910,7 +3985,7 @@ lc
lc
wB
eh
-eh
+ax
en
aa
aa
@@ -4002,7 +4077,7 @@ lc
lc
wB
eh
-eh
+ax
en
aa
aa
@@ -4094,7 +4169,7 @@ lc
lc
wB
eh
-eh
+ax
dY
aa
aa
@@ -4186,7 +4261,7 @@ lc
lc
wB
eh
-eh
+ax
en
aa
aa
@@ -4278,7 +4353,7 @@ lc
lc
wB
eh
-eh
+ax
en
aa
aa
@@ -4369,8 +4444,8 @@ lc
lc
lc
wB
-eh
-eh
+ax
+ax
en
aa
aa
@@ -4461,7 +4536,7 @@ lc
lc
lc
wB
-eh
+ax
eh
dY
aa
@@ -4553,7 +4628,7 @@ lc
lc
lc
wB
-eh
+ax
eh
en
aa
@@ -4645,7 +4720,7 @@ lc
lc
lc
wB
-eh
+ax
eh
en
aa
@@ -4737,7 +4812,7 @@ lc
lc
lc
wB
-eh
+ax
gv
dY
aa
@@ -4829,7 +4904,7 @@ lc
lc
lc
wB
-eh
+ax
eh
en
aa
@@ -4921,7 +4996,7 @@ lc
lc
lc
wB
-eh
+ax
gF
fj
aa
@@ -5013,7 +5088,7 @@ lc
lc
lc
wB
-eh
+ax
eh
en
aa
@@ -5105,7 +5180,7 @@ lc
lc
lc
wB
-eh
+ax
GN
dY
aa
@@ -5197,8 +5272,8 @@ lc
lc
lc
wB
+ax
eh
-BM
en
aa
aa
@@ -5289,8 +5364,8 @@ lc
lc
lc
wB
+ax
eh
-BM
en
aa
aa
@@ -5381,8 +5456,8 @@ lc
lc
lc
wB
+ax
eh
-BM
en
aa
aa
@@ -5473,8 +5548,8 @@ lc
lc
lc
wB
-eh
-Ml
+ax
+gv
dY
aa
aa
@@ -5565,8 +5640,8 @@ lc
lc
lc
wB
+ax
eh
-BM
en
aa
aa
@@ -5657,8 +5732,8 @@ en
en
dY
wB
+ax
eh
-BM
dY
dY
fn
@@ -5749,8 +5824,8 @@ eh
eh
eV
eh
+ax
eh
-BM
eh
eV
eh
diff --git a/code/game/objects/items/devices/scanners/plant_analyzer.dm b/code/game/objects/items/devices/scanners/plant_analyzer.dm
index 78bcd74ae6e..fcce33141df 100644
--- a/code/game/objects/items/devices/scanners/plant_analyzer.dm
+++ b/code/game/objects/items/devices/scanners/plant_analyzer.dm
@@ -1,3 +1,6 @@
+#define PLANT_ANALYZER_STAT_TAB 1
+#define PLANT_ANALYZER_CHEM_TAB 2
+
/obj/item/plant_analyzer
name = "plant analyzer"
desc = "A scanner used to evaluate a plant's various areas of growth, genetic traits and chemicals."
@@ -12,13 +15,21 @@
custom_materials = list(/datum/material/iron = SMALL_MATERIAL_AMOUNT*0.3, /datum/material/glass =SMALL_MATERIAL_AMOUNT*0.2)
/// Cached data from ui_interact
var/list/last_scan_data
+ /// Weakref to the last thing we scanned
+ var/datum/weakref/last_tray_scanned
/// Cached data for the product grinder results
var/static/list/product_grinder_results = list()
+ /// If TRUE the UI opens to the second tab / the chem tab
+ var/shown_tab = PLANT_ANALYZER_STAT_TAB
/obj/item/plant_analyzer/Initialize(mapload)
. = ..()
register_item_context()
+/obj/item/plant_analyzer/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
/obj/item/plant_analyzer/add_item_context(
obj/item/source,
list/context,
@@ -63,6 +74,7 @@
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
+ shown_tab = PLANT_ANALYZER_STAT_TAB
return analyze(user, interacting_with)
/// Same as above, but with right click. Right-clicking scans for chemicals.
@@ -86,6 +98,7 @@
return ITEM_INTERACT_SUCCESS
return ITEM_INTERACT_BLOCKING
+ shown_tab = PLANT_ANALYZER_CHEM_TAB
return analyze(user, scan_target)
/*
@@ -123,8 +136,22 @@
ui = new(user, src, "PlantAnalyzer", "Plant Analyzer")
ui.open()
+/obj/item/plant_analyzer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("setTab")
+ var/tab = params["tab"]
+ if(tab == PLANT_ANALYZER_STAT_TAB || tab == PLANT_ANALYZER_CHEM_TAB)
+ shown_tab = tab
+ return TRUE
+
/obj/item/plant_analyzer/ui_data(mob/user)
- return last_scan_data
+ var/list/data = list()
+ data["active_tab"] = shown_tab
+ data += last_scan_data
+ return data
/obj/item/plant_analyzer/ui_static_data(mob/user)
var/list/data = list()
@@ -140,13 +167,22 @@
data["trait_db"] += trait_data
return data
+/obj/item/plant_analyzer/process(seconds_per_tick)
+ var/atom/real_last_tray_scanned = last_tray_scanned?.resolve()
+ if(QDELETED(real_last_tray_scanned))
+ return PROCESS_KILL
+
+ if(loc.Adjacent(real_last_tray_scanned))
+ analyze(null, real_last_tray_scanned)
+
/// Called when our analyzer is used on something
/obj/item/plant_analyzer/proc/analyze(mob/user, atom/target)
var/obj/item/graft/graft
var/obj/item/seeds/seed
var/obj/machinery/hydroponics/tray
- playsound(src, SFX_INDUSTRIAL_SCAN, 20, TRUE, -2, TRUE, FALSE)
+ if(user)
+ playsound(src, SFX_INDUSTRIAL_SCAN, 20, TRUE, -2, TRUE, FALSE)
if (istype(target, /obj/machinery/hydroponics))
tray = target
@@ -166,12 +202,16 @@
if (!seed && !tray && !graft)
return NONE
- if(!user.can_read(src))
- return ITEM_INTERACT_BLOCKING
+ if(user)
+ if(!user.can_read(src))
+ return ITEM_INTERACT_BLOCKING
+ START_PROCESSING(SSobj, src)
+ last_tray_scanned = WEAKREF(tray) // sets it to null if no tray
last_scan_data = list(
"tray_data" = null,
"seed_data" = null,
+ "plant_data" = null,
"graft_data" = null,
)
@@ -179,6 +219,7 @@
last_scan_data["tray_data"] = list(
"plant_health" = tray.plant_health,
"plant_age" = tray.age,
+ "is_dead" = tray.plant_status == HYDROTRAY_PLANT_DEAD,
"name" = tray.name,
"icon" = tray.icon,
"icon_state" = tray.icon_state,
@@ -198,15 +239,27 @@
"toxins_max" = MAX_TRAY_TOXINS,
"reagents" = list(),
)
- for(var/datum/reagent/reagent in tray.reagents.reagent_list)
+ for(var/datum/reagent/reagent as anything in tray.reagents.reagent_list)
last_scan_data["tray_data"]["reagents"] += list(list(
"name" = reagent.name,
- "volume" = reagent.volume
+ "volume" = round(reagent.volume, CHEMICAL_QUANTISATION_LEVEL),
+ "color" = reagent.color,
))
if(seed)
last_scan_data["seed_data"] = make_seed_data(seed)
+ if(isitem(target) && target.reagents)
+ last_scan_data["plant_data"] = list(
+ "reagents" = list(),
+ )
+ for(var/datum/reagent/reagent as anything in target.reagents.reagent_list)
+ last_scan_data["plant_data"]["reagents"] += list(list(
+ "name" = reagent.name,
+ "volume" = round(reagent.volume, CHEMICAL_QUANTISATION_LEVEL),
+ "color" = reagent.color,
+ ))
+
if(graft)
last_scan_data["graft_data"] = list(
"name" = graft.parent_name,
@@ -218,10 +271,11 @@
"endurance" = graft.endurance,
"weed_rate" = graft.weed_rate,
"weed_chance" = graft.weed_chance,
- "graft_gene" = graft.stored_trait.type
+ "graft_gene" = graft.stored_trait.type,
)
- ui_interact(user)
+ if(user)
+ ui_interact(user)
return ITEM_INTERACT_SUCCESS
@@ -280,4 +334,15 @@
seed_data["juice_name"] = product_grinder_results[seed.product]["juice_name"]
seed_data["grind_results"] = product_grinder_results[seed.product]["grind_results"]
+ seed_data["unique_labels"] = list()
+ seed_data["unique_collapsibles"] = list()
+ var/list/unique_data = seed.get_unique_analyzer_data()
+ for(var/label in unique_data)
+ seed_data[islist(unique_data[label]) ? "unique_collapsibles" : "unique_labels"] += list(list(
+ "label" = label,
+ "data" = unique_data[label],
+ ))
return seed_data
+
+#undef PLANT_ANALYZER_STAT_TAB
+#undef PLANT_ANALYZER_CHEM_TAB
diff --git a/code/modules/events/space_vines/vine_mutations.dm b/code/modules/events/space_vines/vine_mutations.dm
index 8a9d922ee82..9db702169dd 100644
--- a/code/modules/events/space_vines/vine_mutations.dm
+++ b/code/modules/events/space_vines/vine_mutations.dm
@@ -1,6 +1,8 @@
/datum/spacevine_mutation
/// Displayed name of mutation
var/name = ""
+ /// Description of mutation, shown in the plant analyzer
+ var/description = ""
/// Severity of mutation in terms of gameplay, affects appearance chance and how many mutations can be on the same vine
var/severity = 1
/// The mutation's contribution to a given vine's color
@@ -64,6 +66,7 @@
/datum/spacevine_mutation/light
name = "Light"
+ description = "Emits light."
hue = "#B2EA70"
quality = POSITIVE
severity = SEVERITY_TRIVIAL
@@ -74,6 +77,7 @@
/datum/spacevine_mutation/toxicity
name = "Toxic"
+ description = "Releases toxins when touched or eaten."
hue = "#9B3675"
severity = SEVERITY_AVERAGE
quality = NEGATIVE
@@ -91,6 +95,7 @@
/datum/spacevine_mutation/explosive // JC IT'S A BOMB
name = "Explosive"
+ description = "Causes an explosion when destroyed."
hue = "#D83A56"
quality = NEGATIVE
severity = SEVERITY_MAJOR
@@ -108,6 +113,7 @@
/datum/spacevine_mutation/fire_proof
name = "Fire proof"
+ description = "Provides immunity to heat and burn damage."
hue = "#FF616D"
quality = MINOR_NEGATIVE
severity = SEVERITY_ABOVE_AVERAGE
@@ -123,6 +129,7 @@
/datum/spacevine_mutation/cold_proof
name = "Cold proof"
+ description = "Provides immunity to cold damage."
hue = "#0BD5D9"
quality = MINOR_NEGATIVE
severity = SEVERITY_AVERAGE
@@ -133,6 +140,7 @@
/datum/spacevine_mutation/temp_stabilisation
name = "Temperature stabilisation"
+ description = "Stabilizes the temperature of the surrounding area."
hue = "#B09856"
quality = POSITIVE
severity = SEVERITY_MINOR
@@ -156,6 +164,7 @@
/datum/spacevine_mutation/vine_eating
name = "Vine eating"
+ description = "Destroys other Kudzu vines on spread."
hue = "#F4A442"
quality = MINOR_NEGATIVE
severity = SEVERITY_MINOR
@@ -167,6 +176,7 @@
/datum/spacevine_mutation/aggressive_spread //very OP, but im out of other ideas currently
name = "Aggressive spreading"
+ description = "Heavily wounds mobs when spreading or tangling them."
hue = "#316b2f"
severity = SEVERITY_MAJOR
quality = NEGATIVE
@@ -222,6 +232,7 @@
/datum/spacevine_mutation/transparency
name = "transparent"
+ description = "Allows light to pass through."
hue = ""
quality = POSITIVE
severity = SEVERITY_TRIVIAL
@@ -232,6 +243,7 @@
/datum/spacevine_mutation/oxy_eater
name = "Oxygen consuming"
+ description = "Consumes Oxygen from the surrounding area."
hue = "#28B5B5"
severity = SEVERITY_AVERAGE
quality = NEGATIVE
@@ -247,6 +259,7 @@
/datum/spacevine_mutation/nitro_eater
name = "Nitrogen consuming"
+ description = "Consumes Nitrogen from the surrounding area."
hue = "#FF7B54"
severity = SEVERITY_AVERAGE
quality = NEGATIVE
@@ -262,6 +275,7 @@
/datum/spacevine_mutation/carbondioxide_eater
name = "CO2 consuming"
+ description = "Consumes Carbon Dioxide from the surrounding area."
hue = "#798777"
severity = SEVERITY_MINOR
quality = POSITIVE
@@ -277,6 +291,7 @@
/datum/spacevine_mutation/plasma_eater
name = "Plasma consuming"
+ description = "Consumes Plasma from the surrounding area."
hue = "#9074b6"
severity = SEVERITY_AVERAGE
quality = POSITIVE
@@ -292,6 +307,7 @@
/datum/spacevine_mutation/thorns
name = "Thorny"
+ description = "Causes damage when hitting or passing through the vines."
hue = "#9ECCA4"
severity = SEVERITY_AVERAGE
quality = NEGATIVE
@@ -324,6 +340,7 @@
/datum/spacevine_mutation/hardened
name = "Hardened"
+ description = "Provides resistance to cutting attacks, makes vines hardier, and prevents light from passing through."
hue = "#997700"
quality = NEGATIVE
severity = SEVERITY_ABOVE_AVERAGE
@@ -340,6 +357,7 @@
/datum/spacevine_mutation/timid
name = "Timid"
+ description = "Hides the vines under structures and prevents them from tangling mobs."
hue = "#a4a9ac"
quality = POSITIVE
severity = SEVERITY_MINOR
@@ -354,6 +372,7 @@
/datum/spacevine_mutation/flowering
name = "Flowering"
+ description = "Causes the vine to grow flower buds which spawns man eating plants when fully grown."
hue = "#66DE93"
quality = NEGATIVE
severity = SEVERITY_MAJOR
diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm
index 2b97110dafa..842c10a025e 100644
--- a/code/modules/hydroponics/grown/replicapod.dm
+++ b/code/modules/hydroponics/grown/replicapod.dm
@@ -106,11 +106,10 @@
sampleDNA = null
contains_sample = FALSE
-/obj/item/seeds/replicapod/get_unique_analyzer_text()
+/obj/item/seeds/replicapod/get_unique_analyzer_data()
if(contains_sample)
- return "It contains a blood sample with blood DNA (UE) \"[sampleDNA]\"." //blood DNA (UE) shows in medical records and is readable by forensics scanners
- else
- return null
+ return list("Blood DNA" = sampleDNA)
+ return null
/obj/item/seeds/replicapod/harvest(mob/user) //now that one is fun -- Urist
var/obj/machinery/hydroponics/parent = loc
diff --git a/code/modules/hydroponics/grown/weeds/kudzu.dm b/code/modules/hydroponics/grown/weeds/kudzu.dm
index a0a3c940571..87241365cf4 100644
--- a/code/modules/hydroponics/grown/weeds/kudzu.dm
+++ b/code/modules/hydroponics/grown/weeds/kudzu.dm
@@ -50,13 +50,12 @@
plant(user)
to_chat(user, span_notice("You plant the kudzu. You monster."))
-/obj/item/seeds/kudzu/get_unique_analyzer_text()
- var/kudzu_mutations = ""
- var/output_message = ""
- for(var/datum/spacevine_mutation/SM in mutations)
- kudzu_mutations += "[(kudzu_mutations == "") ? "" : ", "][SM.name]"
- output_message += "Plant Mutations: [(kudzu_mutations == "") ? "None." : "[kudzu_mutations]."]"
- return output_message
+/obj/item/seeds/kudzu/get_unique_analyzer_data()
+ var/list/all_mutations = list()
+ for(var/datum/spacevine_mutation/vine_trait in mutations)
+ all_mutations[vine_trait.name] = vine_trait.description
+
+ return list("Kudzu Traits" = all_mutations)
/obj/item/seeds/kudzu/on_chem_reaction(datum/reagents/reagents)
var/list/temp_mut_list = list()
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 5d110f8ef8e..ffa6f596711 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -111,16 +111,16 @@
if(HAS_SILICON_ACCESS(user))
return NONE
+ context[SCREENTIP_CONTEXT_RMB] = "Empty nutrients"
+
switch(plant_status)
if(HYDROTRAY_PLANT_DEAD)
context[SCREENTIP_CONTEXT_LMB] = "Remove dead plant"
- return CONTEXTUAL_SCREENTIP_SET
if(HYDROTRAY_PLANT_HARVESTABLE)
context[SCREENTIP_CONTEXT_LMB] = "Harvest plant"
- return CONTEXTUAL_SCREENTIP_SET
- return NONE
+ return CONTEXTUAL_SCREENTIP_SET
// If the plant is harvestable, we can graft it with secateurs or harvest it with a plant bag.
if(plant_status == HYDROTRAY_PLANT_HARVESTABLE)
diff --git a/code/modules/hydroponics/seeds.dm b/code/modules/hydroponics/seeds.dm
index c2985aef15e..14def128976 100644
--- a/code/modules/hydroponics/seeds.dm
+++ b/code/modules/hydroponics/seeds.dm
@@ -452,9 +452,11 @@
/**
* Override for seeds with unique text for their analyzer. (No newlines at the start or end of unique text!)
- * Returns null if no unique text, or a string of text if there is.
+ * Returns null if no unique data
+ * Return an assoc list (label = text) to add a new line to the analyzer
+ * Return an assoc list (label = list(text = tooltip, text = tooltip)) to add a new collapsible section to the analyzer
*/
-/obj/item/seeds/proc/get_unique_analyzer_text()
+/obj/item/seeds/proc/get_unique_analyzer_data()
return null
/**
diff --git a/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx b/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx
index 83f4e244779..87b14fe667b 100644
--- a/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx
+++ b/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx
@@ -16,6 +16,11 @@ export function PlantAnalyzerGraft(props) {
const { data } = useBackend();
const { graft_data } = data;
+ if (!graft_data) {
+ // This shouldn't be rendered if graft_data is null
+ return null;
+ }
+
return (
();
- const { seed_data, tray_data } = data;
+ const { seed_data, tray_data, cycle_seconds, trait_db } = data;
+
+ if (!seed_data) {
+ // This shouldn't be rendered if seed data is null
+ return null;
+ }
+
+ const all_traits = [
+ ...(seed_data.core_traits || []),
+ ...(seed_data.removable_traits || []),
+ ];
return (
- }
- >
+
-
-
- {seed_data.product_icon && seed_data.product_icon_state && (
-
- )}
+
+
+
+
+
+ {seed_data.product_icon && seed_data.product_icon_state && (
+
+
+
+ )}
+
+
+
+ {seed_data.mutatelist.length > 0 && (
+
+
+
+ {seed_data.mutatelist.map((mutation) => (
+
+
+
+ ))}
+
+
+
+ )}
+
@@ -79,18 +138,25 @@ export function PlantAnalyzerSeed(props) {
{tray_data && (
-
- {tray_data.plant_age * data.cycle_seconds} /{' '}
- {seed_data.lifespan * data.cycle_seconds} seconds
-
+ {tray_data.is_dead ? (
+
+
+ Dead
+
+ ) : (
+
+ {tray_data.plant_age * cycle_seconds} /{' '}
+ {formatPerSecond(seed_data.lifespan, cycle_seconds, false)}
+
+ )}
)}
@@ -98,30 +164,21 @@ export function PlantAnalyzerSeed(props) {
label="Maturation"
tooltip="The age at which the plant starts growing products."
>
- {seed_data.maturation * data.cycle_seconds} seconds
+ {formatPerSecond(seed_data.maturation, cycle_seconds)}
- {seed_data.production * data.cycle_seconds} seconds
+ {formatPerSecond(seed_data.production, cycle_seconds)}
-
- {seed_data.lifespan} / 100
-
+ {formatPerSecond(seed_data.lifespan, cycle_seconds)}
-
- {seed_data.weed_chance && seed_data.weed_rate
- ? seed_data.weed_chance +
- '% chance to grow by ' +
- seed_data.weed_rate
- : 'No weed growth'}
+
+ {`${seed_data.weed_chance}% chance to grow by
+ ${seed_data.weed_rate} every ${cycle_seconds} seconds`}
+ {seed_data.unique_labels.map((label) => (
+
+ {label.data}
+
+ ))}
+
+
+ {all_traits.map((trait) => {
+ const traitInfo = getTraitInfo(trait, trait_db);
+ return (
+
+
+
+ );
+ })}
+
+
+ {seed_data.unique_collapsibles.map((collapsible) => (
+
+
+ {Object.entries(collapsible.data).map(
+ ([dataText, dataTooltip]) => (
+
+
+
+ ),
+ )}
+
+
+ ))}
);
}
-function SeedExtraData(props) {
+export function PlantAnalyzerSeedChems(props) {
const { data } = useBackend();
const { seed_data } = data;
- return (
- <>
- {!!seed_data.graft_gene && (
-
- )}
+ if (!seed_data) {
+ // This shouldn't be rendered if seed data is null
+ return null;
+ }
- {seed_data.removable_traits?.map((trait) => (
-
- ))}
- {seed_data.core_traits?.map((trait) => (
-
- ))}
- {!!seed_data.mutatelist.length && (
-
-
-
- )}
- {!!seed_data.juice_name && (
-
-
-
- )}
- {!!seed_data.distill_reagent && (
-
-
-
- )}
- {seed_data.reagents.length > 0 && (
-
- }
- >
-
-
- )}
- >
+ const totalPercentage = seed_data.reagents.reduce(
+ (sum, reagent) => sum + reagent.rate * 100,
+ 0,
+ );
+
+ const totalVolume =
+ seed_data.reagents.reduce(
+ (sum, reagent) => sum + expectedReagentVolume(reagent, seed_data),
+ 0,
+ ) || 0;
+
+ return (
+
+
+
+ {seed_data.reagents.length === 0 ? (
+
+ No reagent genes
+
+ ) : (
+
+
+ Reagent
+ Percentage
+
+
+
+ Expected Volume
+
+
+
+
+ {seed_data.reagents.map((reagent) => (
+
+
+ {reagent.name}
+
+ {reagent.rate * 100}%
+
+ ~{expectedReagentVolume(reagent, seed_data)}u
+
+
+ ))}
+
+
+ Total
+
+
+ {totalPercentage}%
+ {totalPercentage > 100 && (
+
+ )}
+
+ ~{totalVolume}u
+
+
+
+ Cap
+
+ 100%
+
+ ~{seed_data.volume_mod * seed_data.volume_units}u
+
+
+ {seed_data.grind_results.length > 0 && (
+
+
+ Grinds nutriments into:
+
+ {seed_data.grind_results.join(', ')}
+
+ )}
+ {seed_data.juice_name && (
+
+
+ Juices into:
+
+ {seed_data.juice_name}
+
+ )}
+ {seed_data.distill_reagent && (
+
+
+ Distills into:
+
+ {seed_data.distill_reagent}
+
+ )}
+
+ )}
+
+
+
);
}
+
+export function PlantAnalyzerPlantChems(props) {
+ const { data } = useBackend();
+ const { seed_data, plant_data } = data;
+
+ if (!seed_data || !plant_data) {
+ // This shouldn't be rendered if seed or plant data is null
+ return null;
+ }
+
+ return (
+
+
+
+ {plant_data.reagents.length === 0 ? (
+
+ No reagent genes
+
+ ) : (
+
+
+ Reagent
+ Volume
+
+ {plant_data.reagents.map((reagent) => (
+
+
+ {reagent.name}
+
+ {reagent.volume}
+
+ ))}
+
+
+ Total
+
+
+ {plant_data.reagents.reduce(
+ (sum, reagent) => sum + reagent.volume,
+ 0,
+ )}
+ u
+
+
+
+ )}
+
+
+
+ );
+}
+
+function expectedReagentVolume(
+ reagent: ReagentData,
+ seed_data: SeedData,
+): number {
+ const baseVolume = seed_data.volume_units * seed_data.volume_mod;
+ return Math.round(reagent.rate * baseVolume * (seed_data.potency / 100)) || 0;
+}
+
+function formatPerSecond(
+ value: number,
+ cycle_seconds: number,
+ simplify: boolean = true,
+) {
+ if (!simplify) {
+ return `${value * cycle_seconds} second${value * cycle_seconds > 1 ? 's' : ''}`;
+ }
+ const seconds = Math.round(value * cycle_seconds);
+ if (seconds < 60) {
+ return `${seconds} second${seconds > 1 ? 's' : ''}`;
+ }
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${minutes} minute${minutes > 1 ? 's' : ''}${
+ remainingSeconds > 0
+ ? `, ${remainingSeconds} second${remainingSeconds > 1 ? 's' : ''}`
+ : ''
+ }`;
+}
+
+function getTraitInfo(trait: string, trait_db: PlantAnalyzerData['trait_db']) {
+ const traitData = trait_db.find((t) => t.path === trait);
+ return traitData;
+}
diff --git a/tgui/packages/tgui/interfaces/PlantAnalyzer/Tray.tsx b/tgui/packages/tgui/interfaces/PlantAnalyzer/Tray.tsx
index 88dcd4f54cf..6b1c144a3b4 100644
--- a/tgui/packages/tgui/interfaces/PlantAnalyzer/Tray.tsx
+++ b/tgui/packages/tgui/interfaces/PlantAnalyzer/Tray.tsx
@@ -1,12 +1,13 @@
import {
+ Box,
+ Button,
DmIcon,
- Icon,
LabeledList,
+ NoticeBox,
ProgressBar,
Section,
Stack,
Table,
- Tooltip,
} from 'tgui-core/components';
import { capitalizeFirst } from 'tgui-core/string';
@@ -14,42 +15,70 @@ import { useBackend } from '../../backend';
import { Fallback } from './Fallback';
import type { PlantAnalyzerData } from './types';
-export function PlantAnalyzerTray(props) {
+export function PlantAnalyzerTrayStats(props) {
const { data } = useBackend();
const { tray_data } = data;
+ if (!tray_data) {
+ // This shouldn't be rendered if tray_data is null
+ return null;
+ }
+
return (
-
- {!!tray_data.self_sustaining && (
-
-
-
- )}
- {!!tray_data.being_pollinated && (
-
-
-
- )}
- {tray_data.yield_mod > 1 && (
-
-
-
- )}
- >
- }
- >
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -73,15 +102,32 @@ export function PlantAnalyzerTray(props) {
label="Nutrients"
tooltip="The plant starts withering without nutrients, unless it is a weed. Nutrients may affect plant and tray stats."
>
- 0 ? (
-
- ) : (
- 'No reagents detected.'
- )
- }
- >
+ {tray_data.reagents.length > 0 ? (
+
+
+ {tray_data.nutri} / {tray_data.nutri_max}
+
+ {tray_data.reagents.map((reagent, i) => (
+
+ ))}
+
+ ) : (
{tray_data.nutri} / {tray_data.nutri_max}
-
+ )}
();
+ const { tray_data } = data;
+
+ if (!tray_data) {
+ // This shouldn't be rendered if tray_data is null
+ return null;
+ }
return (
-
-
- Reagents:
-
- {reagents.map((reagent, i) => (
-
- {reagent.name}
-
- {reagent.volume}u
-
-
- ))}
-
+
+ {tray_data.reagents.length === 0 ? (
+
+ No reagents detected
+
+ ) : (
+
+
+ Reagent
+ Volume
+
+ {tray_data.reagents.map((reagent, i) => (
+
+
+ {reagent.name}
+
+ {reagent.volume}u
+
+ ))}
+
+ )}
+
);
}
+
+function nutriToColor(nutri: number, maxNutri: number) {
+ if (nutri < maxNutri * 0.3) return 'red';
+ if (nutri < maxNutri * 0.7) return 'yellow';
+ return 'green';
+}
diff --git a/tgui/packages/tgui/interfaces/PlantAnalyzer/index.tsx b/tgui/packages/tgui/interfaces/PlantAnalyzer/index.tsx
index dfb9be25571..ef3d94efc28 100644
--- a/tgui/packages/tgui/interfaces/PlantAnalyzer/index.tsx
+++ b/tgui/packages/tgui/interfaces/PlantAnalyzer/index.tsx
@@ -1,20 +1,64 @@
+import { Button, Section, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Window } from '../../layouts';
import { PlantAnalyzerGraft } from './Graft';
-import { PlantAnalyzerSeed } from './Seed';
-import { PlantAnalyzerTray } from './Tray';
+import {
+ PlantAnalyzerPlantChems,
+ PlantAnalyzerSeedChems,
+ PlantAnalyzerSeedStats,
+} from './Seed';
+import { PlantAnalyzerTrayChems, PlantAnalyzerTrayStats } from './Tray';
import type { PlantAnalyzerData } from './types';
+import { PlantAnalyzerTabs } from './types';
export function PlantAnalyzer(props) {
- const { data } = useBackend();
- const { graft_data, seed_data, tray_data } = data;
+ const { act, data } = useBackend();
+ const { graft_data, seed_data, tray_data, plant_data, active_tab } = data;
return (
-
+
- {graft_data && }
- {seed_data && }
- {tray_data && }
+
+
+
+
+
+
+
+
+
+
+ {active_tab === PlantAnalyzerTabs.STATS || graft_data ? (
+ <>
+ {graft_data && }
+ {tray_data && }
+ {seed_data && }
+ >
+ ) : (
+ <>
+ {/* grafts don't have any chems */}
+ {tray_data && }
+ {seed_data && }
+ {plant_data && }
+ >
+ )}
);
diff --git a/tgui/packages/tgui/interfaces/PlantAnalyzer/types.ts b/tgui/packages/tgui/interfaces/PlantAnalyzer/types.ts
index a2dde935d69..bd4e908f706 100644
--- a/tgui/packages/tgui/interfaces/PlantAnalyzer/types.ts
+++ b/tgui/packages/tgui/interfaces/PlantAnalyzer/types.ts
@@ -1,36 +1,46 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export enum PlantAnalyzerTabs {
+ STATS = 1,
+ CHEM = 2,
+}
+
export type PlantAnalyzerData = {
- graft_data: GraftData;
- seed_data: SeedData;
- tray_data: TrayData;
+ graft_data: GraftData | null;
+ seed_data: SeedData | null;
+ plant_data: PlantData | null;
+ tray_data: TrayData | null;
// Static
cycle_seconds: number;
trait_db: TraitData[];
+ active_tab: PlantAnalyzerTabs;
};
type TrayData = {
- being_pollinated: boolean;
- icon_state: string;
+ being_pollinated: BooleanLike;
icon: string;
+ icon_state: string;
+ is_dead: BooleanLike;
light_level: number;
name: string;
- nutri_max: number;
nutri: number;
- pests_max: number;
+ nutri_max: number;
pests: number;
+ pests_max: number;
plant_age: number;
plant_health: number;
reagents: ReagentVolume[];
- self_sustaining: boolean;
- toxins_max: number;
+ self_sustaining: BooleanLike;
toxins: number;
- water_max: number;
+ toxins_max: number;
water: number;
- weeds_max: number;
+ water_max: number;
weeds: number;
+ weeds_max: number;
yield_mod: number;
};
-type SeedData = {
+export type SeedData = {
core_traits: string[];
distill_reagent: string;
endurance: number;
@@ -56,6 +66,23 @@ type SeedData = {
weed_chance: number;
weed_rate: number;
yield: number;
+ unique_labels: UniqueSeedLabel[];
+ unique_collapsibles: UniqueSeedCollapsible[];
+};
+
+type UniqueSeedLabel = {
+ label: string;
+ data: string;
+};
+
+type UniqueSeedCollapsible = {
+ label: string;
+ // key is shown text, value is tooltip text
+ data: Record;
+};
+
+export type PlantData = {
+ reagents: ReagentVolume[];
};
type GraftData = {
@@ -73,10 +100,11 @@ type GraftData = {
type ReagentVolume = {
name: string;
- volume: string;
+ volume: number;
+ color: string;
};
-type ReagentData = {
+export type ReagentData = {
name: string;
rate: number;
};