diff --git a/code/__HELPERS/global_lists.dm b/code/__HELPERS/global_lists.dm index b8c009ff4f..9719157d4d 100644 --- a/code/__HELPERS/global_lists.dm +++ b/code/__HELPERS/global_lists.dm @@ -68,6 +68,7 @@ for(var/spath in subtypesof(/datum/species)) var/datum/species/S = new spath() GLOB.species_list[S.id] = spath + GLOB.species_datums[S.id] = S //Surgeries for(var/path in subtypesof(/datum/surgery)) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 513437e57e..92cf7050e0 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -320,6 +320,8 @@ GLOBAL_LIST_INIT(nonstandard_skin_tones, list("orange")) GLOBAL_LIST_EMPTY(species_list) +GLOBAL_LIST_EMPTY(species_datums) + /proc/age2agedescription(age) switch(age) if(0 to 1) diff --git a/code/__HELPERS/reagents.dm b/code/__HELPERS/reagents.dm index de225b3b53..fa655efce4 100644 --- a/code/__HELPERS/reagents.dm +++ b/code/__HELPERS/reagents.dm @@ -95,3 +95,9 @@ if("I'm feeling lucky") chosen_id = pick(subtypesof(/datum/reagent)) return chosen_id + +/proc/find_reagent_object_from_type(input) + if(GLOB.chemical_reagents_list[input]) //prefer IDs! + return GLOB.chemical_reagents_list[input] + else + return null diff --git a/code/game/machinery/limbgrower.dm b/code/game/machinery/limbgrower.dm index aa1d05884e..dc87322b57 100644 --- a/code/game/machinery/limbgrower.dm +++ b/code/game/machinery/limbgrower.dm @@ -1,9 +1,5 @@ -#define LIMBGROWER_MAIN_MENU 1 -#define LIMBGROWER_CATEGORY_MENU 2 -#define LIMBGROWER_CHEMICAL_MENU 3 -//use these for the menu system - - +/// The limbgrower. Makes organd and limbs with synthflesh and chems. +/// See [limbgrower_designs.dm] for everything we can make. /obj/machinery/limbgrower name = "limb grower" desc = "It grows new limbs using Synthflesh." @@ -15,161 +11,235 @@ active_power_usage = 100 circuit = /obj/item/circuitboard/machine/limbgrower - var/operating = FALSE - var/disabled = FALSE + /// The category of limbs we're browing in our UI. + var/selected_category = "human" + /// If we're currently printing something. var/busy = FALSE - var/prod_coeff = 1 + /// How efficient our machine is. Better parts = less chemicals used and less power used. Range of 1 to 0.25. + var/production_coefficient = 1 + /// How long it takes for us to print a limb. Affected by production_coefficient. + var/production_speed = 3 SECONDS + /// The design we're printing currently. var/datum/design/being_built + /// Our internal techweb for limbgrower designs. var/datum/techweb/stored_research - var/selected_category - var/screen = 1 + /// All the categories of organs we can print. var/list/categories = list( - "human" = /datum/species/human, - "lizard" = /datum/species/lizard, - "mammal" = /datum/species/mammal, - "insect" = /datum/species/insect, - "fly" = /datum/species/fly, - "plasmaman" = /datum/species/plasmaman, - "xeno" = /datum/species/xeno, - "other" = /datum/species, - ) - var/list/stored_species = list() + "human", + "lizard", + "mammal", + "insect", + "fly", + "plasmaman", + "xeno", + "other", + ) var/obj/item/disk/data/dna_disk /obj/machinery/limbgrower/Initialize() create_reagents(100, OPENCONTAINER) stored_research = new /datum/techweb/specialized/autounlocking/limbgrower - for(var/i in categories) - var/species = categories[i] - stored_species[i] = new species() . = ..() + AddComponent(/datum/component/plumbing/simple_demand) + AddComponent(/datum/component/simple_rotation, ROTATION_WRENCH | ROTATION_CLOCKWISE, null, CALLBACK(src, .proc/can_be_rotated)) -/obj/machinery/limbgrower/ui_interact(mob/user) +/obj/machinery/limbgrower/ui_interact(mob/user, datum/tgui/ui) . = ..() - if(!is_operational()) - return + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Limbgrower", src) + ui.open() - var/dat = main_win(user) +/obj/machinery/limbgrower/ui_data(mob/user) + var/list/data = list() - switch(screen) - if(LIMBGROWER_MAIN_MENU) - dat = main_win(user) - if(LIMBGROWER_CATEGORY_MENU) - dat = category_win(user,selected_category) - if(LIMBGROWER_CHEMICAL_MENU) - dat = chemical_win(user) + for(var/datum/reagent/reagent_id in reagents.reagent_list) + var/list/reagent_data = list( + reagent_name = reagent_id.name, + reagent_amount = reagent_id.volume, + reagent_type = reagent_id.type + ) + data["reagents"] += list(reagent_data) - var/datum/browser/popup = new(user, "Limb Grower", name, 400, 500) - popup.set_content(dat) - popup.open() + data["total_reagents"] = reagents.total_volume + data["max_reagents"] = reagents.maximum_volume + data["busy"] = busy + var/list/disk_data = list() + disk_data["disk"] = dna_disk //Do i, the machine, have a disk? + disk_data["name"] = dna_disk?.fields["name"] //Name for the human saved if there is one + data["disk"] = disk_data + + return data + +/obj/machinery/limbgrower/ui_static_data(mob/user) + var/list/data = list() + data["categories"] = list() + + var/species_categories = categories.Copy() + for(var/species in species_categories) + species_categories[species] = list() + for(var/design_id in stored_research.researched_designs) + var/datum/design/limb_design = SSresearch.techweb_design_by_id(design_id) + for(var/found_category in species_categories) + if(found_category in limb_design.category) + species_categories[found_category] += limb_design + + for(var/category in species_categories) + var/list/category_data = list( + name = category, + designs = list(), + ) + for(var/datum/design/found_design in species_categories[category]) + var/list/all_reagents = list() + for(var/reagent_typepath in found_design.reagents_list) + var/datum/reagent/reagent_id = find_reagent_object_from_type(reagent_typepath) + var/list/reagent_data = list( + name = reagent_id.name, + amount = (found_design.reagents_list[reagent_typepath] * production_coefficient), + ) + all_reagents += list(reagent_data) + + category_data["designs"] += list(list( + parent_category = category, + name = found_design.name, + id = found_design.id, + needed_reagents = all_reagents, + )) + + data["categories"] += list(category_data) + + return data /obj/machinery/limbgrower/on_deconstruction() - for(var/obj/item/reagent_containers/glass/G in component_parts) - reagents.trans_to(G, G.reagents.maximum_volume) + for(var/obj/item/reagent_containers/glass/our_beaker in component_parts) + reagents.trans_to(our_beaker, our_beaker.reagents.maximum_volume) ..() -/obj/machinery/limbgrower/attackby(obj/item/O, mob/user, params) - if(busy) +/obj/machinery/limbgrower/attackby(obj/item/user_item, mob/living/user, params) + if (busy) to_chat(user, "\The [src] is busy. Please wait for completion of previous operation.") return - if(default_deconstruction_screwdriver(user, "limbgrower_panelopen", "limbgrower_idleoff", O)) - updateUsrDialog() + if(default_deconstruction_screwdriver(user, "limbgrower_panelopen", "limbgrower_idleoff", user_item)) + ui_close(user) return - if(panel_open && default_deconstruction_crowbar(O)) - return - - if(user.a_intent == INTENT_HARM) //so we can hit the machine + if(user_item.tool_behaviour == TOOL_WRENCH && panel_open) return ..() - if(istype(O, /obj/item/disk)) + if(panel_open && default_deconstruction_crowbar(user_item)) + return + + if(istype(user_item, /obj/item/disk)) if(dna_disk) to_chat(user, "\The [src] already has a dna disk, take it out first!") return else - O.forceMove(src) - dna_disk = O - to_chat(user, "You insert \the [O] into \the [src].") + user_item.forceMove(src) + dna_disk = user_item + to_chat(user, "You insert \the [user_item] into \the [src].") + playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0) return -/obj/machinery/limbgrower/Topic(href, href_list) - if(..()) + if(user.a_intent != INTENT_HELP) + return ..() + +/obj/machinery/limbgrower/proc/can_be_rotated() + if(panel_open) + return TRUE + return FALSE + +/obj/machinery/limbgrower/ui_act(action, list/params) + . = ..() + if(.) return - if (!busy) - if(href_list["menu"]) - screen = text2num(href_list["menu"]) - if(href_list["category"]) - selected_category = href_list["category"] + if (busy) + to_chat(usr, "\The [src] is busy. Please wait for completion of previous operation.") + return - if(href_list["disposeI"]) //Get rid of a reagent incase you add the wrong one by mistake - reagents.del_reagent(text2path(href_list["disposeI"])) + switch(action) - if(href_list["make"]) + if("empty_reagent") + reagents.del_reagent(text2path(params["reagent_type"])) + . = TRUE - ///////////////// - //href protection - being_built = stored_research.isDesignResearchedID(href_list["make"]) //check if it's a valid design + if("eject_disk") + eject_disk(usr) + + if("make_limb") + being_built = stored_research.isDesignResearchedID(params["design_id"]) if(!being_built) - return + CRASH("[src] was passed an invalid design id!") + /// All the reagents we're using to make our organ. + var/list/consumed_reagents_list = being_built.reagents_list.Copy() + /// The amount of power we're going to use, based on how much reagent we use. + var/power = 0 - var/synth_cost = being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff - var/power = max(2000, synth_cost/5) + for(var/reagent_id in consumed_reagents_list) + consumed_reagents_list[reagent_id] *= production_coefficient + if(!reagents.has_reagent(reagent_id, consumed_reagents_list[reagent_id])) + audible_message("\The [src] buzzes.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE) + return - if(reagents.has_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)) - busy = TRUE - use_power(power) - flick("limbgrower_fill",src) - icon_state = "limbgrower_idleon" - addtimer(CALLBACK(src, .proc/build_item),32*prod_coeff) + power = max(2000, (power + consumed_reagents_list[reagent_id])) - if(href_list["dna_disk"]) - var/mob/living/carbon/user = usr - if(istype(user)) - if(!dna_disk) - var/obj/item/disk/diskette = user.get_active_held_item() - if(istype(diskette)) - diskette.forceMove(src) - dna_disk = diskette - to_chat(user, "You insert \the [diskette] into \the [src].") - else - dna_disk.forceMove(src.loc) - user.put_in_active_hand(dna_disk) - to_chat(user, "You remove \the [dna_disk] from \the [src].") - dna_disk = null - else - to_chat(user, "You are unable to grasp \the [dna_disk] disk from \the [src].") - else - to_chat(usr, "\The [src] is busy. Please wait for completion of previous operation.") + busy = TRUE + use_power(power) + flick("limbgrower_fill",src) + icon_state = "limbgrower_idleon" + selected_category = params["active_tab"] + addtimer(CALLBACK(src, .proc/build_item, consumed_reagents_list), production_speed * production_coefficient) + . = TRUE - updateUsrDialog() return -/obj/machinery/limbgrower/proc/build_item() - if(reagents.has_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)) //sanity check, if this happens we are in big trouble - reagents.remove_reagent(/datum/reagent/medicine/synthflesh, being_built.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff) - var/buildpath = being_built.build_path - if(ispath(buildpath, /obj/item/bodypart)) //This feels like spaghetti code, but i need to initiliaze a limb somehow - build_limb(buildpath) - else if(ispath(buildpath, /obj/item/organ/genital)) //genitals are uhh... customizable - build_genital(buildpath) - else - //Just build whatever it is - new buildpath(loc) - else - src.visible_message(" Something went very wrong and there isnt enough synthflesh anymore!") - busy = FALSE - flick("limbgrower_unfill",src) - icon_state = "limbgrower_idleoff" - updateUsrDialog() +/* + * The process of beginning to build a limb or organ. + * Goes through and sanity checks that we actually have enough reagent to build our item. + * Then, remove those reagents from our reagents datum. + * + * After the reagents are handled, we can proceede with making the limb or organ. (Limbs are handled in a separate proc) + * + * modified_consumed_reagents_list - the list of reagents we will consume on build, modified by the production coefficient. + */ +/obj/machinery/limbgrower/proc/build_item(list/modified_consumed_reagents_list) + for(var/reagent_id in modified_consumed_reagents_list) + if(!reagents.has_reagent(reagent_id, modified_consumed_reagents_list[reagent_id])) + audible_message("\The [src] buzzes.") + playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE) + break -/obj/machinery/limbgrower/proc/build_limb(buildpath) + reagents.remove_reagent(reagent_id, modified_consumed_reagents_list[reagent_id]) + + var/built_typepath = being_built.build_path + // If we have a bodypart, we need to initialize the limb on its own. Otherwise we can build it here. + if(ispath(built_typepath, /obj/item/bodypart)) + build_limb(built_typepath) + else if(ispath(built_typepath, /obj/item/organ/genital)) //genitals are uhh... customizable + build_genital(built_typepath) + else + new built_typepath(loc) + + busy = FALSE + flick("limbgrower_unfill", src) + icon_state = "limbgrower_idleoff" + +/* + * The process of putting together a limb. + * This is called from after we remove the reagents, so this proc is just initializing the limb type. + * + * This proc handles skin / mutant color, greyscaling, names and descriptions, and various other limb creation steps. + * + * built_typepath - the path of the bodypart we're building. + */ +/obj/machinery/limbgrower/proc/build_limb(built_typepath) //i need to create a body part manually using a set icon (otherwise it doesnt appear) var/obj/item/bodypart/limb - var/datum/species/selected = stored_species[selected_category] - limb = new buildpath(loc) + var/datum/species/selected = GLOB.species_datums[selected_category] + limb = new built_typepath(loc) limb.base_bp_icon = selected.icon_limbs || DEFAULT_BODYPART_ICON_ORGANIC limb.species_id = selected.limbs_id limb.color_src = (MUTCOLORS in selected.species_traits ? MUTCOLORS : (selected.use_skintones ? SKINTONE : FALSE)) @@ -189,135 +259,103 @@ BP.name = "\improper synthetic [lowertext(selected.name)] [limb.name]" BP.desc = "A synthetic [selected_category] limb that will morph on its first use in surgery. This one is for the [parse_zone(limb.body_zone)]." -/obj/machinery/limbgrower/proc/build_genital(buildpath) +/* + * Builds genitals, modifies to be the same + * as the person's cloning data on the data disk + */ +/obj/machinery/limbgrower/proc/build_genital(built_typepath) //i needed to create a way to customize gene tools using dna var/list/features = dna_disk?.fields["features"] if(length(features)) - switch(buildpath) + switch(built_typepath) if(/obj/item/organ/genital/penis) var/obj/item/organ/genital/penis/penis = new(loc) if(features["has_cock"]) penis.shape = features["cock_shape"] penis.length = features["cock_shape"] penis.diameter_ratio = features["cock_diameter_ratio"] - penis.color = sanitize_hexcolor(features["cock_color"], 6) - penis.update_icon() + penis.color = sanitize_hexcolor(features["cock_color"], 6, TRUE) + penis.update() if(/obj/item/organ/genital/testicles) var/obj/item/organ/genital/testicles/balls = new(loc) if(features["has_balls"]) - balls.color = sanitize_hexcolor(features["balls_color"], 6) + balls.color = sanitize_hexcolor(features["balls_color"], 6, TRUE) balls.shape = features["balls_shape"] balls.size = features["balls_size"] balls.fluid_rate = features["balls_cum_rate"] balls.fluid_mult = features["balls_cum_mult"] balls.fluid_efficiency = features["balls_efficiency"] + balls.update() if(/obj/item/organ/genital/vagina) var/obj/item/organ/genital/vagina/vegana = new(loc) - if(features["has_vagina"]) - vegana.color = sanitize_hexcolor(features["vag_color"], 6) + if(features["has_vag"]) + vegana.color = sanitize_hexcolor(features["vag_color"], 6, TRUE) vegana.shape = features["vag_shape"] + vegana.update() if(/obj/item/organ/genital/breasts) var/obj/item/organ/genital/breasts/boobs = new(loc) if(features["has_breasts"]) - boobs.color = sanitize_hexcolor(features["breasts_color"], 6) + boobs.color = sanitize_hexcolor(features["breasts_color"], 6, TRUE) boobs.size = features["breasts_size"] boobs.shape = features["breasts_shape"] if(!features["breasts_producing"]) boobs.genital_flags &= ~(GENITAL_FUID_PRODUCTION|CAN_CLIMAX_WITH|CAN_MASTURBATE_WITH) + boobs.update() else - new buildpath(loc) + new built_typepath(loc) else - new buildpath(loc) + new built_typepath(loc) /obj/machinery/limbgrower/RefreshParts() reagents.maximum_volume = 0 - for(var/obj/item/reagent_containers/glass/G in component_parts) - reagents.maximum_volume += G.volume - G.reagents.trans_to(src, G.reagents.total_volume) - var/T=1.2 - for(var/obj/item/stock_parts/manipulator/M in component_parts) - T -= M.rating*0.2 - prod_coeff = min(1,max(0,T)) // Coeff going 1 -> 0,8 -> 0,6 -> 0,4 + for(var/obj/item/reagent_containers/glass/our_beaker in component_parts) + reagents.maximum_volume += our_beaker.volume + our_beaker.reagents.trans_to(src, our_beaker.reagents.total_volume) + production_coefficient = 1.2 + for(var/obj/item/stock_parts/manipulator/our_manipulator in component_parts) + production_coefficient -= our_manipulator.rating * 0.2 + production_coefficient = clamp(production_coefficient, 0, 1) // coefficient goes from 1 -> 0.8 -> 0.6 -> 0.4 /obj/machinery/limbgrower/examine(mob/user) . = ..() + if(!panel_open) + . += "It looks like as if the panel were open you could rotate it with a wrench." + else + . += "The panel is open." if(in_range(user, src) || isobserver(user)) - . += "The status display reads: Storing up to [reagents.maximum_volume]u of synthflesh.
Synthflesh consumption at [prod_coeff*100]%." + . += "The status display reads: Storing up to [reagents.maximum_volume]u of reagents.
Reagent consumption rate at [production_coefficient * 100]%.
" -/obj/machinery/limbgrower/proc/main_win(mob/user) - var/dat = "

