diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 87d600be4b..a74bdbfeb6 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -567,6 +567,12 @@ GLOBAL_LIST_INIT(all_volume_channels, list( #define SHELTER_DEPLOY_ANCHORED_OBJECTS "anchored objects" #define SHELTER_DEPLOY_SHIP_SPACE "ship not in space" +// Borg hypo injection checks +#define BORGHYPO_STATUS_CONTAINERFULL "container full" +#define BORGHYPO_STATUS_NOCHARGE "not enough charge" +#define BORGHYPO_STATUS_NORECIPE "recipe not found" +#define BORGHYPO_STATUS_SUCCESS "success" + #define PTO_SECURITY "Security" #define PTO_MEDICAL "Medical" #define PTO_ENGINEERING "Engineering" diff --git a/code/game/mecha/equipment/weapons/weapons.dm b/code/game/mecha/equipment/weapons/weapons.dm index 3e215d516e..2cef0b4053 100644 --- a/code/game/mecha/equipment/weapons/weapons.dm +++ b/code/game/mecha/equipment/weapons/weapons.dm @@ -33,12 +33,7 @@ chassis.visible_message(span_warning("[chassis] fires [src]!")) occupant_message(span_warning("You fire [src]!")) src.mecha_log_message("Fired from [src], targeting [target].") - var/target_for_log = "unknown" - if(ismob(target)) - target_for_log = target - else if(target) - target_for_log = "[target.name]" - add_attack_logs(chassis.occupant,target_for_log,"Fired exosuit weapon [src.name] (MANUAL)") + add_attack_logs(chassis.occupant,target,"Fired exosuit weapon [src.name] (MANUAL)") for(var/i = 1 to min(projectiles, projectiles_per_shot)) var/turf/aimloc = targloc diff --git a/code/game/objects/items/toys/toy_customizable.dm b/code/game/objects/items/toys/toy_customizable.dm index 0a8105606e..94e6475775 100644 --- a/code/game/objects/items/toys/toy_customizable.dm +++ b/code/game/objects/items/toys/toy_customizable.dm @@ -150,16 +150,18 @@ if("import_config") . = TRUE var/our_data = params["config"] - var/imported_color = sanitize_hexcolor(our_data["base_color"]) - if(imported_color) - base_color = imported_color - set_new_name(our_data["name"]) + base_color = sanitize_hexcolor(our_data["base_color"]) + var/new_name = sanitize_name(our_data["name"]) + if(new_name) + set_new_name(new_name) added_overlays.Cut() if(!possible_overlays) return for(var/overlay in our_data["overlays"]) if(possible_overlays.Find(overlay["icon_state"])) - added_overlays[overlay["icon_state"]] = list( color = overlay["color"], alpha = overlay["alpha"] ) + var/new_color = sanitize_hexcolor(overlay["color"]) + var/new_alpha = CLAMP(text2num(overlay["alpha"]), 0, 255) + added_overlays[overlay["icon_state"]] = list(color = new_color, alpha = new_alpha) update_icon() if("clear") diff --git a/code/modules/mob/living/silicon/robot/dogborg/dog_defense_modules.dm b/code/modules/mob/living/silicon/robot/dogborg/dog_defense_modules.dm index 89c38a6514..1291279f98 100644 --- a/code/modules/mob/living/silicon/robot/dogborg/dog_defense_modules.dm +++ b/code/modules/mob/living/silicon/robot/dogborg/dog_defense_modules.dm @@ -63,7 +63,7 @@ if(C.brute_damage == 0 && C.electronics_damage == 0) to_chat(R, span_notice("Repair of [C] completed.")) return - if(!R.use_direct_power(power_tick, 500)) //We don't want to drain ourselves too far down during exploration + if(!R.use_direct_power(power_tick, 500)) //We don't want to drain ourselves too far down during exploration to_chat(R, span_warning("Not enough power to initialize the repair system.")) return if(do_after(R, tick_delay, target = R)) diff --git a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm index 649ea0c683..ed542f47fb 100644 --- a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm +++ b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm @@ -249,9 +249,7 @@ UNTYPED_LIST_ADD(robot_chems, list("id" = possible_reagent.id, "name" = possible_reagent.name)) data["name"] = name - var/robot_theme = robot_user.get_ui_theme() - if(robot_theme) - data["theme"] = robot_theme + data["theme"] = robot_user.get_ui_theme() data["chems"] = robot_chems return data diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 97a42d869f..ae40e39830 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -1217,7 +1217,7 @@ if(cell.charge - (amount + lower_limit) <= 0) return FALSE - cell.charge -= amount + cell.use(amount) return TRUE /mob/living/silicon/robot/binarycheck() diff --git a/code/modules/mob/living/silicon/robot/robot_ui.dm b/code/modules/mob/living/silicon/robot/robot_ui.dm index aa673cd700..a07a0d857e 100644 --- a/code/modules/mob/living/silicon/robot/robot_ui.dm +++ b/code/modules/mob/living/silicon/robot/robot_ui.dm @@ -49,10 +49,8 @@ var/mob/living/silicon/robot/R = host data["module_name"] = R.module ? "[R.module]" : null - if(R.emagged) - data["theme"] = "syndicate" - else if (R.ui_theme) - data["theme"] = R.ui_theme + + data["theme"] = R.get_ui_theme() if(!R.module) return data diff --git a/code/modules/mob/living/silicon/robot/robot_ui_decals.dm b/code/modules/mob/living/silicon/robot/robot_ui_decals.dm index f10c52e69b..50024bace8 100644 --- a/code/modules/mob/living/silicon/robot/robot_ui_decals.dm +++ b/code/modules/mob/living/silicon/robot/robot_ui_decals.dm @@ -25,9 +25,7 @@ var/mob/living/silicon/robot/R = host data["active_decals"] = R.robotdecal_on - var/robot_theme = R.get_ui_theme() - if(robot_theme) - data["theme"] = robot_theme + data["theme"] = R.get_ui_theme() return data diff --git a/code/modules/mob/living/silicon/robot/robot_ui_module.dm b/code/modules/mob/living/silicon/robot/robot_ui_module.dm index 96d84514fe..3fa820e4da 100644 --- a/code/modules/mob/living/silicon/robot/robot_ui_module.dm +++ b/code/modules/mob/living/silicon/robot/robot_ui_module.dm @@ -57,10 +57,7 @@ modules |= module_name data["possible_modules"] = modules data["mind_name"] = R.mind.name - if(R.emagged) - data["theme"] = "syndicate" - else if (R.ui_theme) - data["theme"] = R.ui_theme + data["theme"] = R.get_ui_theme() return data diff --git a/code/modules/reagents/machinery/dispenser/dispenser2.dm b/code/modules/reagents/machinery/dispenser/dispenser2.dm index bbee022c51..d1d328c40d 100644 --- a/code/modules/reagents/machinery/dispenser/dispenser2.dm +++ b/code/modules/reagents/machinery/dispenser/dispenser2.dm @@ -202,6 +202,20 @@ container = null . = TRUE + if("import_config") + var/list/our_data = params["config"] + if(!islist(our_data)) + return FALSE + var/list/new_recipes = list() + for(var/key, value in our_data) + if(istext(key) && islist(value)) + for(var/list/steps in value) + if(istext(steps["id"]) && isnum(steps["amount"])) + new_recipes[key] += list(list("id" = steps["id"], "amount" = steps["amount"])) + if(length(new_recipes)) + saved_recipes = new_recipes + . = TRUE + if("record_recipe") recording_recipe = list() . = TRUE @@ -260,7 +274,7 @@ var/amount_actually_dispensed = C.reagents.trans_to(container, dispense_amount) if(dispense_amount != amount_actually_dispensed) visible_message(span_warning("[src] buzzes."), span_warning("You hear a faint buzz.")) - to_chat(ui.user, span_warning("[src] was only able to dispense [amount_actually_dispensed]u out of [dispense_amount]u requested of [label]!")) + to_chat(ui.user, span_warning("[src] was only able to dispense [amount_actually_dispensed ? amount_actually_dispensed : 0]u out of [dispense_amount]u requested of [label]!")) playsound(src, 'sound/machines/buzz-two.ogg', 50, TRUE) break else diff --git a/code/modules/reagents/reagent_containers/borghypo.dm b/code/modules/reagents/reagent_containers/borghypo.dm index 536bed9bc6..a71bad8cf3 100644 --- a/code/modules/reagents/reagent_containers/borghypo.dm +++ b/code/modules/reagents/reagent_containers/borghypo.dm @@ -5,18 +5,39 @@ item_state = "hypo" icon_state = "borghypo" amount_per_transfer_from_this = 5 + min_transfer_amount = 1 volume = 30 - max_transfer_amount = null + max_transfer_amount = 10 + /// The single chemical we have currently selected. Used to index `reagent_volumes`, `reagent_names`, and `reagent_ids`. var/mode = 1 - var/charge_cost = 325 + /// Amount of power this hypo will remove from the robot user's internal cell when a reagent's stores are replenished. + var/charge_cost = 325 // CHOMPEdit var/charge_tick = 0 - var/recharge_time = 5 //Time it takes for shots to recharge (in seconds) - var/bypass_protection = FALSE // If true, can inject through things like spacesuits and armor. + /// Time it takes for shots to recharge (in seconds) + var/recharge_time = 5 + /// If true, can inject through things like spacesuits and armor. + var/bypass_protection = FALSE + /// Affects whether the TGUI will display itself as a chem or drink dispenser. + var/is_dispensing_drinks = FALSE + /// String contents in the TGUI search bar. + var/ui_chemical_search + /// Whether or not we're dispensing just a single reagent or are dispensing multiple reagents via a recipe + var/is_dispensing_recipe = FALSE + /// The recipe we will dispense if `is_dispensing_recipe` is `TRUE` + var/selected_recipe_id + var/hypo_sound = 'sound/effects/hypospray.ogg' // What sound do we play on use? var/list/reagent_ids = list(REAGENT_ID_TRICORDRAZINE, REAGENT_ID_INAPROVALINE, REAGENT_ID_BICARIDINE, REAGENT_ID_ANTITOXIN, REAGENT_ID_KELOTANE, REAGENT_ID_TRAMADOL, REAGENT_ID_DEXALIN, REAGENT_ID_SPACEACILLIN) // CHOMPEdit var/list/reagent_volumes = list() + /// Associated list of the names of each of our reagents. Indexed via `mode`. var/list/reagent_names = list() + /// If we're currently recording a recipe, this will be set to a list containing the recipe's steps. + var/list/recording_recipe + /// Associated list of the recipes we have saved. Indexed via the string ID of the recipe. + var/list/saved_recipes = list() + /// In the hypo's TGUI, this determines the amount buttons that will be available to change this hypo's transfer amount. + var/list/transfer_amounts = list(5, 10) /obj/item/reagent_containers/borghypo/surgeon reagent_ids = list(REAGENT_ID_INAPROVALINE, REAGENT_ID_DEXALIN, REAGENT_ID_TRICORDRAZINE, REAGENT_ID_SPACEACILLIN, REAGENT_ID_OXYCODONE) @@ -34,13 +55,54 @@ bypass_protection = TRUE // Because mercs tend to be in spacesuits. reagent_ids = list(REAGENT_ID_HEALINGNANITES, REAGENT_ID_HYPERZINE, REAGENT_ID_TRAMADOL, REAGENT_ID_OXYCODONE, REAGENT_ID_SPACEACILLIN, REAGENT_ID_PERIDAXON, REAGENT_ID_OSTEODAXON, REAGENT_ID_MYELAMINE, REAGENT_ID_SYNTHBLOOD) +/// Performs a single reagent addition. Returns its success (or error) status at doing so. +/obj/item/reagent_containers/borghypo/proc/try_add_reagent(var/datum/reagents/target_reagents, var/mob/user, var/reagent_id, var/amount) + var/reagent_volume = reagent_volumes[reagent_id] + if(!reagent_volume || reagent_volume < amount) + return BORGHYPO_STATUS_NOCHARGE + + if(!target_reagents.get_free_space()) + return BORGHYPO_STATUS_CONTAINERFULL + + if(hypo_sound) + playsound(src, hypo_sound, 25, TRUE) + + var/amount_to_add = min(amount, reagent_volumes[reagent_id]) + target_reagents.add_reagent(reagent_id, amount_to_add) + reagent_volumes[reagent_id] -= amount_to_add + return BORGHYPO_STATUS_SUCCESS + +/// Attempts to add one reagent or multiple reagents, depending on if this hypo is currently set to dispense a recipe, (see `is_dispensing_recipe`.) Returns its success (or error) status at doing so. +/obj/item/reagent_containers/borghypo/proc/try_injection(var/datum/reagents/target_reagents, var/mob/user) + if(is_dispensing_recipe && selected_recipe_id) + // Add reagents with our selected ID + var/foundRecipe = saved_recipes[selected_recipe_id] + if(!foundRecipe) + to_chat(user, span_warning("Couldn't find recipe ") + span_boldwarning(selected_recipe_id) + span_warning("! Contact a coder.")) + return BORGHYPO_STATUS_NORECIPE + for(var/recipe_step in foundRecipe) + var/step_reagent_id = recipe_step["id"] + var/step_dispense_amount = recipe_step["amount"] + var/result = try_add_reagent(target_reagents, user, step_reagent_id, step_dispense_amount) + switch(result) + if(BORGHYPO_STATUS_CONTAINERFULL) + return result + if(BORGHYPO_STATUS_NOCHARGE) + var/datum/reagent/empty_reagent = SSchemistry.chemical_reagents[step_reagent_id] + to_chat(user, span_warning("[src] doesn't have enough ") + span_boldwarning(empty_reagent.name) + span_warning(" to complete this recipe!")) + return result + return BORGHYPO_STATUS_SUCCESS + else + // Just add reagents + return try_add_reagent(target_reagents, user, reagent_ids[mode], amount_per_transfer_from_this) + /obj/item/reagent_containers/borghypo/Initialize(mapload) . = ..() for(var/T in reagent_ids) reagent_volumes[T] = volume - var/datum/reagent/R = SSchemistry.chemical_reagents[T] - reagent_names += R.name + var/datum/reagent/hypo_reagent = SSchemistry.chemical_reagents[T] + reagent_names += hypo_reagent.name START_PROCESSING(SSobj, src) @@ -54,25 +116,19 @@ charge_tick = 0 if(isrobot(loc)) - var/mob/living/silicon/robot/R = loc - if(R && R.cell) + var/mob/living/silicon/robot/robot_user = loc + if(robot_user && robot_user.cell) for(var/T in reagent_ids) if(reagent_volumes[T] < volume) - if(R.cell.charge - charge_cost < 800) //This is so borgs don't kill themselves with it. + if(!robot_user.use_direct_power(charge_cost, 800)) return 0 - else - R.cell.use(charge_cost) - reagent_volumes[T] = min(reagent_volumes[T] + 5, volume) + reagent_volumes[T] = min(reagent_volumes[T] + 5, volume) return 1 /obj/item/reagent_containers/borghypo/attack(var/mob/living/M, var/mob/user) if(!istype(M)) return - if(!reagent_volumes[reagent_ids[mode]]) - balloon_alert(user, "the injector is empty.") - return - var/mob/living/carbon/human/H = M if(istype(H)) var/obj/item/organ/external/affected = H.get_organ(user.zone_sel.selecting) @@ -86,55 +142,210 @@ */ if(M.can_inject(user, 1, ignore_thickness = bypass_protection)) - balloon_alert(user, "you inject [M] with the injector.") - balloon_alert(M, "you feel a tiny prick!") if(M.reagents) - var/t = min(amount_per_transfer_from_this, reagent_volumes[reagent_ids[mode]]) - M.reagents.add_reagent(reagent_ids[mode], t) - reagent_volumes[reagent_ids[mode]] -= t - add_attack_logs(user, M, "Borg injected with [reagent_ids[mode]]") - to_chat(user, span_notice("[t] units injected. [reagent_volumes[reagent_ids[mode]]] units remaining.")) + var/reagent_id = reagent_ids[mode] + var/amount_to_add = min(amount_per_transfer_from_this, reagent_volumes[reagent_id]) + var/result = try_injection(M.reagents, user) + if(is_dispensing_recipe) + // Log every reagent injected in the recipe + var/foundRecipe = saved_recipes[selected_recipe_id] + for(var/recipe_step in foundRecipe) + var/step_reagent_id = recipe_step["id"] + var/step_dispense_amount = recipe_step["amount"] + add_attack_logs(user, M, "Borg injected with [step_dispense_amount] units of '[step_reagent_id]'") + else + add_attack_logs(user, M, "Borg injected with [amount_to_add] units of '[reagent_id]'") + switch(result) + if(BORGHYPO_STATUS_CONTAINERFULL) + balloon_alert(user, "\the [M] has too many reagents in [M.p_their()] system!") + return + if(BORGHYPO_STATUS_NOCHARGE) + if(is_dispensing_recipe) + balloon_alert(user, "not enough reagents to inject full recipe!") + balloon_alert(M, "you feel multiple tiny pricks in quick succession!") + else + var/datum/reagent/empty_reagent = SSchemistry.chemical_reagents[reagent_id] + balloon_alert(user, "\the [src] doesn't have enough [empty_reagent.name]!") + return + if(BORGHYPO_STATUS_NORECIPE) + balloon_alert(user, "recipe '[selected_recipe_id]' not found!") + return + else + if(is_dispensing_recipe) + balloon_alert(user, "recipe '[selected_recipe_id]' injected into \the [M].") + balloon_alert(M, "you feel multiple tiny pricks in quick succession!") + else + balloon_alert(user, "[amount_to_add] units injected into \the [M].") + balloon_alert(M, "you feel a tiny prick!") return /obj/item/reagent_containers/borghypo/attack_self(mob/user as mob) //Change the mode - var/t - for(var/i = 1 to reagent_ids.len) - if(t) - t += ", " - if(mode == i) - t += span_bold("[reagent_names[i]]") - else - t += "[reagent_names[i]]" - t = "Available reagents: [t]." - to_chat(user,span_infoplain(t)) - + tgui_interact(user) return +/* No longer necessary because we use TGUI for this now! /obj/item/reagent_containers/borghypo/Topic(var/href, var/list/href_list) if(href_list["reagent"]) var/t = reagent_ids.Find(href_list["reagent"]) if(t) playsound(src, 'sound/effects/pop.ogg', 50, 0) mode = t - var/datum/reagent/R = SSchemistry.chemical_reagents[reagent_ids[mode]] - balloon_alert(usr, "synthesizer is now producing '[R.name]'") + var/datum/reagent/new_reagent = SSchemistry.chemical_reagents[reagent_ids[mode]] + balloon_alert(usr, "synthesizer is now producing '[new_reagent.name]'") +*/ + +/obj/item/reagent_containers/borghypo/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state) + . = ..() + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + // Assuming the user is opening the UI, empty the chem search preemptively. + ui_chemical_search = null + ui = new(user, src, "BorgHypo", "Integrated [is_dispensing_drinks ? "Drink Dispenser" : "Chemical Hypo"]") + ui.open() + +/obj/item/reagent_containers/borghypo/tgui_static_data(mob/user) + var/list/static_data = list() + static_data["isDispensingDrinks"] = is_dispensing_drinks + static_data["minTransferAmount"] = min_transfer_amount + static_data["maxTransferAmount"] = max_transfer_amount + return static_data + +/obj/item/reagent_containers/borghypo/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/list/data = list() + if(!isrobot(user)) + return data + var/mob/living/silicon/robot/robot_user = user + data["theme"] = robot_user.get_ui_theme() + data["amount"] = amount_per_transfer_from_this + data["transferAmounts"] = transfer_amounts + + var/list/chemicals = list() + for(var/key, value in reagent_volumes) + var/datum/reagent/available_reagent = SSchemistry.chemical_reagents[key] + // If the user is searching for a particular chemical by name, only add this one if its name matches their search! + if((ui_chemical_search && findtext(available_reagent.name, ui_chemical_search)) || !ui_chemical_search) + UNTYPED_LIST_ADD(chemicals, list("name" = available_reagent.name, "id" = key, "volume" = value)) + data["chemicals"] = chemicals + data["uiChemicalSearch"] = ui_chemical_search + data["selectedReagentId"] = reagent_ids[mode] + data["recipes"] = saved_recipes + data["recordingRecipe"] = recording_recipe + data["isDispensingRecipe"] = is_dispensing_recipe + data["selectedRecipeId"] = selected_recipe_id + + return data + +/obj/item/reagent_containers/borghypo/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + if(.) + return + switch(action) + if("select_reagent") + var/new_mode = reagent_ids.Find(params["selectedReagentId"]) + if(new_mode) + var/datum/reagent/selected_reagent = SSchemistry.chemical_reagents[reagent_ids[new_mode]] + playsound(src, 'sound/effects/pop.ogg', 50, 0) + if(recording_recipe) + UNTYPED_LIST_ADD(recording_recipe, list("id" = selected_reagent.id, "amount" = amount_per_transfer_from_this)) + else + mode = new_mode + balloon_alert(ui.user, "synthesizer is now producing '[selected_reagent.name]'") + is_dispensing_recipe = FALSE + . = TRUE + + if("set_amount") + amount_per_transfer_from_this = clamp(round(text2num(params["amount"]), 1), min_transfer_amount, max_transfer_amount) // Round to nearest 1, clamp between min and max transfer amount + . = TRUE + + if("import_config") + var/list/our_data = params["config"] + if(!islist(our_data)) + return FALSE + var/list/new_recipes = list() + for(var/key, value in our_data) + if(istext(key) && islist(value)) + for(var/list/steps in value) + if(istext(steps["id"]) && isnum(steps["amount"])) + new_recipes[key] += list(list("id" = steps["id"], "amount" = steps["amount"])) + if(length(new_recipes)) + saved_recipes = new_recipes + . = TRUE + + if("record_recipe") + recording_recipe = list() + . = TRUE + + if("cancel_recording") + recording_recipe = null + . = TRUE + + if("clear_recipes") + saved_recipes = list() + . = TRUE + + if("save_recording") + var/name = tgui_input_text(ui.user, "What do you want to name this recipe?", "Recipe Name?", "Recipe Name", MAX_NAME_LEN) + if(tgui_status(ui.user, state) != STATUS_INTERACTIVE) + return + if(saved_recipes[name] && tgui_alert(ui.user, "\"[name]\" already exists, do you want to overwrite it?",, list("No", "Yes")) != "Yes") + return + if(name && recording_recipe) + for(var/list/L in recording_recipe) + var/label = L["id"] + // Verify this hypo can dispense every chemical + if(!reagent_ids.Find(label)) + to_chat(ui.user, span_warning("\The [src] cannot find ") + span_boldwarning(label) + span_warning("!")) + return + saved_recipes[name] = recording_recipe + recording_recipe = null + . = TRUE + + if("remove_recipe") + var/recipe_name = params["recipe"] + // If we've selected the recipe we're deleting, un-select it! + if(selected_recipe_id == recipe_name) + selected_recipe_id = null + is_dispensing_recipe = FALSE + saved_recipes -= recipe_name + . = TRUE + + if("select_recipe") + // Make sure we actually have a recipe saved with the given name before setting it! + var/recipe_name = params["recipe"] + var/selectedRecipe = saved_recipes[recipe_name] + if(!selectedRecipe) + to_chat(ui.user, span_warning("\The [src] cannot find the recipe ") + span_boldwarning(recipe_name) + span_warning("!")) + return + playsound(ui.user, 'sound/effects/pop.ogg', 50, 0) + balloon_alert(ui.user, "synthesizer is using macro: '[recipe_name]'") + is_dispensing_recipe = TRUE + selected_recipe_id = recipe_name + . = TRUE + + if("set_chemical_search") + ui_chemical_search = params["uiChemicalSearch"] + . = TRUE + /obj/item/reagent_containers/borghypo/examine(mob/user) . = ..() if(get_dist(user, src) <= 2) - var/datum/reagent/R = SSchemistry.chemical_reagents[reagent_ids[mode]] - . += span_notice("It is currently producing [R.name] and has [reagent_volumes[reagent_ids[mode]]] out of [volume] units left.") + var/datum/reagent/current_reagent = SSchemistry.chemical_reagents[reagent_ids[mode]] + . += span_notice("It is currently producing [current_reagent.name] and has [reagent_volumes[reagent_ids[mode]]] out of [volume] units left.") /obj/item/reagent_containers/borghypo/service - name = "cyborg drink synthesizer" - desc = "A portable drink dispencer." + name = "integrated drink synthesizer" + desc = "An inbuilt synthesizer capable of fabricating a broad variety of drinks." icon = 'icons/obj/drinks.dmi' icon_state = "shaker" charge_cost = 20 recharge_time = 3 volume = 60 max_transfer_amount = 30 + is_dispensing_drinks = TRUE + transfer_amounts = list(5, 10, 20, 30) + hypo_sound = 'sound/machines/reagent_dispense.ogg' reagent_ids = list(REAGENT_ID_ALE, REAGENT_ID_APPLEJUICE, //CHOMPADD it has literally every other type of juice.. REAGENT_ID_BEER, @@ -192,16 +403,24 @@ if(!target.is_open_container() || !target.reagents) return - if(!reagent_volumes[reagent_ids[mode]]) - to_chat(user, span_notice("[src] is out of this reagent, give it some time to refill.")) - return - - if(!target.reagents.get_free_space()) - balloon_alert(user, "[target] is full!") - return - - var/t = min(amount_per_transfer_from_this, reagent_volumes[reagent_ids[mode]]) - target.reagents.add_reagent(reagent_ids[mode], t) - reagent_volumes[reagent_ids[mode]] -= t - balloon_alert(user, "transfered [t] units to [target].") + var/result = try_injection(target.reagents, user) + switch(result) + if(BORGHYPO_STATUS_CONTAINERFULL) + if(is_dispensing_recipe) + balloon_alert(user, "\the [target] is too full to finish the recipe!") + else + balloon_alert(user, "\the [target] is full!") + if(BORGHYPO_STATUS_NOCHARGE) + if(is_dispensing_recipe) + balloon_alert(user, "not enough reagents to finish recipe '[selected_recipe_id]'!") + else + var/datum/reagent/empty_reagent = SSchemistry.chemical_reagents[reagent_ids[mode]] + balloon_alert(user, "not enough of reagent '[empty_reagent.name]'!") + if(BORGHYPO_STATUS_NORECIPE) + balloon_alert(user, "recipe '[selected_recipe_id]' not found!") + else + if(is_dispensing_recipe) + balloon_alert(user, "recipe '[selected_recipe_id]' dispensed to \the [target].") + else + balloon_alert(user, "[amount_per_transfer_from_this] units dispensed to \the [target].") return diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoChemicals.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoChemicals.tsx new file mode 100644 index 0000000000..383742c824 --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoChemicals.tsx @@ -0,0 +1,33 @@ +import { useBackend } from 'tgui/backend'; +import { ChemDispenserChemicals } from '../ChemDispenser/ChemDispenserChemicals'; +import { BorgHypoSearch } from './BorgHypoSearch'; +import type { Data } from './types'; + +export const BorgHypoChemicals = (props) => { + const { act, data } = useBackend(); + const { + chemicals = [], + isDispensingRecipe, + selectedReagentId, + isDispensingDrinks, + recordingRecipe, + } = data; + const recording = !!recordingRecipe; + return ( + { + if (recording || selectedReagentId !== reagentId) { + act('select_reagent', { + selectedReagentId: reagentId, + }); + } + }} + buttons={} + chemicalButtonSelect={(reagentId) => + !recording && selectedReagentId === reagentId && !isDispensingRecipe + } + /> + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipeDisplay.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipeDisplay.tsx new file mode 100644 index 0000000000..7145b6fc2a --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipeDisplay.tsx @@ -0,0 +1,46 @@ +import { useBackend } from 'tgui/backend'; +import { Box, Section, Stack } from 'tgui-core/components'; +import { formatUnits } from '../common/BeakerContents'; +import type { Data } from './types'; + +export const BorgHypoRecipeDisplay = (props) => { + const { act, data } = useBackend(); + const { recordingRecipe } = data; + + const recording = !!recordingRecipe; + + const recordedContents = + recording && + recordingRecipe.map((r) => ({ + id: r.id, + name: r.id.replace(/_/, ' '), + volume: r.amount, + })); + + return ( +
+ + Recording in progress... + + + ) + } + > + {recording && ( + + {recordedContents.map((reagent, i) => ( + + {formatUnits(reagent.volume)} of {reagent.name} + + ))} + + )} +
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipes.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipes.tsx new file mode 100644 index 0000000000..c66d75971f --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecipes.tsx @@ -0,0 +1,34 @@ +import { useBackend } from 'tgui/backend'; +import { Stack } from 'tgui-core/components'; +import { ChemDispenserRecipes } from '../ChemDispenser/ChemDispenserRecipes'; +import { BorgHypoRecipeDisplay } from './BorgHypoRecipeDisplay'; +import type { Data } from './types'; + +export const BorgHypoRecipes = (props) => { + const { act, data } = useBackend(); + const { recipes, isDispensingRecipe, selectedRecipeId, recordingRecipe } = + data; + + return ( + + + act('record_recipe')} + cancelAct={() => act('cancel_recording')} + saveAct={() => act('save_recording')} + clearAct={() => act('clear_recipes')} + dispenseAct={(recipe) => act('select_recipe', { recipe })} + removeAct={(recipe) => act('remove_recipe', { recipe })} + getDispenseButtonSelected={(recipe) => { + return isDispensingRecipe && selectedRecipeId === recipe; + }} + /> + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecordingBlinker.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecordingBlinker.tsx new file mode 100644 index 0000000000..343bd4a76c --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoRecordingBlinker.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Box, Icon, Stack } from 'tgui-core/components'; +import type { Data } from './types'; + +export const BorgHypoRecordingBlinker = (props) => { + const { data } = useBackend(); + const isRecording = !!data.recordingRecipe; + + const [blink, setBlink] = useState(false); + + useEffect(() => { + if (isRecording) { + const intervalId = setInterval(() => { + setBlink((v) => !v); + }, 1000); + return () => clearInterval(intervalId); + } + }, [isRecording]); + + if (!isRecording) { + return null; + } + + return ( + <> + + + + + + REC + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSearch.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSearch.tsx new file mode 100644 index 0000000000..7cb6269643 --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSearch.tsx @@ -0,0 +1,26 @@ +import { useBackend } from 'tgui/backend'; +import { Input, Stack } from 'tgui-core/components'; +import { BorgHypoRecordingBlinker } from './BorgHypoRecordingBlinker'; +import type { Data } from './types'; + +export const BorgHypoSearch = (props) => { + const { act, data } = useBackend(); + const { isDispensingDrinks } = data; + const uiChemicalsName = isDispensingDrinks ? 'drinks' : 'chemicals'; + return ( + + + + + act('set_chemical_search', { + uiChemicalSearch: input, + }) + } + /> + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSettings.tsx b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSettings.tsx new file mode 100644 index 0000000000..16fbdcc68a --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/BorgHypoSettings.tsx @@ -0,0 +1,18 @@ +import { useBackend } from 'tgui/backend'; +import { ChemDispenserSettings } from '../ChemDispenser/ChemDispenserSettings'; +import type { Data } from './types'; + +export const BorgHypoSettings = (props) => { + const { act, data } = useBackend(); + const { amount, minTransferAmount, maxTransferAmount, transferAmounts } = + data; + return ( + act('set_amount', { amount: amt })} + /> + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/index.tsx b/tgui/packages/tgui/interfaces/BorgHypo/index.tsx new file mode 100644 index 0000000000..120f5024ad --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/index.tsx @@ -0,0 +1,38 @@ +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { Stack } from 'tgui-core/components'; + +import { BorgHypoChemicals } from './BorgHypoChemicals'; +import { BorgHypoRecipes } from './BorgHypoRecipes'; +import { BorgHypoSettings } from './BorgHypoSettings'; +import type { Data } from './types'; + +export const BorgHypo = (props) => { + const { data } = useBackend(); + const { isDispensingDrinks, theme } = data; + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/BorgHypo/types.ts b/tgui/packages/tgui/interfaces/BorgHypo/types.ts new file mode 100644 index 0000000000..a5a011586a --- /dev/null +++ b/tgui/packages/tgui/interfaces/BorgHypo/types.ts @@ -0,0 +1,18 @@ +import type { BooleanLike } from 'tgui-core/react'; +import type { Reagent, Recipe } from '../ChemDispenser/types'; + +export type Data = { + amount: number; + transferAmounts: number[]; + minTransferAmount: number; + maxTransferAmount: number; + chemicals: Reagent[]; + selectedReagentId: string; + recipes: Record; + recordingRecipe: Recipe[]; + isDispensingRecipe: BooleanLike; + selectedRecipeId: string; + uiChemicalSearch: string; + isDispensingDrinks: BooleanLike; + theme: string | null; +}; diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserChemicals.tsx b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserChemicals.tsx index 2e822529e4..1fe2af38b3 100644 --- a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserChemicals.tsx +++ b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserChemicals.tsx @@ -1,50 +1,61 @@ -import { useEffect, useState } from 'react'; +import { type ReactNode, useEffect, useState } from 'react'; import { useBackend } from 'tgui/backend'; import { Button, Icon, Section, Stack, Tooltip } from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import type { Data, Reagent } from './types'; -import type { Data } from './types'; - -export const ChemDispenserChemicals = (props) => { - const { act, data } = useBackend(); - const { chemicals = [] } = data; +export const ChemDispenserChemicals = (props: { + sectionTitle: string; + chemicals: Reagent[]; + /** Called when the user clicks on a reagent dispense button. Arg is the ID of the button's reagent. */ + dispenseAct: (reagentId: string) => void; + /** Optional callback that returns whether or not a reagent dispense button will appear "activated". Arg is the ID of the button's reagent. */ + chemicalButtonSelect?: (reagentId: string) => BooleanLike; + /** Extra UI elements that will appear within the header of the chemical UI. */ + buttons: ReactNode; +}) => { + const { + chemicals, + sectionTitle, + dispenseAct, + chemicalButtonSelect, + buttons, + } = props; const flexFillers: boolean[] = []; - for (let i = 0; i < (chemicals.length + 1) % 3; i++) { - flexFillers.push(true); - } + const sortedChemicals: Reagent[] = chemicals; + sortedChemicals.sort((a, b) => a.name.localeCompare(b.name)); return ( -
} - > +
- {chemicals.map((c, i) => ( - + {sortedChemicals.map((c, i) => ( + ))} - {flexFillers.map((_, i) => ( - - ))}
); }; -const RecordingBlinker = (props) => { +export const RecordingBlinker = (props) => { const { data } = useBackend(); const recording = !!data.recordingRecipe; diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserRecipes.tsx b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserRecipes.tsx index 44c63dd119..6c970b3615 100644 --- a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserRecipes.tsx +++ b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserRecipes.tsx @@ -1,13 +1,42 @@ -import { useBackend } from 'tgui/backend'; import { Box, Button, Section, Stack } from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; +import { handleImportData } from '../PlushieEditor/function'; +import { exportRecipes } from './functions'; +import type { Recipe } from './types'; -import type { Data } from './types'; +export const ChemDispenserRecipes = (props: { + /** Associated list of saved recipe macros. */ + recipes: Record; + /** The current recipe macro that's being recorded, if any. We assume we aren't recording a recipe if this is undefined! */ + recordingRecipe: Recipe[]; + /** Called when the user attempts to start a recipe recording. */ + recordAct: () => void; + /** Called when the user attempts to cancel a recipe recording. */ + cancelAct: () => void; + /** Called when the user attempts to save a recipe recording. */ + saveAct: () => void; + /** Called when the user attempts to clear all recipe recordings. */ + clearAct: () => void; + /** Called when the user attempts to use a recipe macro. */ + dispenseAct: (recipe: string) => void; + /** Called when a recipe dispense button is checking whether or not it will appear "selected". Arg is the ID of the button's reagent. Defaults to false if undefined. */ + getDispenseButtonSelected?: (recipe: string) => BooleanLike; + /** Called when the user attempts to remove a recipe macro. */ + removeAct: (recipe: string) => void; +}) => { + const { + recipes, + recordingRecipe, + recordAct, + cancelAct, + saveAct, + clearAct, + dispenseAct, + getDispenseButtonSelected, + removeAct, + } = props; -export const ChemDispenserRecipes = (props) => { - const { act, data } = useBackend(); - const { recipes, recordingRecipe } = data; - - const recording: boolean = !!recordingRecipe; + const isRecording: boolean = !!recordingRecipe; const recipeData = Object.keys(recipes).sort(); return ( @@ -17,51 +46,61 @@ export const ChemDispenserRecipes = (props) => { scrollable buttons={ - {!recording && ( + {!isRecording && ( - )} - {recording && ( + {isRecording && ( - )} - {recording && ( + {isRecording && ( - )} - {!recording && ( - - act('clear_recipes')} - > - Clear All - - + {!isRecording && ( + <> + + handleImportData(files)} + /> + + + @@ -102,7 +146,7 @@ export const ChemDispenserRecipes = (props) => { confirmIcon="triangle-exclamation" confirmContent={''} color="bad" - onClick={() => act('remove_recipe', { recipe })} + onClick={() => removeAct(recipe)} /> diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserSettings.tsx b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserSettings.tsx index bdf025a5d9..d2fcabdb8f 100644 --- a/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserSettings.tsx +++ b/tgui/packages/tgui/interfaces/ChemDispenser/ChemDispenserSettings.tsx @@ -1,4 +1,3 @@ -import { useBackend } from 'tgui/backend'; import { Button, LabeledList, @@ -7,28 +6,35 @@ import { Stack, } from 'tgui-core/components'; -import { dispenseAmounts } from './constants'; -import type { Data } from './types'; - -export const ChemDispenserSettings = (props) => { - const { act, data } = useBackend(); - const { amount } = data; +export const ChemDispenserSettings = (props: { + /** The dispense amount the user has currently selected. */ + selectedAmount: number; + /** Available amounts for this dispenser to use. */ + availableAmounts: number[]; + /** The minimum allowed selectable amount. Used for the slider UI element. */ + minAmount: number; + /** The maximum allowed selectable amount. Used for the slider UI element. */ + maxAmount: number; + /** Called when the user tries to change the dispensed amount. Arg is the amount the user is trying to set it to. */ + amountAct: (amount: number) => void; +}) => { + const { selectedAmount, availableAmounts, minAmount, maxAmount, amountAct } = + props; return ( -
+
- {dispenseAmounts.map((a, i) => ( + {availableAmounts.map((a, i) => ( @@ -40,14 +46,10 @@ export const ChemDispenserSettings = (props) => { - act('amount', { - amount: value, - }) - } + value={selectedAmount} + minValue={minAmount} + maxValue={maxAmount} + onChange={(e, value) => amountAct(value)} /> diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/functions.ts b/tgui/packages/tgui/interfaces/ChemDispenser/functions.ts new file mode 100644 index 0000000000..ff34bbede3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ChemDispenser/functions.ts @@ -0,0 +1,12 @@ +import { getCurrentTimestamp } from '../VorePanelExport/VorePanelExportTimestamp'; +import type { Recipe } from './types'; + +export function exportRecipes(recipes: Record) { + const blob = new Blob([JSON.stringify(recipes)], { + type: 'application/json', + }); + + const datesegment = getCurrentTimestamp(); + const filename = `ChemRecipes${datesegment}.json`; + Byond.saveBlob(blob, filename, '.json'); +} diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/index.tsx b/tgui/packages/tgui/interfaces/ChemDispenser/index.tsx index 036f06fa4b..7b9bbd5aa3 100644 --- a/tgui/packages/tgui/interfaces/ChemDispenser/index.tsx +++ b/tgui/packages/tgui/interfaces/ChemDispenser/index.tsx @@ -3,13 +3,18 @@ import { Window } from 'tgui/layouts'; import { Stack } from 'tgui-core/components'; import { ChemDispenserBeaker } from './ChemDispenserBeaker'; -import { ChemDispenserChemicals } from './ChemDispenserChemicals'; +import { + ChemDispenserChemicals, + RecordingBlinker, +} from './ChemDispenserChemicals'; import { ChemDispenserRecipes } from './ChemDispenserRecipes'; import { ChemDispenserSettings } from './ChemDispenserSettings'; +import { dispenseAmounts } from './constants'; import type { Data } from './types'; export const ChemDispenser = (props) => { - const { data } = useBackend(); + const { data, act } = useBackend(); + const { recipes, recordingRecipe, glass, chemicals, amount } = data; return ( @@ -20,15 +25,45 @@ export const ChemDispenser = (props) => { - + + act('amount', { + amount: amt, + }) + } + /> - + act('record_recipe')} + cancelAct={() => act('cancel_recording')} + saveAct={() => act('save_recording')} + clearAct={() => act('clear_recipes')} + dispenseAct={(recipe) => + act('dispense_recipe', { recipe }) + } + removeAct={(recipe) => act('remove_recipe', { recipe })} + /> - + + act('dispense', { reagent: reagentId }) + } + buttons={} + /> diff --git a/tgui/packages/tgui/interfaces/ChemDispenser/types.ts b/tgui/packages/tgui/interfaces/ChemDispenser/types.ts index 09eb0c2daa..b88fcf0322 100644 --- a/tgui/packages/tgui/interfaces/ChemDispenser/types.ts +++ b/tgui/packages/tgui/interfaces/ChemDispenser/types.ts @@ -9,12 +9,12 @@ export type Data = { amount: number; isBeakerLoaded: BooleanLike; glass: BooleanLike; - beakerContents: reagent[]; + beakerContents: Reagent[]; beakerCurrentVolume: number | null; beakerMaxVolume: number | null; - chemicals: reagent[]; + chemicals: Reagent[]; recipes: Record; recordingRecipe: Recipe[]; }; -type reagent = { name: string; id: string; volume: number }; +export type Reagent = { name: string; id: string; volume: number }; diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/types.ts b/tgui/packages/tgui/interfaces/ModifyRobot/types.ts index 7008dd7817..65fc3af5ef 100644 --- a/tgui/packages/tgui/interfaces/ModifyRobot/types.ts +++ b/tgui/packages/tgui/interfaces/ModifyRobot/types.ts @@ -38,7 +38,7 @@ export type Data = { law_sets: law_pack[]; active_ais: DropdownEntry[]; selected_ai: string | null; - theme?: string; + theme: string | null; }; export type DropdownEntry = { diff --git a/tgui/packages/tgui/interfaces/RobotChoose/types.ts b/tgui/packages/tgui/interfaces/RobotChoose/types.ts index 6df4494734..4655e977fe 100644 --- a/tgui/packages/tgui/interfaces/RobotChoose/types.ts +++ b/tgui/packages/tgui/interfaces/RobotChoose/types.ts @@ -4,7 +4,7 @@ export type Data = { possible_sprites?: spriteOption[]; currentName: string; isDefaultName: boolean; - theme?: string; + theme: string | null; selected_module?: string; sprite_datum?: string | null; sprite_datum_class?: string | null; diff --git a/tgui/packages/tgui/interfaces/RobotDecals.tsx b/tgui/packages/tgui/interfaces/RobotDecals.tsx index 3202d34b20..8cddea6498 100644 --- a/tgui/packages/tgui/interfaces/RobotDecals.tsx +++ b/tgui/packages/tgui/interfaces/RobotDecals.tsx @@ -5,7 +5,7 @@ import { Box, Button, Input, Section, Stack } from 'tgui-core/components'; import { createSearch } from 'tgui-core/string'; type Data = { - theme?: string; + theme: string | null; all_decals?: string[] | null; all_animations?: string[] | null; active_decals: string[]; diff --git a/tgui/packages/tgui/interfaces/RobotSleeper/types.ts b/tgui/packages/tgui/interfaces/RobotSleeper/types.ts index 91b206c097..001c674978 100644 --- a/tgui/packages/tgui/interfaces/RobotSleeper/types.ts +++ b/tgui/packages/tgui/interfaces/RobotSleeper/types.ts @@ -2,7 +2,7 @@ import type { BooleanLike } from 'tgui-core/react'; export type Data = { name?: string; - theme?: string; + theme: string | null; chems?: RobotChem[]; our_patient: Patient | null; diff --git a/tgui/packages/tgui/interfaces/Robotact/types.ts b/tgui/packages/tgui/interfaces/Robotact/types.ts index 9e5a43f536..57ff18c583 100644 --- a/tgui/packages/tgui/interfaces/Robotact/types.ts +++ b/tgui/packages/tgui/interfaces/Robotact/types.ts @@ -31,7 +31,7 @@ export type Data = { max_health: number; light_color: string; - theme?: string; + theme: string | null; // Modules modules_static: Module[]; diff --git a/tgui/packages/tgui/interfaces/common/BeakerContents.tsx b/tgui/packages/tgui/interfaces/common/BeakerContents.tsx index 4095da20c6..943920f8fb 100644 --- a/tgui/packages/tgui/interfaces/common/BeakerContents.tsx +++ b/tgui/packages/tgui/interfaces/common/BeakerContents.tsx @@ -1,6 +1,6 @@ import { Box, Stack } from 'tgui-core/components'; -const formatUnits = (a) => `${a} unit${a === 1 ? '' : 's'}`; +export const formatUnits = (a) => `${a} unit${a === 1 ? '' : 's'}`; /** * Displays a beaker's contents