diff --git a/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm b/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm index ff8836a4906..38a22222a39 100644 --- a/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm +++ b/_maps/map_files/MetaStation/MetaStation.v41A.II.dmm @@ -66172,7 +66172,6 @@ "cjr" = ( /obj/machinery/mecha_part_fabricator{ dir = 1; - id = "0"; name = "counterfeit fabricator"; req_access = "0" }, diff --git a/code/game/mecha/mech_fabricator.dm b/code/game/mecha/mech_fabricator.dm index 50fdfef3cb3..2985e4ef103 100644 --- a/code/game/mecha/mech_fabricator.dm +++ b/code/game/mecha/mech_fabricator.dm @@ -1,46 +1,59 @@ +#define EXOFAB_BASE_CAPACITY 200000 +#define EXOFAB_CAPACITY_PER_RATING 50000 +#define EXOFAB_EFFICIENCY_PER_RATING 0.15 +#define EXOFAB_SPEED_UPGRADE_MULTIPLIER 0.2 + +/** + * # Exosuit Fabricator + * + * A machine that allows for the production of exosuits and robotic parts. + */ /obj/machinery/mecha_part_fabricator - icon = 'icons/obj/robotics.dmi' - icon_state = "fab-idle" name = "exosuit fabricator" desc = "Nothing is being built." + icon = 'icons/obj/robotics.dmi' + icon_state = "fab-idle" density = TRUE anchored = TRUE use_power = IDLE_POWER_USE idle_power_usage = 20 active_power_usage = 5000 + // Settings + /// Bitflags of design types that can be produced. + var/allowed_design_types = MECHFAB + /// List of categories to display in the UI. Designs intended for each respective category need to have the name in [/datum/design/category]. Defined in [Initialize()][/atom/proc/Initialize]. + var/list/categories = null + /// Unused. Ensures backwards compatibility with some maps. + var/id = null + // Variables + /// Production time multiplier. A lower value means faster production. Updated by [CheckParts()][/atom/proc/CheckParts]. var/time_coeff = 1 + /// Resource efficiency multiplier. A lower value means less resources consumed. Updated by [CheckParts()][/atom/proc/CheckParts]. var/component_coeff = 1 - var/datum/research/files - var/fabricator_type = MECHFAB - var/id - var/sync = 0 - var/part_set - var/datum/design/being_built - var/list/queue = list() - var/processing_queue = 0 - var/screen = "main" - var/temp - var/list/part_sets = list( - "Cyborg", - "Cyborg Repair", - "Ripley", - "Firefighter", - "Odysseus", - "Gygax", - "Durand", - "H.O.N.K", - "Reticence", - "Phazon", - "Exosuit Equipment", - "Cyborg Upgrade Modules", - "Medical", - "Misc" - ) + /// Holds the locally known R&D designs. + var/datum/research/local_designs = null + /// Whether a R&D sync is currently in progress. + var/syncing = FALSE + /// The currently selected category. + var/selected_category = null + /// The design that is being currently being built. + var/datum/design/being_built = null + /// The world.time at which the current design build started. + var/build_start = 0 + /// The world.time at which the current design build will end. + var/build_end = 0 + /// The build queue. Lazy list. + var/list/datum/design/build_queue = null + /// Whether the queue is currently being processed. + var/processing_queue = FALSE /obj/machinery/mecha_part_fabricator/New() - var/datum/component/material_container/materials = AddComponent(/datum/component/material_container, list(MAT_METAL, MAT_GLASS, MAT_SILVER, MAT_GOLD, MAT_DIAMOND, MAT_PLASMA, MAT_URANIUM, MAT_BANANIUM, MAT_TRANQUILLITE, MAT_TITANIUM, MAT_BLUESPACE), 0, FALSE, /obj/item/stack, CALLBACK(src, .proc/is_insertion_ready), CALLBACK(src, .proc/AfterMaterialInsert)) + // Set up some datums + var/datum/component/material_container/materials = AddComponent(/datum/component/material_container, list(MAT_METAL, MAT_GLASS, MAT_SILVER, MAT_GOLD, MAT_DIAMOND, MAT_PLASMA, MAT_URANIUM, MAT_BANANIUM, MAT_TRANQUILLITE, MAT_TITANIUM, MAT_BLUESPACE), 0, FALSE, /obj/item/stack, CALLBACK(src, .proc/can_insert_materials), CALLBACK(src, .proc/on_material_insert)) materials.precise_insertion = TRUE + local_designs = new /datum/research(src) ..() + // Components component_parts = list() component_parts += new /obj/item/circuitboard/mechfab(null) component_parts += new /obj/item/stock_parts/matter_bin(null) @@ -49,10 +62,395 @@ component_parts += new /obj/item/stock_parts/micro_laser(null) component_parts += new /obj/item/stack/sheet/glass(null) RefreshParts() - files = new /datum/research(src) //Setup the research data holder. +/obj/machinery/mecha_part_fabricator/Initialize(mapload) + . = ..() + categories = list( + "Cyborg", + "Cyborg Repair", + "Ripley", + "Firefighter", + "Odysseus", + "Gygax", + "Durand", + "H.O.N.K", + "Reticence", + "Phazon", + "Exosuit Equipment", + "Cyborg Upgrade Modules", + "Medical", + "Misc" + ) + +/obj/machinery/mecha_part_fabricator/Destroy() + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + materials.retrieve_all() + QDEL_NULL(local_designs) + return ..() + +/obj/machinery/mecha_part_fabricator/RefreshParts() + var/coef_mats = 0 + var/coef_resources = 1 + EXOFAB_EFFICIENCY_PER_RATING + var/coef_speed = -1 + for(var/sp in component_parts) + var/obj/item/stock_parts/SP = sp + if(istype(SP, /obj/item/stock_parts/matter_bin)) + coef_mats += SP.rating + else if(istype(SP, /obj/item/stock_parts/micro_laser)) + coef_resources -= SP.rating * EXOFAB_EFFICIENCY_PER_RATING + else if(istype(SP, /obj/item/stock_parts/manipulator)) + coef_speed += SP.rating + + // Update our efficiencies + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + materials.max_amount = (EXOFAB_BASE_CAPACITY + (coef_mats * EXOFAB_CAPACITY_PER_RATING)) + component_coeff = coef_resources + time_coeff = round(initial(time_coeff) - (initial(time_coeff) * coef_speed) * EXOFAB_SPEED_UPGRADE_MULTIPLIER, 0.01) + +/** + * Calculates the total resource cost of a design, applying [/obj/machinery/mecha_part_fabricator/var/component_coeff]. + * + * Arguments: + * * D - The design whose cost to calculate. + */ +/obj/machinery/mecha_part_fabricator/proc/get_design_cost(datum/design/D, roundto = 1) + var/list/resources = list() + for(var/R in D.materials) + var/cost = round(D.materials[R] * component_coeff, roundto) + if(!cost) + continue + resources[R] = cost + return resources + +/** + * Calculates the total build time of a design, applying [/obj/machinery/mecha_part_fabricator/var/time_coeff]. + * + * Arguments: + * * D - The design whose build time to calculate. + */ +/obj/machinery/mecha_part_fabricator/proc/get_design_build_time(datum/design/D, roundto = 1) + return round(initial(D.construction_time) * time_coeff, roundto) + +/** + * Returns whether the machine contains enough resources to build the given design. + * + * Arguments: + * * D - The design to check. + */ +/obj/machinery/mecha_part_fabricator/proc/can_afford_design(datum/design/D) + if(length(D.reagents_list)) // No reagents storage - no reagent designs. + return FALSE + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + return materials.has_materials(get_design_cost(D)) + +/** + * Attempts to build the first item in the queue. + */ +/obj/machinery/mecha_part_fabricator/proc/process_queue() + if(!processing_queue || being_built || !length(build_queue)) + return + var/datum/design/D = build_queue[1] + if(build_design(D)) + build_queue.Cut(1, 2) + +/** + * Given a design, attempts to build it. + * + * Arguments: + * * D - The design to build. + */ +/obj/machinery/mecha_part_fabricator/proc/build_design(datum/design/D) + . = FALSE + if(!local_designs.known_designs[D.id] || !(D.build_type & allowed_design_types)) + return + if(being_built) + atom_say("Error: Something is already being built!") + return + if(!can_afford_design(D)) + atom_say("Error: Insufficient materials to build [D.name]!") + return + + // Subtract the materials from the holder + var/list/final_cost = get_design_cost(D) + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + materials.use_amount(final_cost) + + // Start building the design + var/build_time = get_design_build_time(D) + being_built = D + build_start = world.time + build_end = build_start + build_time + desc = "It's building \a [initial(D.name)]." + use_power = ACTIVE_POWER_USE + add_overlay("fab-active") + addtimer(CALLBACK(src, .proc/build_design_timer_finish, D, final_cost), build_time) + + return TRUE + +/** + * Called when the timer for building a design finishes. + * + * Arguments: + * * D - The design being built. + * * final_cost - The materials consumed during the build. + */ +/obj/machinery/mecha_part_fabricator/proc/build_design_timer_finish(datum/design/D, list/final_cost) + // Spawn the item (in a lockbox if restricted) + var/obj/item/I = new D.build_path(loc) + if(D.locked) + var/obj/item/storage/lockbox/research/large/L = new(get_step(src, SOUTH)) + I.forceMove(L) + L.name += " ([I.name])" + L.origin_tech = I.origin_tech + else + I.forceMove(get_step(src, SOUTH)) + I.materials = final_cost + + // Clean up + being_built = null + build_start = 0 + build_end = 0 + desc = initial(desc) + use_power = IDLE_POWER_USE + cut_overlays() + atom_say("[I] is complete.") + + // Keep the queue processing going if it's on + process_queue() + + SStgui.update_uis(src) + +/** + * Syncs the R&D designs from the first [/obj/machinery/computer/rdconsole] in the area. + */ +/obj/machinery/mecha_part_fabricator/proc/sync() + addtimer(CALLBACK(src, .proc/sync_timer_finish), 3 SECONDS) + syncing = TRUE + +/** + * Called when the timer for syncing finishes. + */ +/obj/machinery/mecha_part_fabricator/proc/sync_timer_finish() + syncing = FALSE + var/area/A = get_area(src) + for(var/obj/machinery/computer/rdconsole/RDC in A) // These computers should have their own global.. + if(!RDC.sync) + continue + RDC.files.push_data(local_designs) + atom_say("Successfully synchronized with R&D servers.") + break + SStgui.update_uis(src) + +/** + * Called by [/datum/component/material_container] when material sheets are inserted in the machine. + * + * Arguments: + * * type_inserted - The material type. + * * id_inserted - The material ID. + * * amount_inserted - The amount of sheets inserted. + */ +/obj/machinery/mecha_part_fabricator/proc/on_material_insert(type_inserted, id_inserted, amount_inserted) + var/stack_name = copytext(id_inserted, 2) + add_overlay("fab-load-[stack_name]") + addtimer(CALLBACK(src, .proc/on_material_insert_timer_finish), 1 SECONDS) + process_queue() + SStgui.update_uis(src) + +/** + * Called when the timer after inserting material sheets finishes. + */ +/obj/machinery/mecha_part_fabricator/proc/on_material_insert_timer_finish() + cut_overlays() + +/** + * Returns whether the machine can accept new materials. + */ +/obj/machinery/mecha_part_fabricator/proc/can_insert_materials(mob/user) + if(panel_open) + to_chat(user, "[src] cannot be loaded with new materials while opened!") + return FALSE + if(being_built) + to_chat(user, "[src] is currently building a part! Please wait until completion.") + return FALSE + return TRUE + +// Interaction code +/obj/machinery/mecha_part_fabricator/attackby(obj/item/W, mob/user, params) + if(default_deconstruction_screwdriver(user, "fab-o", "fab-idle", W)) + return + if(exchange_parts(user, W)) + return + if(default_deconstruction_crowbar(user, W)) + return TRUE + return ..() + +/obj/machinery/mecha_part_fabricator/attack_ghost(mob/user) + ui_interact(user) + +/obj/machinery/mecha_part_fabricator/attack_hand(mob/user) + if(..()) + return + if(!allowed(user) && !isobserver(user)) + to_chat(user, "Access denied.") + return + ui_interact(user) + +/obj/machinery/mecha_part_fabricator/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) + if(!selected_category) + selected_category = categories[1] + + var/datum/asset/materials_assets = get_asset_datum(/datum/asset/simple/materials) + materials_assets.send(user) + + ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) + if(!ui) + ui = new(user, src, ui_key, "ExosuitFabricator", name, 800, 600) + ui.open() + ui.set_autoupdate(FALSE) + +/obj/machinery/mecha_part_fabricator/ui_data(mob/user) + var/list/data = list() + data["syncing"] = syncing + data["processingQueue"] = processing_queue + data["categories"] = categories + data["curCategory"] = selected_category + + // Materials + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + var/list/mats = list() + for(var/MAT in materials.materials) + var/datum/material/M = materials.materials[MAT] + if(!M.amount) + continue + mats[MAT] = M.amount + data["materials"] = mats + data["capacity"] = materials.max_amount + + // Queue and deficit + var/list/queue = list() + var/list/queue_deficit = mats.Copy() // What (and how much) materials are we missing to fully process the queue? Any negative values after the loop mean a deficit for a particular material. + for(var/d in build_queue) + var/datum/design/D = d + var/list/out = list("name" = D.name, "time" = get_design_build_time(D)) + // Add to deficit + var/list/actual_cost = get_design_cost(D) + for(var/cost_id in actual_cost) + queue_deficit[cost_id] -= actual_cost[cost_id] + if(queue_deficit[cost_id] < 0) // This design costs more mats than we have, mark it as such. + out["notEnough"] = TRUE + queue += list(out) + data["queue"] = queue + data["queueDeficit"] = queue_deficit + + // Current category + if(selected_category) + var/list/category_designs = list() + for(var/v in local_designs.known_designs) + var/datum/design/D = local_designs.known_designs[v] + if(!(D.build_type & allowed_design_types) || !(selected_category in D.category) || length(D.reagents_list)) + continue + var/list/design_out = list("id" = D.id, "name" = D.name, "cost" = get_design_cost(D), "time" = get_design_build_time(D)) + if(!can_afford_design(D)) + design_out["notEnough"] = TRUE + category_designs += list(design_out) + data["designs"] = category_designs + + // Current build + if(being_built) + data["building"] = being_built.name + data["buildStart"] = build_start + data["buildEnd"] = build_end + data["worldTime"] = world.time + else // Redundant data to ensure the clientside state is refreshed. + data["building"] = null + data["buildStart"] = null + data["buildEnd"] = null + + return data + +/obj/machinery/mecha_part_fabricator/ui_act(action, params) + if(..()) + return + + . = TRUE + switch(action) + if("category") + var/new_cat = params["cat"] + if(!(new_cat in categories)) + return + selected_category = new_cat + if("build") + var/id = params["id"] + var/datum/design/D = local_designs.known_designs[id] + if(!D) + return + build_design(D) + if("queue") + var/id = params["id"] + if(!(id in local_designs.known_designs)) + return + var/datum/design/D = local_designs.known_designs[id] + if(!(D.build_type & allowed_design_types) || length(D.reagents_list)) + return + LAZYADD(build_queue, D) + process_queue() + if("queueall") + LAZYINITLIST(build_queue) + for(var/v in local_designs.known_designs) + var/datum/design/D = local_designs.known_designs[v] + if(!(D.build_type & allowed_design_types) || !(selected_category in D.category) || length(D.reagents_list)) + continue + build_queue += D + process_queue() + if("unqueue") + if(!build_queue) + return + var/index = text2num(params["index"]) + if(!ISINDEXSAFE(build_queue, index)) + return + build_queue.Cut(index, index + 1) + if("unqueueall") + if(!build_queue) + return + build_queue = list() + if("queueswap") + if(!build_queue) + return + var/index_from = text2num(params["from"]) + var/index_to = text2num(params["to"]) + if(!ISINDEXSAFE(build_queue, index_from) || !ISINDEXSAFE(build_queue, index_to)) + return + build_queue.Swap(index_from, index_to) + if("process") + processing_queue = !processing_queue + process_queue() + if("sync") + if(syncing) + return + sync() + if("withdraw") + var/id = params["id"] + var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) + var/datum/material/M = materials.materials[id] + if(!M || !M.amount) + return + var/num_sheets = input(usr, "How many sheets do you want to withdraw?", "Withdrawing [M.name]") as num|null + if(isnull(num_sheets) || num_sheets <= 0) + return + materials.retrieve_sheets(num_sheets, id) + else + return FALSE + add_fingerprint(usr) + +/** + * # Exosuit Fabricator (upgraded) + * + * Upgraded variant of [/obj/machinery/mecha_part_fabricator]. + */ /obj/machinery/mecha_part_fabricator/upgraded/New() ..() + // Upgraded components + QDEL_LIST(component_parts) component_parts = list() component_parts += new /obj/item/circuitboard/mechfab(null) component_parts += new /obj/item/stock_parts/matter_bin/super(null) @@ -62,404 +460,14 @@ component_parts += new /obj/item/stack/sheet/glass(null) RefreshParts() -/obj/machinery/mecha_part_fabricator/Destroy() - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - materials.retrieve_all() - return ..() - -/obj/machinery/mecha_part_fabricator/RefreshParts() - var/T = 0 - - //maximum stocking amount (default 300000, 600000 at T4) - for(var/obj/item/stock_parts/matter_bin/M in component_parts) - T += M.rating - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - materials.max_amount = (200000 + (T*50000)) - - //resources adjustment coefficient (1 -> 0.85 -> 0.7 -> 0.55) - T = 1.15 - for(var/obj/item/stock_parts/micro_laser/Ma in component_parts) - T -= Ma.rating*0.15 - component_coeff = T - - //building time adjustment coefficient (1 -> 0.8 -> 0.6) - T = -1 - for(var/obj/item/stock_parts/manipulator/Ml in component_parts) - T += Ml.rating - time_coeff = round(initial(time_coeff) - (initial(time_coeff)*(T))/5,0.01) - - -/obj/machinery/mecha_part_fabricator/proc/output_parts_list(set_name) - var/output = "" - for(var/v in files.known_designs) - var/datum/design/D = files.known_designs[v] - if(D.build_type & fabricator_type) - if(!(set_name in D.category)) - continue - output += "
[output_part_info(D)]
\[" - if(check_resources(D)) - output += "Build | " - output += "Add to queue\]\[?\]
" - return output - -/obj/machinery/mecha_part_fabricator/proc/output_part_info(datum/design/D) - var/output = "[initial(D.name)] (Cost: [output_part_cost(D)]) [get_construction_time_w_coeff(D)/10] seconds" - return output - -/obj/machinery/mecha_part_fabricator/proc/output_part_cost(datum/design/D) - var/i = 0 - var/output - for(var/c in D.materials) - output += "[i?" | ":null][get_resource_cost_w_coeff(D, c)] [material2name(c)]" - i++ - return output - -/obj/machinery/mecha_part_fabricator/proc/output_available_resources() - var/output - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - for(var/mat_id in materials.materials) - var/datum/material/M = materials.materials[mat_id] - output += "[M.name]: [M.amount] cm³" - if(M.amount >= MINERAL_MATERIAL_AMOUNT) - output += "- Remove \[1\]" - if(M.amount >= (MINERAL_MATERIAL_AMOUNT * 10)) - output += " | \[10\]" - output += " | \[All\]" - output += "
" - return output - -/obj/machinery/mecha_part_fabricator/proc/get_resources_w_coeff(datum/design/D) - var/list/resources = list() - for(var/R in D.materials) - resources[R] = get_resource_cost_w_coeff(D, R) - return resources - -/obj/machinery/mecha_part_fabricator/proc/check_resources(datum/design/D) - if(D.reagents_list.len) // No reagents storage - no reagent designs. - return FALSE - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - if(materials.has_materials(get_resources_w_coeff(D))) - return TRUE - return FALSE - -/obj/machinery/mecha_part_fabricator/proc/build_part(datum/design/D) - being_built = D - desc = "It's building \a [initial(D.name)]." - var/list/res_coef = get_resources_w_coeff(D) - - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - materials.use_amount(res_coef) - overlays += "fab-active" - use_power = ACTIVE_POWER_USE - updateUsrDialog() - sleep(get_construction_time_w_coeff(D)) - use_power = IDLE_POWER_USE - overlays -= "fab-active" - desc = initial(desc) - - var/obj/item/I = new D.build_path(loc) - if(D.locked) - var/obj/item/storage/lockbox/research/large/L = new /obj/item/storage/lockbox/research/large(get_step(src, SOUTH)) - I.forceMove(L) - L.name += " ([I.name])" - L.origin_tech = I.origin_tech - else - I.forceMove(get_step(src, SOUTH)) - if(istype(I)) - I.materials = res_coef - atom_say("[I] is complete.") - being_built = null - - updateUsrDialog() - return TRUE - -/obj/machinery/mecha_part_fabricator/proc/update_queue_on_page() - send_byjax(usr,"mecha_fabricator.browser","queue",list_queue()) - return - -/obj/machinery/mecha_part_fabricator/proc/add_part_set_to_queue(set_name) - if(set_name in part_sets) - for(var/v in files.known_designs) - var/datum/design/D = files.known_designs[v] - if(D.build_type & fabricator_type) - if(set_name in D.category) - add_to_queue(D) - -/obj/machinery/mecha_part_fabricator/proc/add_to_queue(D) - if(!istype(queue)) - queue = list() - if(D) - queue[++queue.len] = D - return queue.len - -/obj/machinery/mecha_part_fabricator/proc/remove_from_queue(index) - if(!isnum(index) || !istype(queue) || (index<1 || index>queue.len)) - return FALSE - queue.Cut(index,++index) - return TRUE - -/obj/machinery/mecha_part_fabricator/proc/process_queue() - var/datum/design/D = queue[1] - if(!D) - remove_from_queue(1) - if(queue.len) - return process_queue() - else - return - temp = null - while(D) - if(stat&(NOPOWER|BROKEN)) - return FALSE - if(!check_resources(D)) - atom_say("Not enough resources. Queue processing stopped.") - temp = {"Not enough resources to build next part.
- Try again | Return"} - return FALSE - remove_from_queue(1) - build_part(D) - D = listgetindex(queue, 1) - atom_say("Queue processing finished successfully.") - -/obj/machinery/mecha_part_fabricator/proc/list_queue() - var/output = "Queue contains:" - if(!istype(queue) || !queue.len) - output += "
Nothing" - else - output += "
    " - var/i = 0 - for(var/datum/design/D in queue) - i++ - var/obj/part = D.build_path - output += "" - output += initial(part.name) + " - " - output += "[i>1?"":null] " - output += "[i↓":null] " - output += "Remove" - - output += "
" - output += "\[Process queue | Clear queue\]" - return output - -/obj/machinery/mecha_part_fabricator/proc/sync() - temp = "Updating local R&D database..." - updateUsrDialog() - sleep(30) //only sleep if called by user - var/area/localarea = get_area(src) - - for(var/obj/machinery/computer/rdconsole/RDC in localarea.contents) - if(!RDC.sync) - continue - RDC.files.push_data(files) - temp = "Processed equipment designs.
" - //check if the tech coefficients have changed - temp += "Return" - - updateUsrDialog() - atom_say("Successfully synchronized with R&D server.") - return - - temp = "Unable to connect to local R&D Database.
Please check your connections and try again.
Return" - updateUsrDialog() - return - -/obj/machinery/mecha_part_fabricator/proc/get_resource_cost_w_coeff(datum/design/D, resource, roundto = 1) - return round(D.materials[resource]*component_coeff, roundto) - -/obj/machinery/mecha_part_fabricator/proc/get_construction_time_w_coeff(datum/design/D, roundto = 1) //aran - return round(initial(D.construction_time)*time_coeff, roundto) - -/obj/machinery/mecha_part_fabricator/attack_ghost(mob/user) - interact(user) - -/obj/machinery/mecha_part_fabricator/attack_hand(mob/user) - if(..()) - return 1 - if(!allowed(user) && !isobserver(user)) - to_chat(user, "Access denied.") - return 1 - return interact(user) - -/obj/machinery/mecha_part_fabricator/interact(mob/user) - var/dat, left_part - if(..()) - return - user.set_machine(src) - var/turf/exit = get_step(src,(dir)) - if(exit.density) - atom_say("Error! Part outlet is obstructed.") - return - if(temp) - left_part = temp - else if(being_built) - var/obj/I = being_built.build_path - left_part = {"Building [initial(I.name)].
- Please wait until completion...
"} - else - switch(screen) - if("main") - left_part = output_available_resources()+"
" - left_part += "Sync with R&D servers
" - for(var/part_set in part_sets) - left_part += "[part_set] - \[Add all parts to queue\]
" - if("parts") - left_part += output_parts_list(part_set) - left_part += "
Return" - dat = {" - - [name] - - - - - - - - -
- [left_part] - - [list_queue()] -
"} - var/datum/browser/popup = new(user, "mecha_fabricator", name, 1000, 600) - popup.set_content(dat) - popup.open(0) - onclose(user, "mecha_fabricator") - return - -/obj/machinery/mecha_part_fabricator/Topic(href, href_list) - if(..()) - return 1 - var/datum/topic_input/afilter = new /datum/topic_input(href,href_list) - if(href_list["part_set"]) - var/tpart_set = afilter.getStr("part_set") - if(tpart_set) - if(tpart_set=="clear") - part_set = null - else - part_set = tpart_set - screen = "parts" - if(href_list["part"]) - var/T = afilter.getStr("part") - for(var/v in files.known_designs) - var/datum/design/D = files.known_designs[v] - if(D.build_type & fabricator_type) - if(D.id == T) - if(!processing_queue) - build_part(D) - else - add_to_queue(D) - break - if(href_list["add_to_queue"]) - var/T = afilter.getStr("add_to_queue") - for(var/v in files.known_designs) - var/datum/design/D = files.known_designs[v] - if(D.build_type & fabricator_type) - if(D.id == T) - add_to_queue(D) - break - return update_queue_on_page() - if(href_list["remove_from_queue"]) - remove_from_queue(afilter.getNum("remove_from_queue")) - return update_queue_on_page() - if(href_list["partset_to_queue"]) - add_part_set_to_queue(afilter.get("partset_to_queue")) - return update_queue_on_page() - if(href_list["process_queue"]) - spawn(0) - if(processing_queue || being_built) - return FALSE - processing_queue = 1 - process_queue() - processing_queue = 0 - if(href_list["clear_temp"]) - temp = null - if(href_list["screen"]) - screen = href_list["screen"] - if(href_list["queue_move"] && href_list["index"]) - var/index = afilter.getNum("index") - var/new_index = index + afilter.getNum("queue_move") - if(isnum(index) && isnum(new_index)) - if(ISINRANGE(new_index,1,queue.len)) - queue.Swap(index,new_index) - return update_queue_on_page() - if(href_list["clear_queue"]) - queue = list() - return update_queue_on_page() - if(href_list["sync"]) - sync() - if(href_list["part_desc"]) - var/T = afilter.getStr("part_desc") - for(var/v in files.known_designs) - var/datum/design/D = files.known_designs[v] - if(D.build_type & fabricator_type) - if(D.id == T) - var/obj/part = D.build_path - temp = {"

[initial(part.name)] description:

- [initial(part.desc)]
- Return - "} - break - - if(href_list["remove_mat"] && href_list["material"]) - var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - materials.retrieve_sheets(text2num(href_list["remove_mat"]), href_list["material"]) - - updateUsrDialog() - return - -/obj/machinery/mecha_part_fabricator/proc/AfterMaterialInsert(type_inserted, id_inserted, amount_inserted) - var/stack_name = material2name(id_inserted) - overlays += "fab-load-[stack_name]" - sleep(10) - overlays -= "fab-load-[stack_name]" - updateUsrDialog() - -/obj/machinery/mecha_part_fabricator/attackby(obj/item/W, mob/user, params) - if(default_deconstruction_screwdriver(user, "fab-o", "fab-idle", W)) - return - - if(exchange_parts(user, W)) - return - - if(default_deconstruction_crowbar(user, W)) - return TRUE - - else - return ..() - -/obj/machinery/mecha_part_fabricator/proc/material2name(ID) - return copytext(ID,2) - -/obj/machinery/mecha_part_fabricator/proc/is_insertion_ready(mob/user) - if(panel_open) - to_chat(user, "You can't load [src] while it's opened!") - return FALSE - if(being_built) - to_chat(user, "\The [src] is currently processing! Please wait until completion.") - return FALSE - - return TRUE - +/** + * # Spacepod Fabricator + * + * Spacepod variant of [/obj/machinery/mecha_part_fabricator]. + */ /obj/machinery/mecha_part_fabricator/spacepod name = "spacepod fabricator" - fabricator_type = PODFAB - part_sets = list( "Pod_Weaponry", - "Pod_Armor", - "Pod_Cargo", - "Pod_Parts", - "Pod_Frame", - "Misc") + allowed_design_types = PODFAB req_access = list(ACCESS_MECHANIC) /obj/machinery/mecha_part_fabricator/spacepod/New() @@ -474,6 +482,27 @@ component_parts += new /obj/item/stack/sheet/glass(null) RefreshParts() +/obj/machinery/mecha_part_fabricator/spacepod/Initialize(mapload) + . = ..() + categories = list( + "Pod_Weaponry", + "Pod_Armor", + "Pod_Cargo", + "Pod_Parts", + "Pod_Frame", + "Misc" + ) + +/** + * # Robotic Fabricator + * + * Cyborgs-only variant of [/obj/machinery/mecha_part_fabricator]. + */ /obj/machinery/mecha_part_fabricator/robot - name = "Robotic Fabricator" - part_sets = list("Cyborg") + name = "robotic fabricator" + categories = list("Cyborg") + +#undef EXOFAB_BASE_CAPACITY +#undef EXOFAB_CAPACITY_PER_RATING +#undef EXOFAB_EFFICIENCY_PER_RATING +#undef EXOFAB_SPEED_UPGRADE_MULTIPLIER diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm index f1dcb1cc0a8..36d51843b8c 100644 --- a/code/modules/client/asset_cache.dm +++ b/code/modules/client/asset_cache.dm @@ -325,3 +325,13 @@ GLOBAL_LIST_EMPTY(asset_datums) assets = list( "safe_dial.png" = 'icons/safe_dial.png' ) + +// Materials (metal, glass...) +/datum/asset/simple/materials + verify = FALSE + +/datum/asset/simple/materials/register() + for(var/n in list("metal", "glass", "silver", "gold", "diamond", "uranium", "plasma", "clown", "mime", "titanium", "plastic")) + assets["sheet-[n].png"] = icon('icons/obj/items.dmi', "sheet-[n]") + assets["sheet-bluespace.png"] = icon('icons/obj/telescience.dmi', "polycrystal") + ..() diff --git a/tgui/packages/tgui/components/Countdown.js b/tgui/packages/tgui/components/Countdown.js new file mode 100644 index 00000000000..50d61f99be8 --- /dev/null +++ b/tgui/packages/tgui/components/Countdown.js @@ -0,0 +1,64 @@ +import { Component } from 'inferno'; +import { Box } from './Box'; + +export class Countdown extends Component { + constructor(props) { + super(props); + this.timer = null; + this.state = { + value: Math.max(props.timeLeft * 100, 0), // ds -> ms + }; + } + + tick() { + const newValue = Math.max(this.state.value - this.props.rate, 0); + if (newValue <= 0) { + clearInterval(this.timer); + } + this.setState(prevState => { + return { + value: newValue, + }; + }); + } + + componentDidMount() { + this.timer = setInterval(() => this.tick(), this.props.rate); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + componentDidUpdate(prevProps) { + if (this.props.current !== prevProps.current) { + // https://github.com/yannickcr/eslint-plugin-react/issues/1707 + // eslint-disable-next-line react/no-did-update-set-state + this.setState(prevState => { + return { + value: Math.max(this.props.timeLeft * 100, 0), + }; + }); + } + if (!this.timer) { + this.componentDidMount(); + } + } + + render() { + const { + format, + ...rest + } = this.props; + const formatted = new Date(this.state.value).toISOString().slice(11, 19); + return ( + + {format ? format(this.state.value, formatted) : formatted} + + ); + } +} + +Countdown.defaultProps = { + rate: 1000, +}; diff --git a/tgui/packages/tgui/components/ProgressBar.js b/tgui/packages/tgui/components/ProgressBar.js index 5cc8a88bf29..35d80fffff0 100644 --- a/tgui/packages/tgui/components/ProgressBar.js +++ b/tgui/packages/tgui/components/ProgressBar.js @@ -1,5 +1,6 @@ -import { clamp01, scale, keyOfMatchingRange, toFixed } from 'common/math'; +import { clamp01, keyOfMatchingRange, scale, toFixed } from 'common/math'; import { classes, pureComponentHooks } from 'common/react'; +import { Component } from 'inferno'; import { computeBoxClassName, computeBoxProps } from './Box'; export const ProgressBar = props => { @@ -42,3 +43,55 @@ export const ProgressBar = props => { }; ProgressBar.defaultHooks = pureComponentHooks; + +export class ProgressBarCountdown extends Component { + constructor(props) { + super(props); + this.timer = null; + this.state = { + value: Math.max(props.current * 100, 0), // ds -> ms + }; + } + + tick() { + const newValue = Math.max(this.state.value + this.props.rate, 0); + if (newValue <= 0) { + clearInterval(this.timer); + } + this.setState(prevState => { + return { + value: newValue, + }; + }); + } + + componentDidMount() { + this.timer = setInterval(() => this.tick(), this.props.rate); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + render() { + const { + start, + current, + end, + ...rest + } = this.props; + const frac = (this.state.value / 100 - start) / (end - start); + return ( + + ); + } +} + +ProgressBarCountdown.defaultProps = { + rate: 1000, +}; + +ProgressBar.Countdown = ProgressBarCountdown; diff --git a/tgui/packages/tgui/interfaces/ExosuitFabricator.js b/tgui/packages/tgui/interfaces/ExosuitFabricator.js new file mode 100644 index 00000000000..ee689a17cb0 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ExosuitFabricator.js @@ -0,0 +1,369 @@ +import { Fragment } from 'inferno'; +import { classes } from '../../common/react'; +import { createSearch } from '../../common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Divider, Dropdown, Flex, Icon, Input, ProgressBar, Section } from '../components'; +import { Countdown } from '../components/Countdown'; +import { Window } from '../layouts'; + +// __DEFINES/construction.dm, L73 +const MINERAL_MATERIAL_AMOUNT = 2000; + +const iconNameOverrides = { + bananium: "clown", + tranquillite: "mime", +}; + +export const ExosuitFabricator = (properties, context) => { + const { act, data } = useBackend(context); + const { + building, + } = data; + return ( + + + + + + + + + {building && ( + + + + )} + + + + + + + + + + + + + + + + ); +}; + +const Materials = (properties, context) => { + const { act, data } = useBackend(context); + const { + materials, + capacity, + } = data; + const totalMats = Object.values(materials).reduce((a, b) => a + b, 0); + return ( +
+ {(totalMats / capacity * 100).toPrecision(3)}% full + + }> + {["$metal", "$glass", "$silver", "$gold", "$uranium", "$titanium", "$plasma", "$diamond", "$bluespace", "$bananium", "$tranquillite", "$plastic"].map(name => ( + act("withdraw", { + id: name, + })} + /> + ))} +
+ ); +}; + +const Designs = (properties, context) => { + const { act, data } = useBackend(context); + const { + curCategory, + categories, + designs, + syncing, + } = data; + const [searchText, setSearchText] = useLocalState(context, "searchText", ""); + const searcher = createSearch(searchText, design => { + return design.name; + }); + const filteredDesigns = designs.filter(searcher); + return ( +
act("category", { + cat: cat, + })} + width="150px" + /> + } + height="100%" + buttons={ + +
+ ); +}; + +const Building = (properties, context) => { + const { act, data } = useBackend(context); + const { + building, + buildStart, + buildEnd, + worldTime, + } = data; + return ( +
+ + + + + Building {building} +  ( f.substr(3)} + />) + +
+ ); +}; + +const Queue = (properties, context) => { + const { act, data } = useBackend(context); + const { + queue, + processingQueue, + } = data; + const queueDeficit = Object.entries(data.queueDeficit).filter(a => a[1] < 0); + const queueTime = queue.reduce((a, b) => a + b.time, 0); + return ( +
+
+ ); +}; + +const MaterialCount = (properties, context) => { + const { act, data } = useBackend(context); + const { + id, + amount, + lineDisplay, + onClick, + ...rest + } = properties; + const cleanId = id.replace("$", ""); + const storedAmount = data.materials[id] || 0; + const curAmount = amount || storedAmount; + if (curAmount <= 0 && !(cleanId === "metal" || cleanId === "glass")) { + return; + } + const insufficient = amount && (amount > storedAmount); + return ( + + + + + + {!lineDisplay ? ( + + + {cleanId} + + + {curAmount.toLocaleString("en-US")} cm³ ({Math.round(curAmount / MINERAL_MATERIAL_AMOUNT * 10) / 10} sheets) + + + ) : ( + + {curAmount.toLocaleString("en-US")} + + )} + + + ); +}; + +const Design = (properties, context) => { + const { act, data } = useBackend(context); + const design = properties.design; + return ( + +