[src] Menu:


" - dat += "[dna_disk ? "Remove" : "Insert"] cloning data disk" - dat += "
" - dat += "Chemical Storage" - dat += materials_printout() - dat += "" - - for(var/C in categories) - dat += "" - dat += "" - //one category per line - - dat += "
[C]
" - return dat - -/obj/machinery/limbgrower/proc/category_win(mob/user,selected_category) - var/dat = "Return to main menu" - dat += "

Browsing [selected_category]:


" - dat += materials_printout() - - for(var/v in stored_research.researched_designs) - var/datum/design/D = SSresearch.techweb_design_by_id(v) - if(!(selected_category in D.category)) - continue - if(disabled || !can_build(D)) - dat += "[D.name]" - else - dat += "[D.name]" - dat += "[get_design_cost(D)]
" - - dat += "
" - return dat - - -/obj/machinery/limbgrower/proc/chemical_win(mob/user) - var/dat = "Return to main menu" - dat += "

Browsing Chemical Storage:


" - dat += materials_printout() - - for(var/datum/reagent/R in reagents.reagent_list) - dat += "[R.name]: [R.volume]" - dat += "Purge
" - - dat += "
" - return dat - -/obj/machinery/limbgrower/proc/materials_printout() - var/dat = "Total amount:> [reagents.total_volume] / [reagents.maximum_volume] cm3
" - return dat - -/obj/machinery/limbgrower/proc/can_build(datum/design/D) - return (reagents.has_reagent(/datum/reagent/medicine/synthflesh, D.reagents_list[/datum/reagent/medicine/synthflesh]*prod_coeff)) //Return whether the machine has enough synthflesh to produce the design - -/obj/machinery/limbgrower/proc/get_design_cost(datum/design/D) - var/dat - if(D.reagents_list[/datum/reagent/medicine/synthflesh]) - dat += "[D.reagents_list[/datum/reagent/medicine/synthflesh] * prod_coeff] Synthetic flesh " - return dat +/* + * Checks our reagent list to see if a design can be built. + * + * limb_design - the design we're checking for buildability. + * + * returns TRUE if we have enough reagent to build it. Returns FALSE if we do not. + */ +/obj/machinery/limbgrower/proc/can_build(datum/design/limb_design) + for(var/datum/reagent/reagent_id in limb_design.reagents_list) + if(!reagents.has_reagent(reagent_id, limb_design.reagents_list[reagent_id] * production_coefficient)) + return FALSE + return TRUE +/// Emagging a limbgrower allows you to build synthetic armblades. /obj/machinery/limbgrower/emag_act(mob/user) . = ..() if(obj_flags & EMAGGED) return - for(var/id in SSresearch.techweb_designs) - var/datum/design/D = SSresearch.techweb_design_by_id(id) - if((D.build_type & LIMBGROWER) && ("emagged" in D.category)) - stored_research.add_design(D) + for(var/design_id in SSresearch.techweb_designs) + var/datum/design/found_design = SSresearch.techweb_design_by_id(design_id) + if((found_design.build_type & LIMBGROWER) && ("emagged" in found_design.category)) + stored_research.add_design(found_design) to_chat(user, "A warning flashes onto the screen, stating that safety overrides have been deactivated!") obj_flags |= EMAGGED - return TRUE + update_static_data(user) /obj/machinery/limbgrower/AltClick(mob/living/user) . = ..() + eject_disk(user) + +/obj/machinery/limbgrower/proc/eject_disk(mob/user) if(istype(user) && user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK)) if(busy) to_chat(user, "\The [src] is busy. Please wait for completion of previous operation.") @@ -326,6 +364,7 @@ dna_disk.forceMove(src.loc) user.put_in_active_hand(dna_disk) to_chat(user, "You remove \the [dna_disk] from \the [src].") + playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0) dna_disk = null else to_chat(user, "\The [src] has doesn't have a disk on it!") diff --git a/tgui/packages/tgui/interfaces/Limbgrower.js b/tgui/packages/tgui/interfaces/Limbgrower.js new file mode 100644 index 0000000000..61aa5c8a02 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Limbgrower.js @@ -0,0 +1,109 @@ +import { useBackend, useSharedState } from '../backend'; +import { Box, Button, Dimmer, Icon, LabeledList, Section, Tabs } from '../components'; +import { Window } from '../layouts'; + +export const Limbgrower = (props, context) => { + const { act, data } = useBackend(context); + const { + reagents = [], + total_reagents, + max_reagents, + categories = [], + busy, + disk = [], + } = data; + const [tab, setTab] = useSharedState( + context, 'category', categories[0]?.name); + const designList = categories + .find(category => category.name === tab) + ?.designs; + + return ( + + {!!busy && ( + + + {' Building...'} + + )} + +
act('eject_disk')} + disabled={!disk['disk']} + /> + }> + {disk['name'] ? ( +
+ Containing data for {disk['name']},
+ Attempting to create genitalia will use the disk's data. +
+ ) : disk['disk'] ? "No data." : "No disk."} +
+
+ + {total_reagents} / {max_reagents} reagent capacity used. + + + {reagents.map(reagent => ( + act('empty_reagent', { + reagent_type: reagent.reagent_type, + })} /> + )}> + {reagent.reagent_amount}u + + ))} + +
+
+ + {categories.map(category => ( + setTab(category.name)}> + {category.name} + + ))} + + + {designList.map(design => ( + act('make_limb', { + design_id: design.id, + active_tab: design.parent_category, + })} /> + )}> + {design.needed_reagents.map(reagent => ( + + {reagent.name}: {reagent.amount}u + + ))} + + ))} + +
+
+
+ ); +};