From 67b19027b244776ad82a92a4ebede89bb00db259 Mon Sep 17 00:00:00 2001 From: RikuTheKiller <88713943+RikuTheKiller@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:41:59 +0300 Subject: [PATCH] Add Botanical Encyclopedia app for browsing knowledge about plants (#96652) ## About The Pull Request The app comes pre-installed on all botanist PDAs and can be manually installed by anyone on any device. I extracted the seed extractor UI into a seed table component that the botanical encyclopedia now also uses. I also made the seed extractor Scrap and Take buttons separate instead of sharing a button with a toggle. I'm not great at UI work so expect some rough edges and bugs. It does work in testing. Video of me testing the botanical encyclopedia: https://streamable.com/um3yul Video of me testing the seed extractor: https://streamable.com/5fn3ll ## Why It's Good For The Game It allows players to access plant information without code-diving or using the wiki. ## Changelog :cl: add: Added Botanical Encyclopedia app for browsing knowledge about plants, which comes pre-installed on all botanist PDAs. /:cl: --- code/__DEFINES/modular_computer.dm | 1 + code/__HELPERS/botany.dm | 55 ++ .../items/devices/scanners/plant_analyzer.dm | 2 +- code/modules/asset_cache/assets/seeds.dm | 13 - code/modules/hydroponics/seed_extractor.dm | 43 +- .../modules/hydroponics/unique_plant_genes.dm | 8 +- .../computers/item/role_tablet_presets.dm | 3 + .../programs/botanical_encyclopedia.dm | 52 ++ .../file_system/programs/ntdownloader.dm | 1 + tgstation.dme | 3 +- .../interfaces/NtosBotanicalEncyclopedia.tsx | 25 + .../tgui/interfaces/PlantAnalyzer/Graft.tsx | 2 +- .../tgui/interfaces/SeedExtractor.tsx | 470 +----------------- tgui/packages/tgui/interfaces/SeedTable.tsx | 439 ++++++++++++++++ 14 files changed, 603 insertions(+), 514 deletions(-) create mode 100644 code/__HELPERS/botany.dm delete mode 100644 code/modules/asset_cache/assets/seeds.dm create mode 100644 code/modules/modular_computers/file_system/programs/botanical_encyclopedia.dm create mode 100644 tgui/packages/tgui/interfaces/NtosBotanicalEncyclopedia.tsx create mode 100644 tgui/packages/tgui/interfaces/SeedTable.tsx diff --git a/code/__DEFINES/modular_computer.dm b/code/__DEFINES/modular_computer.dm index 16b0996a78f..e80d91f4bb4 100644 --- a/code/__DEFINES/modular_computer.dm +++ b/code/__DEFINES/modular_computer.dm @@ -39,6 +39,7 @@ #define PROGRAM_CATEGORY_ENGINEERING "Engineering" #define PROGRAM_CATEGORY_SUPPLY "Supply" #define PROGRAM_CATEGORY_SCIENCE "Science" +#define PROGRAM_CATEGORY_SERVICE "Service" ///The default amount a program should take in cell use. #define PROGRAM_BASIC_CELL_USE 2 WATTS diff --git a/code/__HELPERS/botany.dm b/code/__HELPERS/botany.dm new file mode 100644 index 00000000000..33daa527b85 --- /dev/null +++ b/code/__HELPERS/botany.dm @@ -0,0 +1,55 @@ +/// Returns a list of seed data generated from the given seeds. +/// Filling the "name", "icon" and "icon_state" fields of the data is the caller's responsibility. +/proc/generate_seed_data_from(obj/item/seeds/seeds) + var/list/seed_data = list() + + seed_data["lifespan"] = seeds.lifespan + seed_data["endurance"] = seeds.endurance + seed_data["maturation"] = seeds.maturation + seed_data["production"] = seeds.production + seed_data["yield"] = seeds.yield + seed_data["potency"] = seeds.potency + seed_data["instability"] = seeds.instability + + seed_data["traits"] = list() + for(var/datum/plant_gene/trait/trait in seeds.genes) + seed_data["traits"] += trait.type + + seed_data["reagents"] = list() + for(var/datum/plant_gene/reagent/reagent in seeds.genes) + seed_data["reagents"] += list(list( + "name" = reagent.name, + "rate" = reagent.rate + )) + + var/datum/plant_gene/trait/maxchem/volume_mod_trait = locate(/datum/plant_gene/trait/maxchem) in seeds.genes + seed_data["volume_mod"] = volume_mod_trait ? volume_mod_trait.rate : 1 + + var/datum/plant_gene/trait/modified_volume/volume_unit_trait = locate(/datum/plant_gene/trait/modified_volume) in seeds.genes + seed_data["volume_units"] = volume_unit_trait ? volume_unit_trait.new_capacity : PLANT_REAGENT_VOLUME + + seed_data["mutatelist"] = list() + for(var/obj/item/seeds/mutant as anything in seeds.mutatelist) + seed_data["mutatelist"] += initial(mutant.plantname) + + if(ispath(seeds.product, /obj/item)) + var/obj/item/product = new seeds.product + + seed_data["grind_results"] = list() + for(var/datum/reagent/reagent as anything in product.grind_results()) + seed_data["grind_results"] += initial(reagent.name) + + if(istype(product, /obj/item/food/grown)) + var/obj/item/food/grown/grown_product = product + + var/datum/reagent/distill_reagent = grown_product.distill_reagent + if (ispath(distill_reagent, /datum/reagent)) + seed_data["distill_reagent"] = initial(distill_reagent.name) + + var/datum/reagent/juice_reagent = grown_product.juice_typepath() + if (ispath(juice_reagent, /datum/reagent)) + seed_data["juice_name"] = initial(juice_reagent.name) + + qdel(product) + + return seed_data diff --git a/code/game/objects/items/devices/scanners/plant_analyzer.dm b/code/game/objects/items/devices/scanners/plant_analyzer.dm index 97bd1630d63..d4a12361a0c 100644 --- a/code/game/objects/items/devices/scanners/plant_analyzer.dm +++ b/code/game/objects/items/devices/scanners/plant_analyzer.dm @@ -320,7 +320,7 @@ var/datum/plant_gene/trait/maxchem/volume_trait = locate(/datum/plant_gene/trait/maxchem) in seed.genes var/datum/plant_gene/trait/modified_volume/volume_unit_trait = locate(/datum/plant_gene/trait/modified_volume) in seed.genes seed_data["volume_mod"] = volume_trait ? volume_trait.rate : 1 - seed_data["volume_units"] = volume_unit_trait ? volume_unit_trait.new_capcity : PLANT_REAGENT_VOLUME + seed_data["volume_units"] = volume_unit_trait ? volume_unit_trait.new_capacity : PLANT_REAGENT_VOLUME seed_data["mutatelist"] = list() for(var/obj/item/seeds/mutant as anything in seed.mutatelist) seed_data["mutatelist"] += initial(mutant.plantname) diff --git a/code/modules/asset_cache/assets/seeds.dm b/code/modules/asset_cache/assets/seeds.dm deleted file mode 100644 index 8def22ee44b..00000000000 --- a/code/modules/asset_cache/assets/seeds.dm +++ /dev/null @@ -1,13 +0,0 @@ -/datum/asset/spritesheet_batched/seeds - name = "seeds" - -/datum/asset/spritesheet_batched/seeds/create_spritesheets() - var/list/id_list = list() - for (var/obj/item/seeds/seed_type as anything in subtypesof(/obj/item/seeds)) - var/icon = initial(seed_type.icon) - var/icon_state = initial(seed_type.icon_state) - var/id = sanitize_css_class_name("[icon][icon_state]") - if(id in id_list) //no dupes - continue - id_list += id - insert_icon(id, uni_icon(icon, icon_state)) diff --git a/code/modules/hydroponics/seed_extractor.dm b/code/modules/hydroponics/seed_extractor.dm index 5d29760e4bf..52ab1882842 100644 --- a/code/modules/hydroponics/seed_extractor.dm +++ b/code/modules/hydroponics/seed_extractor.dm @@ -182,43 +182,11 @@ if(piles[seed_id]) has_seed_data = TRUE else - seed_data = list() - seed_data["icon"] = sanitize_css_class_name("[initial(to_add.icon)][initial(to_add.icon_state)]") + seed_data = generate_seed_data_from(to_add) seed_data["name"] = capitalize(replacetext(to_add.name,"pack of ", "")); - seed_data["lifespan"] = to_add.lifespan - seed_data["endurance"] = to_add.endurance - seed_data["maturation"] = to_add.maturation - seed_data["production"] = to_add.production - seed_data["yield"] = to_add.yield - seed_data["potency"] = to_add.potency - seed_data["instability"] = to_add.instability + seed_data["icon"] = initial(to_add.icon) + seed_data["icon_state"] = initial(to_add.icon_state) seed_data["refs"] = list(WEAKREF(to_add)) - seed_data["traits"] = list() - for(var/datum/plant_gene/trait/trait in to_add.genes) - seed_data["traits"] += trait.type - seed_data["reagents"] = list() - for(var/datum/plant_gene/reagent/reagent in to_add.genes) - seed_data["reagents"] += list(list( - "name" = reagent.name, - "rate" = reagent.rate - )) - var/datum/plant_gene/trait/maxchem/volume_trait = locate(/datum/plant_gene/trait/maxchem) in to_add.genes - var/datum/plant_gene/trait/modified_volume/volume_unit_trait = locate(/datum/plant_gene/trait/modified_volume) in to_add.genes - seed_data["volume_mod"] = volume_trait ? volume_trait.rate : 1 - seed_data["volume_units"] = volume_unit_trait ? volume_unit_trait.new_capcity : PLANT_REAGENT_VOLUME - seed_data["mutatelist"] = list() - for(var/obj/item/seeds/mutant as anything in to_add.mutatelist) - seed_data["mutatelist"] += initial(mutant.plantname) - if(to_add.product) - var/obj/item/food/grown/product = new to_add.product - var/datum/reagent/product_distill_reagent = product.distill_reagent - seed_data["distill_reagent"] = initial(product_distill_reagent.name) - var/datum/reagent/product_juice_typepath = product.juice_typepath() - seed_data["juice_name"] = initial(product_juice_typepath.name) - seed_data["grind_results"] = list() - for(var/datum/reagent/reagent as anything in product.grind_results()) - seed_data["grind_results"] += initial(reagent.name) - qdel(product) if(!isnull(taking_from)) if(ismob(taking_from)) @@ -307,8 +275,3 @@ found_seed.forceMove(drop_location()) visible_message(span_notice("[found_seed] falls onto the floor."), null, span_hear("You hear a soft clatter."), COMBAT_MESSAGE_RANGE) . = TRUE - -/obj/machinery/seed_extractor/ui_assets(mob/user) - return list( - get_asset_datum(/datum/asset/spritesheet_batched/seeds) - ) diff --git a/code/modules/hydroponics/unique_plant_genes.dm b/code/modules/hydroponics/unique_plant_genes.dm index ad65e5f3a8e..f0f71b8a9e8 100644 --- a/code/modules/hydroponics/unique_plant_genes.dm +++ b/code/modules/hydroponics/unique_plant_genes.dm @@ -479,7 +479,7 @@ description = "It has extra reagent volume." icon = FA_ICON_VIALS /// The new number we set the plant's max_volume to. - var/new_capcity = 100 + var/new_capacity = 100 /datum/plant_gene/trait/modified_volume/on_new_plant(obj/item/our_plant, newloc) . = ..() @@ -488,20 +488,20 @@ var/obj/item/food/grown/grown_plant = our_plant if(istype(grown_plant)) - grown_plant.max_volume = new_capcity + grown_plant.max_volume = new_capacity /// Omegaweed's funny 420 max volume gene /datum/plant_gene/trait/modified_volume/omega_weed name = "Dank Vesicles" description = "It can hold up to 420 units of reagents." icon = FA_ICON_CANNABIS - new_capcity = 420 + new_capacity = 420 /// Cherry Bomb's increased max volume gene /datum/plant_gene/trait/modified_volume/cherry_bomb name = "Powder-Filled Bulbs" description = "It can hold up to 125 units of reagents." - new_capcity = 125 + new_capacity = 125 /// Plants that explode when used (based on their reagent contents) /datum/plant_gene/trait/bomb_plant diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm index 47e10bf5372..e24e1b8ae84 100644 --- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm +++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm @@ -326,6 +326,9 @@ icon_state = "/obj/item/modular_computer/pda/botanist" greyscale_config = /datum/greyscale_config/tablet/stripe_thick greyscale_colors = "#50E193#E26F41#71A7CA" + starting_programs = list( + /datum/computer_file/program/botanical_encyclopedia + ) /obj/item/modular_computer/pda/cook name = "cook PDA" diff --git a/code/modules/modular_computers/file_system/programs/botanical_encyclopedia.dm b/code/modules/modular_computers/file_system/programs/botanical_encyclopedia.dm new file mode 100644 index 00000000000..ded932f8434 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/botanical_encyclopedia.dm @@ -0,0 +1,52 @@ +/datum/computer_file/program/botanical_encyclopedia + filename = "botanyapp" + filedesc = "Botanical Encyclopedia" + downloader_category = PROGRAM_CATEGORY_SERVICE + program_open_overlay = "bountyboard" + extended_desc = "A program for browsing knowledge about plants." + size = 2 + tgui_id = "NtosBotanicalEncyclopedia" + program_icon = FA_ICON_SEEDLING + + /// Lazy list of all seed data for valid seed types. + /// Generated when first needed to display the UI. + var/static/list/all_seed_data = null + +/datum/computer_file/program/botanical_encyclopedia/proc/generate_all_seed_data() + all_seed_data = list() + + for(var/obj/item/seeds/seed_type as anything in valid_subtypesof(/obj/item/seeds)) + if (ispath(seed_type, /obj/item/seeds/random)) + continue + + var/obj/item/seeds/seeds = new seed_type + + var/list/seed_data = generate_seed_data_from(seeds) + + seed_data["name"] = full_capitalize(initial(seed_type.plantname)) + seed_data["icon"] = initial(seed_type.growing_icon) + seed_data["icon_state"] = initial(seed_type.icon_harvest) || "[initial(seed_type.species)]-harvest" + + all_seed_data += list(seed_data) + + qdel(seeds) + +/datum/computer_file/program/botanical_encyclopedia/ui_static_data(mob/user) + if(isnull(all_seed_data)) + generate_all_seed_data() + + var/list/data = list() + + data["seeds"] = all_seed_data + data["cycle_seconds"] = HYDROTRAY_CYCLE_DELAY / 10 + data["trait_db"] = list() + + for(var/datum/plant_gene/trait as anything in GLOB.plant_traits) + data["trait_db"] += list(list( + "path" = trait.type, + "name" = trait.get_name(), + "icon" = trait.icon, + "description" = trait.description + )) + + return data diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm index 5e665962717..08dc6217f8b 100644 --- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm +++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm @@ -30,6 +30,7 @@ PROGRAM_CATEGORY_ENGINEERING, PROGRAM_CATEGORY_SUPPLY, PROGRAM_CATEGORY_SCIENCE, + PROGRAM_CATEGORY_SERVICE, ) /datum/computer_file/program/ntnetdownload/kill_program(mob/user) diff --git a/tgstation.dme b/tgstation.dme index f04048858ea..0bfd51ca163 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -444,6 +444,7 @@ #include "code\__HELPERS\atoms.dm" #include "code\__HELPERS\auxtools.dm" #include "code\__HELPERS\bitflag_lists.dm" +#include "code\__HELPERS\botany.dm" #include "code\__HELPERS\chat.dm" #include "code\__HELPERS\chat_filter.dm" #include "code\__HELPERS\clients.dm" @@ -3743,7 +3744,6 @@ #include "code\modules\asset_cache\assets\research_designs.dm" #include "code\modules\asset_cache\assets\rtd.dm" #include "code\modules\asset_cache\assets\safe.dm" -#include "code\modules\asset_cache\assets\seeds.dm" #include "code\modules\asset_cache\assets\sheetmaterials.dm" #include "code\modules\asset_cache\assets\slot_machines.dm" #include "code\modules\asset_cache\assets\supplypods.dm" @@ -5853,6 +5853,7 @@ #include "code\modules\modular_computers\file_system\programs\atmosscan.dm" #include "code\modules\modular_computers\file_system\programs\betting.dm" #include "code\modules\modular_computers\file_system\programs\borg_monitor.dm" +#include "code\modules\modular_computers\file_system\programs\botanical_encyclopedia.dm" #include "code\modules\modular_computers\file_system\programs\bounty_board.dm" #include "code\modules\modular_computers\file_system\programs\budgetordering.dm" #include "code\modules\modular_computers\file_system\programs\card.dm" diff --git a/tgui/packages/tgui/interfaces/NtosBotanicalEncyclopedia.tsx b/tgui/packages/tgui/interfaces/NtosBotanicalEncyclopedia.tsx new file mode 100644 index 00000000000..06d9be134b0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosBotanicalEncyclopedia.tsx @@ -0,0 +1,25 @@ +import { NtosWindow } from '../layouts'; +import { useBackend } from '../backend'; +import { SeedTable, SeedData, TraitData } from './SeedTable'; + +type Data = { + seeds: SeedData[]; + trait_db: TraitData[]; + cycle_seconds: number; +} + +export const NtosBotanicalEncyclopedia = (props) => { + const { data } = useBackend(); + + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx b/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx index 87b14fe667b..29d92a315f2 100644 --- a/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx +++ b/tgui/packages/tgui/interfaces/PlantAnalyzer/Graft.tsx @@ -8,7 +8,7 @@ import { import { capitalizeFirst } from 'tgui-core/string'; import { useBackend } from '../../backend'; -import { TraitTooltip } from '../SeedExtractor'; +import { TraitTooltip } from '../SeedTable'; import { Fallback } from './Fallback'; import type { PlantAnalyzerData } from './types'; diff --git a/tgui/packages/tgui/interfaces/SeedExtractor.tsx b/tgui/packages/tgui/interfaces/SeedExtractor.tsx index f3b857c805c..de55283d28f 100644 --- a/tgui/packages/tgui/interfaces/SeedExtractor.tsx +++ b/tgui/packages/tgui/interfaces/SeedExtractor.tsx @@ -1,472 +1,34 @@ -import { sortBy } from 'es-toolkit'; -import { useState } from 'react'; -import { - Box, - Button, - Icon, - Input, - NoticeBox, - ProgressBar, - Section, - Table, - Tooltip, -} from 'tgui-core/components'; -import { classes } from 'tgui-core/react'; -import { createSearch } from 'tgui-core/string'; - +import { Button } from 'tgui-core/components'; import { useBackend } from '../backend'; import { Window } from '../layouts'; - -type TraitData = { - path: string; - name: string; - icon: string; - description: string; -}; - -type ReagentData = { - name: string; - rate: number; -}; - -type SeedData = { - key: string; - amount: number; - name: string; - lifespan: number; - endurance: number; - maturation: number; - production: number; - yield: number; - potency: number; - instability: number; - icon: string; - volume_mod: number; - volume_units: number; - traits: string[]; - reagents: ReagentData[]; - mutatelist: string[]; - grind_results: string[]; - distill_reagent: string; - juice_name: string; -}; +import { SeedTable, type SeedData, type TraitData } from './SeedTable'; type SeedExtractorData = { - // Dynamic seeds: SeedData[]; - // Static trait_db: TraitData[]; cycle_seconds: number; }; export const SeedExtractor = (props) => { const { act, data } = useBackend(); - const [searchText, setSearchText] = useState(''); - const [sortField, setSortField] = useState('name'); - const [action, toggleAction] = useState(true); - const search = createSearch(searchText, (item: SeedData) => item.name); - const seeds_filtered = - searchText.length > 0 ? data.seeds.filter(search) : data.seeds; - const seeds = sortBy(seeds_filtered || [], [ - (item: SeedData) => item[sortField as keyof SeedData], - ]); - sortField !== 'name' && seeds.reverse(); return ( - + -
- - - - - - - - setSortField('potency')} - > - PTN - - - - - - setSortField('yield')} - > - YLD - - - - - - setSortField('instability')} - > - INS - - - - - - setSortField('endurance')} - > - END - - - - - - setSortField('lifespan')} - > - LFS - - - - - - setSortField('maturation')} - > - MTR - - - - - - setSortField('production')} - > - PRD - - - - - {sortField !== 'name' && ( - -
- {seeds.length === 0 && ( - - No seeds found. - - )} -
+ + <> +