diff --git a/code/__DEFINES/atom_hud.dm b/code/__DEFINES/atom_hud.dm index adb70a44240..7b65c539cd4 100644 --- a/code/__DEFINES/atom_hud.dm +++ b/code/__DEFINES/atom_hud.dm @@ -46,6 +46,8 @@ #define DIAG_CAMERA_HUD "21" /// Steady Hacked APC effect, visible only to Malf AIs #define MALF_APC_HUD "22" +/// Big Manipulator interaction point HUDs +#define BIG_MANIP_HUD "23" //by default everything in the hud_list of an atom is an image //a value in hud_list with one of these will change that behavior diff --git a/code/datums/wires/big_manipulator.dm b/code/datums/wires/big_manipulator.dm index f66786d06d0..4d56f5470e1 100644 --- a/code/datums/wires/big_manipulator.dm +++ b/code/datums/wires/big_manipulator.dm @@ -14,41 +14,31 @@ return ..() /datum/wires/big_manipulator/interactable(mob/user) - var/obj/machinery/big_manipulator/manipulator_big = holder + var/obj/machinery/big_manipulator/holder_manipulator = holder - return manipulator_big.panel_open ? ..() : FALSE + return holder_manipulator.panel_open ? ..() : FALSE /datum/wires/big_manipulator/get_status() - var/obj/machinery/big_manipulator/manipulator_big = holder + var/obj/machinery/big_manipulator/holder_manipulator = holder var/list/status = list() - status += "The big light bulb [manipulator_big.power_access_wire_cut ? "has went out" : "is glowing [manipulator_big.on ? "green" : "red"]"]." - status += "The small red light shows: [isnull(manipulator_big.containment_obj) ? "empty" : "full"]." - status += "Text on the yellow board shows: [manipulator_big.selected_type.name]." - status += "There are [manipulator_big.interaction_mode] text on the small blue panel." - status += "The purple light is [manipulator_big.override_priority ? "on" : "off"]." - status += "The number on small white panel shows [manipulator_big.manipulator_throw_range]." + status += "The big light bulb [holder_manipulator.power_access_wire_cut ? "is off" : "is glowing [holder_manipulator.on ? "green" : "red"]"]." + status += "The small light bulb [holder_manipulator.held_object ? "is glowing bright green" : "is off"]." + status += "The green number on the display shows [length(holder_manipulator.pickup_points)]." + status += "The red number on the display shows [length(holder_manipulator.dropoff_points)]." return status /datum/wires/big_manipulator/on_pulse(wire) - var/obj/machinery/big_manipulator/manipulator_big = holder + var/obj/machinery/big_manipulator/holder_manipulator = holder switch(wire) if(WIRE_ON) - manipulator_big.try_press_on(usr) + holder_manipulator.try_press_on(usr) if(WIRE_DROP) - manipulator_big.drop_held_object() - if(WIRE_ITEM_TYPE) - manipulator_big.cycle_pickup_type() - if(WIRE_CHANGE_MODE) - manipulator_big.change_mode() - if(WIRE_ONE_PRIORITY_BUTTON) - manipulator_big.override_priority = !manipulator_big.override_priority - if(WIRE_THROW_RANGE) - manipulator_big.cycle_throw_range() + holder_manipulator.drop_held_atom() /datum/wires/big_manipulator/on_cut(wire, mend, source) - var/obj/machinery/big_manipulator/manipulator_big = holder + var/obj/machinery/big_manipulator/holder_manipulator = holder if(wire == WIRE_ON) if(mend) - manipulator_big.power_access_wire_cut = FALSE + holder_manipulator.power_access_wire_cut = FALSE return - manipulator_big.power_access_wire_cut = TRUE + holder_manipulator.power_access_wire_cut = TRUE diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index 190e7f66f50..8611d41305c 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -44,7 +44,7 @@ hud_icons = list(FAN_HUD) /datum/atom_hud/data/diagnostic - hud_icons = list(DIAG_HUD, DIAG_STAT_HUD, DIAG_BATT_HUD, DIAG_MECH_HUD, DIAG_BOT_HUD, DIAG_TRACK_HUD, DIAG_CAMERA_HUD, DIAG_AIRLOCK_HUD, DIAG_LAUNCHPAD_HUD) + hud_icons = list(DIAG_HUD, DIAG_STAT_HUD, DIAG_BATT_HUD, DIAG_MECH_HUD, DIAG_BOT_HUD, DIAG_TRACK_HUD, DIAG_CAMERA_HUD, DIAG_AIRLOCK_HUD, DIAG_LAUNCHPAD_HUD, BIG_MANIP_HUD) /datum/atom_hud/data/bot_path hud_icons = list(DIAG_PATH_HUD) diff --git a/code/game/machinery/big_manipulator.dm b/code/game/machinery/big_manipulator.dm deleted file mode 100644 index 632d8378c99..00000000000 --- a/code/game/machinery/big_manipulator.dm +++ /dev/null @@ -1,968 +0,0 @@ -#define INTERACT_DROP "drop" -#define INTERACT_USE "use" -#define INTERACT_THROW "throw" - -#define TAKE_ITEMS 1 -#define TAKE_CLOSETS 2 -#define TAKE_HUMANS 3 - -#define DELAY_STEP 0.1 -#define MAX_DELAY 30 - -#define MIN_DELAY_TIER_1 2 -#define MIN_DELAY_TIER_2 1.4 -#define MIN_DELAY_TIER_3 0.8 -#define MIN_DELAY_TIER_4 0.2 - -#define STATUS_BUSY "busy" -#define STATUS_IDLE "idle" -#define STATUS_INSTALLING_WORKER "installing_worker" -#define STATUS_UNINSTALLING_WORKER "uninstalling_worker" - -#define WORKER_SINGLE_USE "single" -#define WORKER_EMPTY_USE "empty" -#define WORKER_NORMAL_USE "normal" - -/// The Big Manipulator's core. Main part of the mechanism that carries out the entire process. -/obj/machinery/big_manipulator - name = "big manipulator" - desc = "Operates different objects. Truly, a groundbreaking innovation..." - icon = 'icons/map_icons/objects.dmi' - icon_state = "/obj/machinery/big_manipulator" - post_init_icon_state = "core" - density = TRUE - circuit = /obj/item/circuitboard/machine/big_manipulator - greyscale_colors = "#d8ce13" - greyscale_config = /datum/greyscale_config/big_manipulator - /// Min time manipulator can have in delay. Changing on upgrade. - var/minimal_delay = MIN_DELAY_TIER_1 - /// The time it takes for the manipulator to complete the action cycle. - var/interaction_delay = MIN_DELAY_TIER_1 - - /// Using high tier manipulators speeds up big manipulator and requires more energy. - var/power_use_lvl = 0.2 - /// The status of the manipulator - `IDLE` or `BUSY`. - var/status = STATUS_IDLE - /// Is the manipulator turned on? - var/on = FALSE - /// The direction the manipulator arm will take items from. - var/take_here = NORTH - /// The direction the manipulator arm will drop items to. - var/drop_here = SOUTH - /// The turf where the manipulator arm will take items from. - var/turf/take_turf - /// The turf where the manipulator arm will drop items to. - var/turf/drop_turf - /// Priority settings depending on the manipulator drop mode that are available to this manipulator. Filled during Initialize. - var/list/priority_settings_for_drop = list() - /// Priority settings depending on the manipulator use mode that are available to this manipulator. Filled during Initialize. - var/list/priority_settings_for_use = list() - /// What priority settings are available to use at the moment. - /// We also use this list to sort priorities from ascending to descending. - var/list/allowed_priority_settings = list() - /// The object inside the manipulator. - var/datum/weakref/containment_obj - /// The object used as a filter. - var/datum/weakref/filter_obj - /// The poor monkey that needs to use mode works. - var/datum/weakref/monkey_worker - /// Whether the monkey is set to use combat mode for interaction - var/worker_combat_mode = FALSE - /// Whether the worker will use right click instead of left click interaction - var/worker_alt_mode = FALSE - /// A list to denote the above alt mode, to avoid making a new list on every single interaction - var/list/resolved_modifiers = list("button" = "left") - /// weakref to id that locked this manipualtor. - var/datum/weakref/locked_by_this_id - /// Is manipulator locked by identity id. - var/id_locked = FALSE - /// The manipulator's arm. - var/obj/effect/big_manipulator_arm/manipulator_arm - - /// How should the manipulator interact with the object? - var/interaction_mode = INTERACT_DROP - /// How should the worker interact with the object? - var/worker_interaction = WORKER_NORMAL_USE - /// The distance the thrown object should travel when thrown. - var/manipulator_throw_range = 1 - /// Overrides the priority selection, only accessing the top priority list element. - var/override_priority = FALSE - /// The `type` the manipulator will interact with only. - var/atom/selected_type - /// Is the power access wire cut? Disables the power button if `TRUE`. - var/power_access_wire_cut = FALSE - /// List where we can set selected type. Taking items by Initialize. - var/list/allowed_types_to_pick_up = list( - /obj/item, - /obj/structure/closet, - ) - -/obj/machinery/big_manipulator/Initialize(mapload) - . = ..() - take_and_drop_turfs_check() - create_manipulator_arm() - RegisterSignal(manipulator_arm, COMSIG_QDELETING, PROC_REF(on_hand_qdel)) - manipulator_lvl() - set_up_priority_settings() - selected_type = allowed_types_to_pick_up[1] - if(on) - press_on(pressed_by = null) - set_wires(new /datum/wires/big_manipulator(src)) - - register_context() - -/// Init priority settings list for all modes. -/obj/machinery/big_manipulator/proc/set_up_priority_settings() - for(var/datum/manipulator_priority/priority_for_drop as anything in subtypesof(/datum/manipulator_priority/for_drop)) - priority_settings_for_drop += new priority_for_drop - for(var/datum/manipulator_priority/priority_for_use as anything in subtypesof(/datum/manipulator_priority/for_use)) - priority_settings_for_use += new priority_for_use - update_priority_list() - -/obj/machinery/big_manipulator/examine(mob/user) - . = ..() - . += "You can change direction with alternative wrench usage." - var/mob/monkey_resolve = monkey_worker?.resolve() - if(!isnull(monkey_resolve)) - . += "You can see [monkey_resolve]: [src] manager." - -/obj/machinery/big_manipulator/add_context(atom/source, list/context, obj/item/held_item, mob/user) - . = ..() - - if(isnull(held_item)) - context[SCREENTIP_CONTEXT_LMB] = panel_open ? "Interact with wires" : "Open UI" - return CONTEXTUAL_SCREENTIP_SET - - if(held_item.tool_behaviour == TOOL_WRENCH) - context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Una" : "A"]nchor" - context[SCREENTIP_CONTEXT_RMB] = "Rotate clockwise" - return CONTEXTUAL_SCREENTIP_SET - if(held_item.tool_behaviour == TOOL_SCREWDRIVER) - context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel" - return CONTEXTUAL_SCREENTIP_SET - if(held_item.tool_behaviour == TOOL_CROWBAR && panel_open) - context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" - return CONTEXTUAL_SCREENTIP_SET - if(is_wire_tool(held_item) && panel_open) - context[SCREENTIP_CONTEXT_LMB] = "Interact with wires" - return CONTEXTUAL_SCREENTIP_SET - -/obj/machinery/big_manipulator/Destroy(force) - . = ..() - manipulator_arm.vis_contents.Cut() - qdel(manipulator_arm) - if(!isnull(containment_obj)) - var/obj/containment_resolve = containment_obj?.resolve() - containment_resolve?.forceMove(get_turf(containment_resolve)) - containment_obj = null - if(!isnull(filter_obj)) - var/obj/filter_resolve = filter_obj?.resolve() - filter_resolve?.forceMove(get_turf(filter_resolve)) - filter_obj = null - var/mob/living/carbon/human/monkey_resolve = monkey_worker?.resolve() - if(!isnull(monkey_resolve)) - monkey_resolve.forceMove(get_turf(monkey_resolve)) - monkey_resolve.remove_offsets("[src]") - monkey_resolve = null - locked_by_this_id = null - -/obj/machinery/big_manipulator/Exited(atom/movable/gone, direction) - if(gone == monkey_worker?.resolve()) - var/mob/living/carbon/human/poor_monkey = monkey_worker.resolve() - REMOVE_TRAIT(poor_monkey, TRAIT_AI_PAUSED, "[src]") - monkey_worker = null - poor_monkey.remove_offsets("[src]") - if(interaction_mode == INTERACT_USE) - change_mode() - if(manipulator_arm.vis_contents.Find(gone)) - manipulator_arm.vis_contents.Cut(gone) - ..() - -/obj/machinery/big_manipulator/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) - . = ..() - take_and_drop_turfs_check() - if(isnull(get_turf(src))) - qdel(manipulator_arm) - return - if(!manipulator_arm) - create_manipulator_arm() - -/obj/machinery/big_manipulator/emag_act(mob/user, obj/item/card/emag/emag_card) - . = ..() - if(obj_flags & EMAGGED) - return FALSE - balloon_alert(user, "overloaded functions installed") - obj_flags |= EMAGGED - allowed_types_to_pick_up += /mob/living - return TRUE - -/obj/machinery/big_manipulator/wrench_act(mob/living/user, obj/item/tool) - . = ..() - default_unfasten_wrench(user, tool, time = 1 SECONDS) - return ITEM_INTERACT_SUCCESS - -/obj/machinery/big_manipulator/wrench_act_secondary(mob/living/user, obj/item/tool) - . = ..() - if(status == STATUS_BUSY || on) - to_chat(user, span_warning("[src] is activated!")) - return ITEM_INTERACT_BLOCKING - rotate_big_hand() - playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE) - return ITEM_INTERACT_SUCCESS - -/obj/machinery/big_manipulator/can_be_unfasten_wrench(mob/user, silent) - if(status == STATUS_BUSY || on) - to_chat(user, span_warning("[src] is activated!")) - return FAILED_UNFASTEN - return ..() - -/obj/machinery/big_manipulator/default_unfasten_wrench(mob/user, obj/item/wrench, time) - . = ..() - if(. == SUCCESSFUL_UNFASTEN) - take_and_drop_turfs_check() - -/obj/machinery/big_manipulator/screwdriver_act(mob/living/user, obj/item/tool) - if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool)) - return ITEM_INTERACT_SUCCESS - return ITEM_INTERACT_BLOCKING - -/obj/machinery/big_manipulator/crowbar_act(mob/living/user, obj/item/tool) - . = ..() - if(default_deconstruction_crowbar(tool)) - return ITEM_INTERACT_SUCCESS - return ITEM_INTERACT_BLOCKING - -/obj/machinery/big_manipulator/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if(user.combat_mode) - return NONE - if(!panel_open) - return NONE - - if(isidcard(tool)) - var/obj/item/card/id/clicked_by_this_id = tool - if(!isnull(locked_by_this_id)) - var/obj/item/card/id/resolve_id = locked_by_this_id.resolve() - if(clicked_by_this_id != resolve_id) - balloon_alert(user, "locked by another id!") - return ITEM_INTERACT_BLOCKING - locked_by_this_id = null - change_id_locked_status(user) - return ITEM_INTERACT_SUCCESS - locked_by_this_id = WEAKREF(clicked_by_this_id) - change_id_locked_status(user) - return ITEM_INTERACT_SUCCESS - - if(is_wire_tool(tool)) - wires.interact(user) - return ITEM_INTERACT_SUCCESS - - return NONE - -/obj/machinery/big_manipulator/RefreshParts() - . = ..() - - manipulator_lvl() - -/obj/machinery/big_manipulator/proc/eject_worker(mob/user) - if(isnull(monkey_worker)) - return - if(status == STATUS_BUSY) - balloon_alert(user, "turn it off first!") - return - if(status == STATUS_UNINSTALLING_WORKER) - balloon_alert(user, "already uninstalling worker, please wait") - return - - var/mob/living/carbon/human/poor_monkey = monkey_worker.resolve() - if(!ismonkey(poor_monkey)) - if(!QDELETED(poor_monkey)) - poor_monkey.drop_all_held_items() - poor_monkey.set_lying_angle(0) - poor_monkey.forceMove(get_turf(src)) - monkey_worker = null - return - - status = STATUS_UNINSTALLING_WORKER - balloon_alert(user, "uninstalling monkey worker...") - if(!do_after(user, 3 SECONDS, src)) - balloon_alert(user, "interrupted") - return - balloon_alert(user, "monkey worker uninstalled") - poor_monkey.drop_all_held_items() - poor_monkey.set_lying_angle(0) - poor_monkey.forceMove(get_turf(src)) - status = STATUS_IDLE - -/obj/machinery/big_manipulator/mouse_drop_receive(atom/monkey, mob/user, params) - if(!ismonkey(monkey) || !isnull(monkey_worker)) - return - if(status == STATUS_BUSY) - balloon_alert(user, "turn it off first!") - return - if(status == STATUS_INSTALLING_WORKER) - balloon_alert(user, "already installing worker, please wait") - return - var/mob/living/carbon/human/poor_monkey = monkey - if(poor_monkey.mind) - balloon_alert(user, "too smart! might demand pay!") - return - poor_monkey.balloon_alert(user, "installing monkey worker...") - status = STATUS_INSTALLING_WORKER - if(!do_after(user, 3 SECONDS, poor_monkey)) - poor_monkey.balloon_alert(user, "interrupted") - status = STATUS_IDLE - return - balloon_alert(user, "monkey worker installed") - monkey_worker = WEAKREF(poor_monkey) - ADD_TRAIT(poor_monkey, TRAIT_AI_PAUSED, "[src]") - poor_monkey.drop_all_held_items() - poor_monkey.forceMove(src) - poor_monkey.set_combat_mode(worker_combat_mode) - status = STATUS_IDLE - manipulator_arm.vis_contents += poor_monkey - poor_monkey.dir = manipulator_arm.dir - poor_monkey.set_lying_angle(dir2angle(manipulator_arm.dir)) - poor_monkey.add_offsets( - "[src]", - x_add = 32 + manipulator_arm.calculate_item_offset(TRUE, pixels_to_offset = 16), - y_add = 32 + manipulator_arm.calculate_item_offset(FALSE, pixels_to_offset = 16) - ) - -/obj/machinery/big_manipulator/proc/change_id_locked_status(mob/user) - id_locked = !id_locked - balloon_alert(user, "successfully [!id_locked ? "un" : ""]locked") - -/// Creat manipulator hand effect on manipulator core. -/obj/machinery/big_manipulator/proc/create_manipulator_arm() - manipulator_arm = new/obj/effect/big_manipulator_arm(src) - manipulator_arm.dir = take_here - vis_contents += manipulator_arm - -/// Check servo tier and change manipulator speed, power_use and colour. -/obj/machinery/big_manipulator/proc/manipulator_lvl() - var/datum/stock_part/servo/locate_servo = locate() in component_parts - if(!locate_servo) - return - switch(locate_servo.tier) - if(-INFINITY to 1) - minimal_delay = interaction_delay = MIN_DELAY_TIER_1 - power_use_lvl = 0.2 - set_greyscale(COLOR_YELLOW) - manipulator_arm?.set_greyscale(COLOR_YELLOW) - if(2) - minimal_delay = interaction_delay = MIN_DELAY_TIER_2 - power_use_lvl = 0.4 - set_greyscale(COLOR_ORANGE) - manipulator_arm?.set_greyscale(COLOR_ORANGE) - if(3) - minimal_delay = interaction_delay = MIN_DELAY_TIER_3 - power_use_lvl = 0.6 - set_greyscale(COLOR_RED) - manipulator_arm?.set_greyscale(COLOR_RED) - if(4 to INFINITY) - minimal_delay = interaction_delay = MIN_DELAY_TIER_4 - power_use_lvl = 0.8 - set_greyscale(COLOR_PURPLE) - manipulator_arm?.set_greyscale(COLOR_PURPLE) - - active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * power_use_lvl - -/// Changing take and drop turf tiles when we anchore manipulator or if manipulator not in turf. -/obj/machinery/big_manipulator/proc/take_and_drop_turfs_check() - if(anchored && isturf(src.loc)) - take_turf = get_step(src, take_here) - drop_turf = get_step(src, drop_here) - else - take_turf = null - drop_turf = null - -/// Changing take and drop turf dirs and also changing manipulator hand sprite dir. -/obj/machinery/big_manipulator/proc/rotate_big_hand() - switch(take_here) - if(NORTH) - take_here = EAST - drop_here = WEST - if(EAST) - take_here = SOUTH - drop_here = NORTH - if(SOUTH) - take_here = WEST - drop_here = EAST - if(WEST) - take_here = NORTH - drop_here = SOUTH - manipulator_arm.dir = take_here - var/mob/monkey = monkey_worker?.resolve() - if(!isnull(monkey)) - monkey.dir = manipulator_arm.dir - take_and_drop_turfs_check() - -/// Deliting hand will destroy our manipulator core. -/obj/machinery/big_manipulator/proc/on_hand_qdel() - SIGNAL_HANDLER - - deconstruct(TRUE) - -/// Pre take and drop proc from [take and drop procs loop]: -/// Can we begin the `take-and-drop` loop? -/obj/machinery/big_manipulator/proc/is_ready_to_work() - if(worker_interaction == WORKER_EMPTY_USE) - try_take_thing() - return TRUE - if(isclosedturf(drop_turf)) - on = !on - say("Output blocked") - return FALSE - for(var/take_item in take_turf.contents) - if(!check_filter(take_item)) - continue - try_take_thing(take_turf, take_item) - break - return TRUE - -/// First take and drop proc from [take and drop procs loop]: -/// Check if we can take item from take_turf to work with him. This proc also calling from ATOM_ENTERED signal. -/obj/machinery/big_manipulator/proc/try_take_thing(datum/source, atom/movable/target) - SIGNAL_HANDLER - - var/empty_hand_check = worker_interaction == WORKER_EMPTY_USE && interaction_mode == INTERACT_USE - - if(!on) - return - if(!anchored) - return - if(status == STATUS_BUSY) - return - if(!empty_hand_check) - if(QDELETED(source) || QDELETED(target)) - return - if(!isturf(target.loc)) - return - if(!check_filter(target)) - return - if(!use_energy(active_power_usage, force = FALSE)) - on = FALSE - say("Not enough energy!") - return - start_work(target, empty_hand_check) - -/// Second take and drop proc from [take and drop procs loop]: -/// Taking our item and start manipulator hand rotate animation. -/obj/machinery/big_manipulator/proc/start_work(atom/movable/target, hand_is_empty = FALSE) - if(!hand_is_empty) - target.forceMove(src) - containment_obj = WEAKREF(target) - manipulator_arm.update_claw(containment_obj) - status = STATUS_BUSY - do_rotate_animation(1) - check_next_move(target, hand_is_empty) - -/// 2.5 take and drop proc from [take and drop procs loop]: -/// Choose what we will do with our item by checking the interaction_mode. -/obj/machinery/big_manipulator/proc/check_next_move(atom/movable/target, hand_is_empty = FALSE) - if(hand_is_empty) - addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand)), interaction_delay SECONDS) - return - switch(interaction_mode) - if(INTERACT_DROP) - addtimer(CALLBACK(src, PROC_REF(drop_thing), target), interaction_delay SECONDS) - if(INTERACT_USE) - addtimer(CALLBACK(src, PROC_REF(use_thing), target), interaction_delay SECONDS) - if(INTERACT_THROW) - addtimer(CALLBACK(src, PROC_REF(throw_thing), target), interaction_delay SECONDS) - -/// 3.1 take and drop proc from [take and drop procs loop]: -/// Drop our item. -/// Checks the priority to drop item not only ground but also in the storage. -/obj/machinery/big_manipulator/proc/drop_thing(atom/movable/target) - var/where_we_drop = search_type_by_priority_in_drop_turf(allowed_priority_settings) - if(isnull(where_we_drop)) - addtimer(CALLBACK(src, PROC_REF(drop_thing), target), interaction_delay SECONDS) - return - if((where_we_drop == drop_turf) || !isitem(target)) - target.forceMove(drop_turf) - target.dir = get_dir(get_turf(target), get_turf(src)) - else - var/atom/drop_target = where_we_drop - if(drop_target.atom_storage) - if(!drop_target.atom_storage.attempt_insert(target, override = TRUE, messages = FALSE)) - target.forceMove(drop_target.drop_location()) - else - target.forceMove(where_we_drop) - finish_manipulation() - -/// 3.2 take and drop proc from [take and drop procs loop]: -/// Use our item on random atom in drop turf contents then -/// Starts manipulator hand backward animation by defualt, but -/// You can also set the setting in ui so that it does not return to its privious position and continues to use object in its hand. -/// Checks the priority so that you can configure which object it will select: mob/obj/turf. -/// Also can use filter to interact only with obj in filter. -/obj/machinery/big_manipulator/proc/use_thing(atom/movable/target, hand_is_empty = FALSE) - var/obj/obj_resolve = containment_obj?.resolve() - if(isnull(obj_resolve)) - finish_manipulation() - return - var/mob/living/carbon/human/monkey_resolve = monkey_worker?.resolve() - if(isnull(monkey_resolve)) - finish_manipulation() - return - /// If we forceMoved from manipulator we are free now. - if(obj_resolve.loc != src && obj_resolve.loc != monkey_resolve) - finish_manipulation() - return - if(!isitem(target)) - target.forceMove(drop_turf) /// We use only items - target.dir = get_dir(get_turf(target), get_turf(src)) - finish_manipulation() - return - var/obj/item/im_item = target - var/atom/type_to_use = search_type_by_priority_in_drop_turf(allowed_priority_settings) - if(isnull(type_to_use)) - check_end_of_use(im_item, item_was_used = FALSE) - return - monkey_resolve.put_in_active_hand(im_item) - if(im_item.GetComponent(/datum/component/two_handed)) /// Using two-handed items in two hands. - im_item.attack_self(monkey_resolve) - im_item.melee_attack_chain(monkey_resolve, type_to_use, resolved_modifiers) - do_attack_animation(drop_turf) - manipulator_arm.do_attack_animation(drop_turf) - check_end_of_use(im_item, item_was_used = TRUE) - -/obj/machinery/big_manipulator/proc/use_thing_with_empty_hand() - var/mob/living/carbon/human/monkey_resolve = monkey_worker?.resolve() - if(isnull(monkey_resolve)) - finish_manipulation() - return - var/atom/type_to_use = search_type_by_priority_in_drop_turf(allowed_priority_settings) - if(isnull(type_to_use)) - check_end_of_use_for_use_with_empty_hand() - return - /// For empty hand on an item, we use the item in-hand - if(isitem(type_to_use)) - var/obj/item/interact_with_item = type_to_use - var/resolve_loc = interact_with_item.loc - monkey_resolve.put_in_active_hand(interact_with_item) - interact_with_item.attack_self(monkey_resolve) - interact_with_item.forceMove(resolve_loc) - else - monkey_resolve.UnarmedAttack(type_to_use, modifiers = resolved_modifiers) - do_attack_animation(drop_turf) - manipulator_arm.do_attack_animation(drop_turf) - check_end_of_use_for_use_with_empty_hand() - -/obj/machinery/big_manipulator/proc/check_end_of_use_for_use_with_empty_hand(obj/item/my_item, item_was_used) - if(!on || (worker_interaction != WORKER_EMPTY_USE && interaction_mode == INTERACT_USE)) - finish_manipulation() - return - addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand), my_item), interaction_delay SECONDS) - -/// Check what we gonna do next with our item. Drop it or use again. -/obj/machinery/big_manipulator/proc/check_end_of_use(obj/item/my_item, item_was_used) - if(!on) - my_item.forceMove(drop_turf) - my_item.dir = get_dir(get_turf(my_item), get_turf(src)) - finish_manipulation() - return - if(worker_interaction == WORKER_SINGLE_USE && item_was_used) - my_item.forceMove(drop_turf) - my_item.dir = get_dir(get_turf(my_item), get_turf(src)) - finish_manipulation() - return - addtimer(CALLBACK(src, PROC_REF(use_thing), my_item), interaction_delay SECONDS) - -/// 3.3 take and drop proc from [take and drop procs loop]: -/// Throw item away!!! -/obj/machinery/big_manipulator/proc/throw_thing(atom/movable/target) - if(!(isitem(target) || isliving(target))) - target.forceMove(drop_turf) - target.dir = get_dir(get_turf(target), get_turf(src)) - finish_manipulation() /// We throw only items and living mobs - return - var/obj/item/im_item = target - im_item.forceMove(drop_turf) - im_item.throw_at(get_edge_target_turf(get_turf(src), drop_here), manipulator_throw_range - 1, 2) - src.do_attack_animation(drop_turf) - manipulator_arm.do_attack_animation(drop_turf) - finish_manipulation() - -/// End of thirds take and drop proc from [take and drop procs loop]: -/// Starts manipulator hand backward animation. -/obj/machinery/big_manipulator/proc/finish_manipulation() - containment_obj = null - manipulator_arm.update_claw(null) - do_rotate_animation(0) - addtimer(CALLBACK(src, PROC_REF(end_work)), interaction_delay SECONDS) - -/// Fourth and last take and drop proc from [take and drop procs loop]: -/// Finishes work and begins to look for a new item for [take and drop procs loop]. -/obj/machinery/big_manipulator/proc/end_work() - status = STATUS_IDLE - is_ready_to_work() - -/// Rotates manipulator hand 90 degrees. -/obj/machinery/big_manipulator/proc/do_rotate_animation(backward) - animate(manipulator_arm, transform = matrix(90, MATRIX_ROTATE), interaction_delay SECONDS * 0.5) - addtimer(CALLBACK(src, PROC_REF(finish_rotate_animation), backward), interaction_delay SECONDS * 0.5) - -/// Rotates manipulator hand from 90 degrees to 180 or 0 if backward. -/obj/machinery/big_manipulator/proc/finish_rotate_animation(backward) - animate(manipulator_arm, transform = matrix(180 * backward, MATRIX_ROTATE), interaction_delay SECONDS * 0.5) - -/obj/machinery/big_manipulator/proc/check_filter(atom/movable/target) - if (target.anchored || HAS_TRAIT(target, TRAIT_NODROP)) - return FALSE - if(!istype(target, selected_type)) - return FALSE - /// We use filter only on items. closets, humans and etc don't need filter check. - if(!isitem(target)) - return TRUE - var/obj/item/target_item = target - if (target_item.item_flags & (ABSTRACT|DROPDEL)) - return FALSE - var/filtered_obj = filter_obj?.resolve() - if((filtered_obj && !istype(target_item, filtered_obj))) - return FALSE - return TRUE - -/// Proc called when we changing item interaction mode. -/obj/machinery/big_manipulator/proc/change_mode() - var/list/available_modes = list(INTERACT_DROP, INTERACT_USE, INTERACT_THROW) - - if(isnull(monkey_worker)) - available_modes = list(INTERACT_DROP, INTERACT_THROW) - - interaction_mode = cycle_value(interaction_mode, available_modes) - update_priority_list() - is_ready_to_work() - -/// Update priority list in ui. Creating new list and sort it by priority number. -/obj/machinery/big_manipulator/proc/update_priority_list() - allowed_priority_settings = list() - var/list/priority_mode_list - if(interaction_mode == INTERACT_DROP) - priority_mode_list = priority_settings_for_drop.Copy() - if(interaction_mode == INTERACT_USE) - priority_mode_list = priority_settings_for_use.Copy() - if(isnull(priority_mode_list)) - return - for(var/we_need_increasing in 1 to length(priority_mode_list)) - for(var/datum/manipulator_priority/what_priority in priority_mode_list) - if(what_priority.number != we_need_increasing) - continue - allowed_priority_settings += what_priority - -/// Proc that return item by type in priority list. Selects item and increasing priority number if don't found req type. -/obj/machinery/big_manipulator/proc/search_type_by_priority_in_drop_turf(list/priority_list) - var/lazy_counter = 1 - for(var/datum/manipulator_priority/take_type in priority_list) - /// If we set override_priority on TRUE we don't go to priority below. - if(lazy_counter > 1 && override_priority) - return null - /// If we need turf we don't check turf.contents and just return drop_turf. - if(take_type.what_type == /turf) - return drop_turf - lazy_counter++ - for(var/type_in_priority in drop_turf.contents) - if(!istype(type_in_priority, take_type.what_type)) - continue - return type_in_priority - -/// Proc call when we press on/off button -/obj/machinery/big_manipulator/proc/press_on(pressed_by) - if(pressed_by) - on = !on - if(!is_ready_to_work()) - return - if(on) - RegisterSignal(take_turf, COMSIG_ATOM_ENTERED, PROC_REF(try_take_thing)) - RegisterSignal(take_turf, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON, PROC_REF(try_take_thing)) - else - UnregisterSignal(take_turf, COMSIG_ATOM_ENTERED) - UnregisterSignal(take_turf, COMSIG_ATOM_AFTER_SUCCESSFUL_INITIALIZED_ON) - -/// Proc that check if button not cutted when we press on button. -/obj/machinery/big_manipulator/proc/try_press_on(mob/user) - if(power_access_wire_cut) - balloon_alert(user, "button is cut off!") - return - press_on(pressed_by = TRUE) - -/// Drop item that manipulator is manipulating. -/obj/machinery/big_manipulator/proc/drop_held_object() - if(isnull(containment_obj)) - return - var/obj/obj_resolve = containment_obj?.resolve() - obj_resolve?.forceMove(get_turf(obj_resolve)) - finish_manipulation() - -/// Changes manipulator working speed time. -/obj/machinery/big_manipulator/proc/change_delay(new_delay) - interaction_delay = round(clamp(new_delay, minimal_delay, MAX_DELAY), DELAY_STEP) - -/obj/machinery/big_manipulator/ui_interact(mob/user, datum/tgui/ui) - if(id_locked) - to_chat(user, span_warning("[src] is locked behind id authentication!")) - ui?.close() - return - if(!anchored) - to_chat(user, span_warning("[src] isn't attached to the ground!")) - ui?.close() - return - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "BigManipulator") - ui.open() - -/obj/machinery/big_manipulator/ui_data(mob/user) - var/list/data = list() - data["active"] = on - data["item_as_filter"] = filter_obj?.resolve() - data["selected_type"] = selected_type.name - data["interaction_mode"] = interaction_mode - data["has_worker"] = !isnull(monkey_worker) - data["worker_interaction"] = worker_interaction - data["worker_combat_mode"] = worker_combat_mode - data["worker_alt_mode"] = worker_alt_mode - data["highest_priority"] = override_priority - data["throw_range"] = manipulator_throw_range - var/list/priority_list = list() - data["settings_list"] = list() - for(var/datum/manipulator_priority/allowed_setting as anything in allowed_priority_settings) - var/list/priority_data = list() - priority_data["name"] = allowed_setting.name - priority_data["priority_width"] = allowed_setting.number - priority_list += list(priority_data) - data["settings_list"] = priority_list - data["min_delay"] = minimal_delay - data["interaction_delay"] = interaction_delay - return data - -/obj/machinery/big_manipulator/ui_static_data(mob/user) - var/list/data = list() - data["delay_step"] = DELAY_STEP - data["max_delay"] = MAX_DELAY - return data - -/obj/machinery/big_manipulator/ui_act(action, params, datum/tgui/ui) - . = ..() - if(.) - return - switch(action) - if("on") - try_press_on(ui.user) - return TRUE - if("eject_worker") - eject_worker(ui.user) - return TRUE - if("drop") - drop_held_object() - return TRUE - if("change_take_item_type") - cycle_pickup_type() - return TRUE - if("change_mode") - change_mode() - return TRUE - if("add_filter") - var/mob/living/living_user = ui.user - if(!isliving(living_user)) - return FALSE - var/obj/give_obj_back = filter_obj?.resolve() - if(give_obj_back) - give_obj_back.forceMove(get_turf(src)) - filter_obj = null - is_ready_to_work() - to_chat(living_user, span_warning("Filter removed")) - return TRUE - var/obj/item/get_active_held_item = living_user.get_active_held_item() - if(isnull(get_active_held_item)) - to_chat(living_user, span_warning("You need item in hand to put it as filter")) - return FALSE - filter_obj = WEAKREF(get_active_held_item) - get_active_held_item.forceMove(src) - is_ready_to_work() - return TRUE - if("highest_priority_change") - override_priority = !override_priority - return TRUE - if("worker_interaction_change") - cycle_worker_interaction() - return TRUE - if("worker_combat_mode_change") - worker_combat_mode = !worker_combat_mode - var/mob/living/carbon/human/monkey_resolve = monkey_worker?.resolve() - monkey_resolve?.set_combat_mode(worker_combat_mode) - return TRUE - if("worker_alt_mode_change") - worker_alt_mode = !worker_alt_mode - resolved_modifiers["button"] = (worker_alt_mode ? "right" : "left") - if(resolved_modifiers["button"] == "right") - resolved_modifiers["right"] = TRUE - else - resolved_modifiers.Remove("right") - return TRUE - if("change_priority") - var/new_priority_number = params["priority"] - for(var/datum/manipulator_priority/new_order as anything in allowed_priority_settings) - if(new_order.number != new_priority_number) - continue - new_order.number-- - check_similarities(new_order.number) - break - update_priority_list() - return TRUE - if("cycle_throw_range") - cycle_throw_range() - return TRUE - if("changeDelay") - change_delay(text2num(params["new_delay"])) - return TRUE - -/// Using on change_priority: looks for a setting with the same number that we set earlier and reduce it. -/obj/machinery/big_manipulator/proc/check_similarities(number_we_minus) - for(var/datum/manipulator_priority/similarities as anything in allowed_priority_settings) - if(similarities.number != number_we_minus) - continue - similarities.number++ - break - -/// Manipulator hand. Effect we animate to show that the manipulator is working and moving something. -/obj/effect/big_manipulator_arm - name = "mechanical claw" - desc = "Takes and drops objects." - icon = 'icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi' - icon_state = "hand" - layer = LOW_ITEM_LAYER - appearance_flags = KEEP_TOGETHER | LONG_GLIDE | TILE_BOUND | PIXEL_SCALE - anchored = TRUE - greyscale_config = /datum/greyscale_config/manipulator_arm - pixel_x = -32 - pixel_y = -32 - /// We get item from big manipulator and takes its icon to create overlay. - var/datum/weakref/item_in_my_claw - /// Var to icon that used as overlay on manipulator claw to show what item it grabs. - var/mutable_appearance/icon_overlay - -/obj/effect/big_manipulator_arm/update_overlays() - . = ..() - . += update_item_overlay() - -/obj/effect/big_manipulator_arm/proc/update_item_overlay() - if(isnull(item_in_my_claw)) - return icon_overlay = null - var/atom/movable/item_data = item_in_my_claw.resolve() - icon_overlay = mutable_appearance(item_data.icon, item_data.icon_state, item_data.layer, src, item_data.plane, item_data.alpha, item_data.appearance_flags) - icon_overlay.color = item_data.color - icon_overlay.appearance = item_data.appearance - icon_overlay.pixel_w = 32 + calculate_item_offset(is_x = TRUE) - icon_overlay.pixel_z = 32 + calculate_item_offset(is_x = FALSE) - return icon_overlay - -/// Updates item that is in the claw. -/obj/effect/big_manipulator_arm/proc/update_claw(clawed_item) - item_in_my_claw = clawed_item - update_appearance() - -/// Calculate x and y coordinates so that the item icon appears in the claw and not somewhere in the corner. -/obj/effect/big_manipulator_arm/proc/calculate_item_offset(is_x = TRUE, pixels_to_offset = 32) - var/offset - switch(dir) - if(NORTH) - offset = is_x ? 0 : pixels_to_offset - if(SOUTH) - offset = is_x ? 0 : -pixels_to_offset - if(EAST) - offset = is_x ? pixels_to_offset : 0 - if(WEST) - offset = is_x ? -pixels_to_offset : 0 - return offset - -/// Priorities that manipulator use to choose to work on item with type same with what_type. -/datum/manipulator_priority - /// Name that user will see in ui. - var/name - /// What type carries this priority. - var/what_type - /** - * Place in the priority queue. The lower the number, the more important the priority. - * Doesn't really matter what number you enter, user can set priority for themselves, - * BUT!!! - * Don't write the same numbers in the same parent otherwise something may go wrong. - */ - var/number - -/datum/manipulator_priority/for_drop/on_floor - name = "DROP ON FLOOR" - what_type = /turf - number = 1 - -/datum/manipulator_priority/for_drop/in_storage - name = "DROP IN STORAGE" - what_type = /obj/item/storage - number = 2 - -/datum/manipulator_priority/for_use/on_living - name = "USE ON LIVING" - what_type = /mob/living - number = 1 - -/datum/manipulator_priority/for_use/on_structure - name = "USE ON STRUCTURE" - what_type = /obj/structure - number = 2 - -/datum/manipulator_priority/for_use/on_machinery - name = "USE ON MACHINERY" - what_type = /obj/machinery - number = 3 - -/datum/manipulator_priority/for_use/on_items - name = "USE ON ITEM" - what_type = /obj/item - number = 4 - -/// Cycles the given value in the given list. Retuns the next value in the list, or the first one if the list isn't long enough. -/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values) - var/current_index = possible_values.Find(current_value) - if(current_index == null) - return possible_values[1] - - var/next_index = (current_index % possible_values.len) + 1 - return possible_values[next_index] - -/obj/machinery/big_manipulator/proc/cycle_worker_interaction() - var/list/worker_modes = list(WORKER_NORMAL_USE, WORKER_SINGLE_USE, WORKER_EMPTY_USE) - worker_interaction = cycle_value(worker_interaction, worker_modes) - -/obj/machinery/big_manipulator/proc/cycle_throw_range() - var/list/possible_ranges = list(1, 2, 3, 4, 5, 6, 7) - manipulator_throw_range = cycle_value(manipulator_throw_range, possible_ranges) - -/obj/machinery/big_manipulator/proc/cycle_pickup_type() - selected_type = cycle_value(selected_type, allowed_types_to_pick_up) - is_ready_to_work() - -#undef INTERACT_DROP -#undef INTERACT_USE -#undef INTERACT_THROW - -#undef TAKE_ITEMS -#undef TAKE_CLOSETS -#undef TAKE_HUMANS - -#undef DELAY_STEP -#undef MAX_DELAY - -#undef WORKER_NORMAL_USE -#undef WORKER_SINGLE_USE -#undef WORKER_EMPTY_USE - -#undef STATUS_IDLE -#undef STATUS_BUSY -#undef STATUS_INSTALLING_WORKER -#undef STATUS_UNINSTALLING_WORKER - -#undef MIN_DELAY_TIER_1 -#undef MIN_DELAY_TIER_2 -#undef MIN_DELAY_TIER_3 -#undef MIN_DELAY_TIER_4 diff --git a/code/game/machinery/big_manipulator/_defines.dm b/code/game/machinery/big_manipulator/_defines.dm new file mode 100644 index 00000000000..362831bd8e7 --- /dev/null +++ b/code/game/machinery/big_manipulator/_defines.dm @@ -0,0 +1,67 @@ +// How should the manipulator interact with the point +#define INTERACT_DROP "DROP" +#define INTERACT_USE "USE" +#define INTERACT_THROW "THROW" + +// What should be picked up from the point +#define TAKE_ITEMS 1 +#define TAKE_CLOSETS 2 +#define TAKE_HUMANS 3 + +#define MIN_SPEED_MULTIPLIER_TIER_1 0.5 +#define MIN_SPEED_MULTIPLIER_TIER_2 0.4 +#define MIN_SPEED_MULTIPLIER_TIER_3 0.3 +#define MIN_SPEED_MULTIPLIER_TIER_4 0.1 + +#define MAX_SPEED_MULTIPLIER_TIER_1 2 +#define MAX_SPEED_MULTIPLIER_TIER_2 3 +#define MAX_SPEED_MULTIPLIER_TIER_3 5 +#define MAX_SPEED_MULTIPLIER_TIER_4 6 + +#define MAX_INTERACTION_POINTS_TIER_1 2 +#define MAX_INTERACTION_POINTS_TIER_2 3 +#define MAX_INTERACTION_POINTS_TIER_3 4 +#define MAX_INTERACTION_POINTS_TIER_4 6 + +#define CURRENT_TASK_NONE "NO TASK" // manipulator is off +#define CURRENT_TASK_IDLE "IDLE" // manipulator is skipping a cycle because it has nothing to do +#define CURRENT_TASK_MOVING_PICKUP "MOVING TO PICKUP POINT" +#define CURRENT_TASK_MOVING_DROPOFF "MOVING TO DROPOFF POINT" +#define CURRENT_TASK_INTERACTING "INTERACTING" +#define CURRENT_TASK_STOPPING "STOPPING" + +// How should the worker interact with the point +#define WORKER_SINGLE_USE "SINGLE TIME" +#define WORKER_EMPTY_USE "EMPTY HAND" +#define WORKER_NORMAL_USE "NORMAL" + +// The tasking schedules the manipulator uses to iterate through points +#define TASKING_ROUND_ROBIN "Round Robin" // 1 - 2 - 3 - 2 - 3 +#define TASKING_STRICT_ROBIN "Strict Robin" // 1 - 2 - 3 - (waiting for 1) - 1 - 2 +#define TASKING_PREFER_FIRST "Prefer First" // 1 - 2 - 1 - 2 - 3 - 2 - 1 - 3 (first availiable) + +// Defines if this point is a pickup or a dropoff point +#define TRANSFER_TYPE_PICKUP "PICK UP" +#define TRANSFER_TYPE_DROPOFF "DROP OFF" + +#define BASE_POWER_USAGE 0.2 +#define BASE_INTERACTION_TIME 0.3 SECONDS + +/// How long will the manipulator wait if there's nothing to do +#define CYCLE_SKIP_TIMEOUT 1 SECONDS + +// How should overflow should be handled +#define POINT_OVERFLOW_ALLOWED "ALLOW" +#define POINT_OVERFLOW_FILTERS "TO FILTERS" +#define POINT_OVERFLOW_HELD "TO HELD" +#define POINT_OVERFLOW_FORBIDDEN "FORBID" + +// What should the manipulator do after there's nothing else to interact with on this point anymore +#define POST_INTERACTION_DROP_AT_POINT "AT DROPOFF" +#define POST_INTERACTION_DROP_AT_MACHINE "AT MACHINE" +#define POST_INTERACTION_DROP_NEXT_FITTING "AT ANY FITTING" +#define POST_INTERACTION_WAIT "CONTINUE" + +// Some macros for interaction checks +#define IS_STOPPING (current_task == CURRENT_TASK_STOPPING) +#define IS_BUSY (current_task != CURRENT_TASK_NONE) diff --git a/code/game/machinery/big_manipulator/big_manipulator.dm b/code/game/machinery/big_manipulator/big_manipulator.dm new file mode 100644 index 00000000000..27d85155242 --- /dev/null +++ b/code/game/machinery/big_manipulator/big_manipulator.dm @@ -0,0 +1,854 @@ +/obj/machinery/big_manipulator + name = "big manipulator" + desc = "Operates different objects. Truly, a groundbreaking innovation..." + icon = 'icons/obj/machines/big_manipulator_parts/big_manipulator_core.dmi' + icon_state = "core" + post_init_icon_state = "core" + density = TRUE + circuit = /obj/item/circuitboard/machine/big_manipulator + greyscale_colors = "#d8ce13" + greyscale_config = /datum/greyscale_config/big_manipulator + hud_possible = list(BIG_MANIP_HUD) + + /// Is the manipulator turned on? + var/on = FALSE + /// Was the next cycle already scheduled? + var/next_cycle_scheduled = FALSE + + /// How quickly the manipulator will process it's actions. + var/speed_multiplier = 1 + var/min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1 + var/max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1 + + /// The current task. + var/current_task = CURRENT_TASK_NONE + var/current_task_start_time = 0 + var/current_task_duration = 0 + + /// The object inside the manipulator. + var/datum/weakref/held_object = null + /// The chimp worker that uses the manipulator (handles USE cases). + var/datum/weakref/monkey_worker = null + /// Weakref to the ID that locked this manipulator. + var/datum/weakref/id_lock = null + /// The manipulator's arm. + var/obj/effect/big_manipulator_arm/manipulator_arm = null + /// Is the power access wire cut? Disables the power button if `TRUE`. + var/power_access_wire_cut = FALSE + + /// How many interaction points of each kind can we have? + var/interaction_point_limit = MAX_INTERACTION_POINTS_TIER_1 + /// List of pickup points. + var/list/pickup_points = list() + /// List of dropoff points. + var/list/dropoff_points = list() + + /// Which tasking scenario we use for pickup points? + var/pickup_tasking = TASKING_ROUND_ROBIN + /// Which tasking scenario we use for dropoff points? + var/dropoff_tasking = TASKING_ROUND_ROBIN + + /// List of all HUD icons resembling interaction points. + var/list/hud_points = list() + + /// Pickup strategy for tasking. + var/datum/tasking_strategy/pickup_strategy + /// Dropoff strategy for tasking. + var/datum/tasking_strategy/dropoff_strategy + +/// Re-creates hud images for the points +/obj/machinery/big_manipulator/proc/update_hud() + LAZYCLEARLIST(hud_points) + + var/image/main_hud = hud_list[BIG_MANIP_HUD] + if(!main_hud) + return + + main_hud.loc = get_turf(src) + main_hud.appearance = mutable_appearance('icons/effects/interaction_points.dmi', null, ABOVE_ALL_MOB_LAYER, src, GAME_PLANE) + + main_hud.overlays.Cut() + var/list/point_overlays = list() + + for(var/i in 1 to length(pickup_points)) + var/datum/interaction_point/point = pickup_points[i] + var/turf/target_turf = point.interaction_turf + if(target_turf) + var/mutable_appearance/point_appearance = mutable_appearance('icons/effects/interaction_points.dmi', "pickup_[i]", ABOVE_ALL_MOB_LAYER, src, GAME_PLANE) + var/turf/manip_turf = get_turf(src) + point_appearance.pixel_x = (target_turf.x - manip_turf.x) * 32 + point_appearance.pixel_y = (target_turf.y - manip_turf.y) * 32 + point_overlays += point_appearance + + for(var/i in 1 to length(dropoff_points)) + var/datum/interaction_point/point = dropoff_points[i] + var/turf/target_turf = point.interaction_turf + if(target_turf) + var/mutable_appearance/point_appearance = mutable_appearance('icons/effects/interaction_points.dmi', "dropoff_[i]", ABOVE_ALL_MOB_LAYER, src, GAME_PLANE) + var/turf/manip_turf = get_turf(src) + point_appearance.pixel_x = (target_turf.x - manip_turf.x) * 32 + point_appearance.pixel_y = (target_turf.y - manip_turf.y) * 32 + point_overlays += point_appearance + + main_hud.overlays += point_overlays + hud_points += main_hud + set_hud_image_active(BIG_MANIP_HUD) + +/// Attempts to find a suitable turf near the manipulator +/obj/machinery/big_manipulator/proc/find_suitable_turf() + var/turf/center = get_turf(src) + + for(var/turf/checked_turf in orange(center, 1)) + if(!isclosedturf(checked_turf)) + return checked_turf + + // didn't find any :boowomp: + return null + +/// Attempts to create a new interaction point and assign it to the correct list. +/obj/machinery/big_manipulator/proc/create_new_interaction_point(mob/user, turf/new_turf, list/new_filters, new_filters_status, new_interaction_mode, transfer_type) + if(!new_turf || !isturf(new_turf)) + new_turf = find_suitable_turf() + if(!new_turf) + balloon_alert(user, "no suitable turfs found!") + return FALSE + + var/list/current_points = (transfer_type == TRANSFER_TYPE_PICKUP) ? pickup_points : dropoff_points + var/point_type = (transfer_type == TRANSFER_TYPE_PICKUP) ? "pickup" : "dropoff" + + if(length(current_points) + 1 > interaction_point_limit) + balloon_alert(user, "[point_type] point limit reached!") + return FALSE + + var/datum/stock_part/servo/locate_servo = locate() in component_parts + var/manipulator_tier = locate_servo ? locate_servo.tier : 1 + + var/datum/interaction_point/new_interaction_point = new(new_turf, new_filters, new_filters_status, new_interaction_mode, manipulator_tier) + + if(QDELETED(new_interaction_point)) // if something STILL somehow went wrong + return FALSE + + current_points += new_interaction_point + + if(obj_flags & EMAGGED) + new_interaction_point.type_filters += /mob/living + + if(is_operational) + update_hud() + + return new_interaction_point + +/obj/machinery/big_manipulator/Initialize(mapload) + . = ..() + create_manipulator_arm() + process_upgrades() + if(on) + toggle_power_state(null) + set_wires(new /datum/wires/big_manipulator(src)) + register_context() + + for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds) + diag_hud.add_atom_to_hud(src) + + prepare_huds() + update_hud() + update_strategies() + +/// Checks the component tiers, adjusting the properties of the manipulator. +/obj/machinery/big_manipulator/proc/process_upgrades() + var/datum/stock_part/servo/locate_servo = locate() in component_parts + if(!locate_servo) + return + + var/manipulator_tier = locate_servo.tier + switch(manipulator_tier) + if(-INFINITY to 1) + min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_1 + max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_1 + interaction_point_limit = MAX_INTERACTION_POINTS_TIER_1 + set_greyscale(COLOR_YELLOW) + manipulator_arm?.set_greyscale(COLOR_YELLOW) + if(2) + min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_2 + max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_2 + interaction_point_limit = MAX_INTERACTION_POINTS_TIER_2 + set_greyscale(COLOR_ORANGE) + manipulator_arm?.set_greyscale(COLOR_ORANGE) + if(3) + min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_3 + max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_3 + interaction_point_limit = MAX_INTERACTION_POINTS_TIER_3 + set_greyscale(COLOR_RED) + manipulator_arm?.set_greyscale(COLOR_RED) + if(4 to INFINITY) + min_speed_multiplier = MIN_SPEED_MULTIPLIER_TIER_4 + max_speed_multiplier = MAX_SPEED_MULTIPLIER_TIER_4 + interaction_point_limit = MAX_INTERACTION_POINTS_TIER_4 + set_greyscale(COLOR_PURPLE) + manipulator_arm?.set_greyscale(COLOR_PURPLE) + + active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION * BASE_POWER_USAGE * manipulator_tier + + for(var/datum/interaction_point/each_point in pickup_points) + each_point.interaction_priorities = each_point.fill_priority_list(manipulator_tier) + + for(var/datum/interaction_point/each_point in dropoff_points) + each_point.interaction_priorities = each_point.fill_priority_list(manipulator_tier) + +/obj/machinery/big_manipulator/examine(mob/user) + . = ..() + var/mob/monkey_resolve = monkey_worker?.resolve() + if(!isnull(monkey_resolve)) + . += "You can see a poor [monkey_resolve.name] buckled to [src]. You wonder if it's getting paid enough." + +/obj/machinery/big_manipulator/attack_hand_secondary(mob/living/user, list/modifiers) + try_press_on(user) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/machinery/big_manipulator/add_context(atom/source, list/context, obj/item/held_item, mob/user) + . = ..() + + context[SCREENTIP_CONTEXT_RMB] = "Toggle" + + if(isnull(held_item)) + context[SCREENTIP_CONTEXT_LMB] = panel_open ? "Interact with wires" : "Open UI" + return CONTEXTUAL_SCREENTIP_SET + + if(held_item.tool_behaviour == TOOL_WRENCH) + context[SCREENTIP_CONTEXT_LMB] = "[anchored ? "Una" : "A"]nchor" + context[SCREENTIP_CONTEXT_RMB] = "Rotate clockwise" + return CONTEXTUAL_SCREENTIP_SET + if(held_item.tool_behaviour == TOOL_SCREWDRIVER) + context[SCREENTIP_CONTEXT_LMB] = "[panel_open ? "Close" : "Open"] panel" + return CONTEXTUAL_SCREENTIP_SET + if(held_item.tool_behaviour == TOOL_CROWBAR && panel_open) + context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" + return CONTEXTUAL_SCREENTIP_SET + if(is_wire_tool(held_item) && panel_open) + context[SCREENTIP_CONTEXT_LMB] = "Interact with wires" + return CONTEXTUAL_SCREENTIP_SET + +/obj/machinery/big_manipulator/Destroy(force) + remove_all_huds() + for(var/datum/atom_hud/data/diagnostic/diag_hud in GLOB.huds) + diag_hud.remove_atom_from_hud(src) + + QDEL_NULL(manipulator_arm) + // QDEL_NULL(monkey_worker.resolve()) + // QDEL_NULL(held_object.resolve()) + id_lock = null + . = ..() + +/obj/machinery/big_manipulator/Exited(atom/movable/gone, direction) + . = ..() + if(isnull(monkey_worker)) + return + + var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker.resolve() + if(gone != poor_monkey) + return + + manipulator_arm.vis_contents -= poor_monkey + poor_monkey.remove_offsets(type) + monkey_worker = null + +/obj/machinery/big_manipulator/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + + if(!old_loc || !isturf(old_loc)) + return + + var/turf/old_turf = old_loc + var/turf/new_turf = get_turf(src) + if(!new_turf || old_turf == new_turf) + return + + var/dx = new_turf.x - old_turf.x + var/dy = new_turf.y - old_turf.y + + if(dx == 0 && dy == 0) // if we rotated for instance + return + + for(var/datum/interaction_point/point in pickup_points) + update_point_position(point, dx, dy) + + for(var/datum/interaction_point/point in dropoff_points) + update_point_position(point, dx, dy) + + if(is_operational) + update_hud() + +/// Updates a single interaction point's position by the given offset +/obj/machinery/big_manipulator/proc/update_point_position(datum/interaction_point/point, dx, dy) + if(!point || !point.interaction_turf) + return + + var/turf/old_turf = point.interaction_turf + if(!old_turf) + return + + var/turf/manipulator_turf = get_turf(src) + if(!manipulator_turf) + return + + var/turf/new_turf = locate(old_turf.x + dx, old_turf.y + dy, manipulator_turf.z) + + // if manipulator is not anchored, allow points to be anywhere (even in walls) to not mess up your stuff when moving it + if(!anchored) + if(new_turf) + point.interaction_turf = new_turf + return + + if(!new_turf || isclosedturf(new_turf)) + new_turf = find_suitable_turf_near(new_turf || old_turf) + if(!new_turf) + remove_invalid_point(point) + return + + if(new_turf == old_turf) + return + + point.interaction_turf = new_turf + +/// Finds a suitable turf near the given location +/obj/machinery/big_manipulator/proc/find_suitable_turf_near(turf/center) + if(!center) + return null + + var/turf/manipulator_turf = get_turf(src) + if(!manipulator_turf) + return null + + for(var/turf/each in orange(1, src)) + if(!isclosedturf(each)) + return each + + return null + +/// Removes an invalid interaction point from the lists. +/obj/machinery/big_manipulator/proc/remove_invalid_point(datum/interaction_point/point) + if(!point) + return + + pickup_points.Remove(point) + dropoff_points.Remove(point) + + qdel(point) + if(is_operational) + update_hud() + +/obj/machinery/big_manipulator/emag_act(mob/user, obj/item/card/emag/emag_card) + . = ..() + if(obj_flags & EMAGGED) + return FALSE + + balloon_alert(user, "overloaded") + obj_flags |= EMAGGED + + for(var/datum/interaction_point/pickup_point in pickup_points) + pickup_point.type_filters += /mob/living + for(var/datum/interaction_point/dropoff_point in dropoff_points) + dropoff_point.type_filters += /mob/living + + return TRUE + +/obj/machinery/big_manipulator/wrench_act(mob/living/user, obj/item/tool) + . = ..() + default_unfasten_wrench(user, tool, time = 1 SECONDS) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/big_manipulator/can_be_unfasten_wrench(mob/user, silent) + if(current_task != CURRENT_TASK_NONE || on) + to_chat(user, span_warning("[src] is activated!")) + return FAILED_UNFASTEN + return ..() + +/obj/machinery/big_manipulator/default_unfasten_wrench(mob/user, obj/item/wrench, time) + . = ..() + if(. == SUCCESSFUL_UNFASTEN) + if(anchored) // on anchoring, validate all points and remove invalid ones + validate_all_points() + update_hud() + else + remove_all_huds() + +/obj/machinery/big_manipulator/screwdriver_act(mob/living/user, obj/item/tool) + if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool)) + return ITEM_INTERACT_SUCCESS + return ITEM_INTERACT_BLOCKING + +/obj/machinery/big_manipulator/crowbar_act(mob/living/user, obj/item/tool) + . = ..() + if(default_deconstruction_crowbar(tool)) + return ITEM_INTERACT_SUCCESS + return ITEM_INTERACT_BLOCKING + +/obj/machinery/big_manipulator/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(user.combat_mode) + return NONE + if(!panel_open || !is_wire_tool(tool)) + return NONE + wires.interact(user) + return ITEM_INTERACT_SUCCESS + +/obj/machinery/big_manipulator/RefreshParts() + . = ..() + process_upgrades() + +/obj/machinery/big_manipulator/mouse_drop_dragged(atom/drop_point, mob/user, src_location, over_location, params) + if(current_task != CURRENT_TASK_NONE) + balloon_alert(user, "turn it off first!") + return + + var/mob/living/carbon/human/species/monkey/poor_monkey = monkey_worker?.resolve() + if(!poor_monkey) + return + + balloon_alert(user, "trying to unbuckle...") + if(!do_after(user, 3 SECONDS, src)) + balloon_alert(user, "interrupted") + return + + balloon_alert(user, "unbuckled") + poor_monkey.drop_all_held_items() + poor_monkey.forceMove(drop_point) + +/obj/machinery/big_manipulator/mouse_drop_receive(atom/monkey, mob/user, params) + if(current_task != CURRENT_TASK_NONE) + balloon_alert(user, "turn it off first!") + return + + if(monkey_worker?.resolve()) + return + + if(!ismonkey(monkey)) + return + + var/mob/living/carbon/human/species/monkey/poor_monkey = monkey + if(poor_monkey.mind) + balloon_alert(user, "too smart!") + return + + poor_monkey.balloon_alert(user, "trying to buckle...") + if(!do_after(user, 3 SECONDS, poor_monkey)) + poor_monkey.balloon_alert(user, "interrupted") + return + + balloon_alert(user, "buckled") + monkey_worker = WEAKREF(poor_monkey) + poor_monkey.drop_all_held_items() + poor_monkey.forceMove(src) + manipulator_arm.vis_contents += poor_monkey + poor_monkey.dir = manipulator_arm.dir + poor_monkey.add_offsets( + type, + x_add = 32 + manipulator_arm.calculate_item_offset(TRUE, pixels_to_offset = 16), + y_add = 32 + manipulator_arm.calculate_item_offset(FALSE, pixels_to_offset = 16) + ) + +/obj/machinery/big_manipulator/attackby(obj/item/some_item, mob/user, params) + . = ..() + if(!isidcard(some_item)) + return + + var/obj/item/card/id/clicked_by_this_id = some_item + + if(!id_lock) + id_lock = WEAKREF(clicked_by_this_id) + balloon_alert(user, "successfully locked") + return + var/obj/item/card/id/resolve_id = id_lock.resolve() + if(clicked_by_this_id != resolve_id) + balloon_alert(user, "locked by another id") + return + id_lock = null + balloon_alert(user, "successfully unlocked") + +/// Attaching the arm effect to the core. +/obj/machinery/big_manipulator/proc/create_manipulator_arm() + manipulator_arm = new /obj/effect/big_manipulator_arm(src) + manipulator_arm.dir = NORTH + vis_contents += manipulator_arm + +/obj/machinery/big_manipulator/proc/toggle_power_state(mob/user) + var/newly_on = !on + + if(!user) + on = newly_on + if(!on) + remove_all_huds() + return + + if(newly_on) + if(!powered()) + balloon_alert(user, "no power!") + return + + if(!anchored) + balloon_alert(user, "anchor first!") + return + + validate_all_points() + + on = newly_on + SStgui.update_uis(src) + try_kickstart(user) + + else + drop_held_atom() + on = newly_on + next_cycle_scheduled = FALSE + // Set stopping task instead of ending current task immediately + if(current_task != CURRENT_TASK_NONE && current_task != CURRENT_TASK_STOPPING) + start_task(CURRENT_TASK_STOPPING, 0) + // Schedule automatic completion of stopping task + addtimer(CALLBACK(src, PROC_REF(complete_stopping_task)), 1 SECONDS) + else + end_current_task() + SStgui.update_uis(src) + +/obj/machinery/big_manipulator/proc/validate_all_points() + for(var/datum/interaction_point/point in pickup_points) + if(!point.is_valid()) + pickup_points -= point + qdel(point) + + for(var/datum/interaction_point/point in dropoff_points) + if(!point.is_valid()) + dropoff_points -= point + qdel(point) + + if(is_operational) + update_hud() + +/// Attempts to press the power button. +/obj/machinery/big_manipulator/proc/try_press_on(mob/living/carbon/human/user) + if(power_access_wire_cut) + balloon_alert(user, "unresponsive!") + return + + // Reject activation during stopping task + if(current_task == CURRENT_TASK_STOPPING) + balloon_alert(user, "stopping in progress!") + return + + toggle_power_state(user) + if(on) + balloon_alert(user, "activated") + else + balloon_alert(user, "deactivated") + +/// Drop the held atom. +/obj/machinery/big_manipulator/proc/drop_held_atom() + if(isnull(held_object)) + return + var/obj/obj_resolve = held_object?.resolve() + obj_resolve?.forceMove(get_turf(obj_resolve)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) // MCBALAAM TODO + +/obj/machinery/big_manipulator/ui_interact(mob/user, datum/tgui/ui) + if(id_lock) + to_chat(user, span_warning("[src] is locked behind ID authentication!")) + ui?.close() + return + if(!anchored) + to_chat(user, span_warning("[src] isn't attached to the ground!")) + ui?.close() + return + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "BigManipulator") + ui.open() + +/obj/machinery/big_manipulator/ui_data(mob/user) + var/list/data = list() + data["active"] = on + data["current_task"] = current_task + data["current_task_duration"] = current_task_duration + data["speed_multiplier"] = speed_multiplier + data["min_speed_multiplier"] = min_speed_multiplier + data["max_speed_multiplier"] = max_speed_multiplier + data["manipulator_position"] = "[x],[y]" + data["pickup_tasking"] = pickup_tasking + data["dropoff_tasking"] = dropoff_tasking + + var/list/pickup_points_data = list() + for(var/datum/interaction_point/point in pickup_points) + var/list/point_data = list() + point_data["name"] = point.name + point_data["id"] = REF(point) + var/turf/resolved_turf = point.interaction_turf + point_data["turf"] = resolved_turf ? "[resolved_turf.x],[resolved_turf.y]" : "0,0" + point_data["mode"] = "PICK" + var/list/filter_names = list() + for(var/obj/item/some_path as anything in point.atom_filters) + filter_names += some_path::name + point_data["item_filters"] = filter_names + point_data["filters_status"] = point.should_use_filters + point_data["filtering_mode"] = point.filtering_mode + point_data["worker_interaction"] = point.worker_interaction + point_data["overflow_status"] = point.overflow_status + point_data["worker_use_rmb"] = point.worker_use_rmb + point_data["worker_combat_mode"] = point.worker_combat_mode + point_data["throw_range"] = point.throw_range + + var/list/settings_list_pick = list() + for(var/datum/manipulator_priority/pr_pick in point.interaction_priorities) + var/list/entry_pick = list() + entry_pick["name"] = pr_pick.name + entry_pick["active"] = pr_pick.active + settings_list_pick += list(entry_pick) + point_data["settings_list"] = settings_list_pick + pickup_points_data += list(point_data) + data["pickup_points"] = pickup_points_data + + var/list/dropoff_points_data = list() + for(var/datum/interaction_point/point in dropoff_points) + var/list/point_data = list() + point_data["name"] = point.name + point_data["id"] = REF(point) + var/turf/resolved_turf = point.interaction_turf + point_data["turf"] = resolved_turf ? "[resolved_turf.x],[resolved_turf.y]" : "0,0" + point_data["mode"] = point.interaction_mode + var/list/filter_names = list() + for(var/obj/item/some_path as anything in point.atom_filters) + filter_names += some_path::name + point_data["item_filters"] = filter_names + point_data["filters_status"] = point.should_use_filters + point_data["filtering_mode"] = point.filtering_mode + point_data["worker_interaction"] = point.worker_interaction + point_data["overflow_status"] = point.overflow_status + point_data["worker_use_rmb"] = point.worker_use_rmb + point_data["worker_combat_mode"] = point.worker_combat_mode + point_data["throw_range"] = point.throw_range + point_data["use_post_interaction"] = point.use_post_interaction + + var/list/settings_list_drop = list() + for(var/datum/manipulator_priority/pr_drop in point.interaction_priorities) + var/list/entry_drop = list() + entry_drop["name"] = pr_drop.name + entry_drop["active"] = pr_drop.active + settings_list_drop += list(entry_drop) + point_data["settings_list"] = settings_list_drop + dropoff_points_data += list(point_data) + data["dropoff_points"] = dropoff_points_data + + return data + +/obj/machinery/big_manipulator/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) + return + + switch(action) + if("run_cycle") + try_press_on(ui.user) + return TRUE + + if("drop_held_atom") + drop_held_atom() + return TRUE + + if("create_pickup_point") + create_new_interaction_point(ui.user, null, list(), FALSE, null, TRANSFER_TYPE_PICKUP) + return TRUE + + if("create_dropoff_point") + create_new_interaction_point(ui.user, null, list(), FALSE, INTERACT_DROP, TRANSFER_TYPE_DROPOFF) + return TRUE + + if("reset_tasking_Pickup Points") + pickup_strategy.current_index = 1 + balloon_alert(ui.user, "index reset") + return TRUE + + if("reset_tasking_Dropoff Points") + dropoff_strategy.current_index = 1 + balloon_alert(ui.user, "index reset") + return TRUE + + if("cycle_tasking_schedule") + var/new_schedule = params["new_schedule"] + + if(new_schedule in list(TASKING_ROUND_ROBIN, TASKING_STRICT_ROBIN, TASKING_PREFER_FIRST)) + var/is_pickup = params["is_pickup"] + if(is_pickup) + pickup_tasking = new_schedule + else + dropoff_tasking = new_schedule + update_strategies() + return TRUE + + if("adjust_interaction_speed") + var/new_speed = text2num(params["new_speed"]) + if(isnull(new_speed)) + return FALSE + + speed_multiplier = clamp(new_speed, min_speed_multiplier, max_speed_multiplier) + return TRUE + + if("unbuckle") + unbuckle_all_mobs() + monkey_worker = null + return TRUE + + if("adjust_point_param") + return adjust_param_for_point(params["pointId"], params["param"], params["value"], ui.user) + +/obj/machinery/big_manipulator/proc/adjust_param_for_point(point_ref, param, value, mob/user) + if(!param) // there may be no value if we're resetting stuff + return FALSE + + var/datum/interaction_point/target_point = locate(point_ref) in (pickup_points + dropoff_points) + if(!target_point) + return FALSE + + switch(param) + if("set_name") + target_point.name = sanitize_name(value, allow_numbers = TRUE) + return TRUE + + if("toggle_priority") + return target_point.tick_priority_by_index(value) + + if("remove_point") + pickup_points.Remove(target_point) + dropoff_points.Remove(target_point) // one'll hit for sure + update_hud() + qdel(target_point) + return TRUE + + if("reset_atom_filters") + target_point.atom_filters = list() + return TRUE + + if("cycle_dropoff_point_interaction") + target_point.interaction_mode = cycle_value(target_point.interaction_mode, monkey_worker ? list(INTERACT_DROP, INTERACT_THROW, INTERACT_USE) : list(INTERACT_DROP, INTERACT_THROW)) + var/datum/stock_part/servo/locate_servo = locate() in component_parts + var/manipulator_tier = locate_servo ? locate_servo.tier : 1 + target_point.interaction_priorities = target_point.fill_priority_list(manipulator_tier) + return TRUE + + if("toggle_filter_skip") + target_point.should_use_filters = !target_point.should_use_filters + return TRUE + + if("cycle_pickup_point_type") + target_point.filtering_mode = cycle_value(target_point.filtering_mode, obj_flags & EMAGGED ? list(TAKE_ITEMS, TAKE_CLOSETS, TAKE_HUMANS) : list(TAKE_ITEMS, TAKE_CLOSETS)) + return TRUE + + if("cycle_worker_interaction") + target_point.worker_interaction = cycle_value(target_point.worker_interaction, list(WORKER_NORMAL_USE, WORKER_SINGLE_USE, WORKER_EMPTY_USE)) + return TRUE + + if("cycle_overflow_status") + target_point.overflow_status = cycle_value(target_point.overflow_status, list(POINT_OVERFLOW_ALLOWED, POINT_OVERFLOW_FILTERS, POINT_OVERFLOW_HELD, POINT_OVERFLOW_FORBIDDEN)) + return TRUE + + if("cycle_throw_range") + target_point.throw_range = cycle_value(target_point.throw_range, list(1, 2, 3, 4, 5, 6, 7)) + return TRUE + + if("cycle_post_interaction") + target_point.use_post_interaction = cycle_value(target_point.use_post_interaction, list(POST_INTERACTION_DROP_AT_POINT, POST_INTERACTION_DROP_AT_MACHINE, POST_INTERACTION_DROP_NEXT_FITTING, POST_INTERACTION_WAIT)) + return TRUE + + if("add_atom_filter_from_held") + var/obj/item/held_item = user.get_active_held_item() + + if(!held_item) + return FALSE + + for(var/filter_path in target_point.atom_filters) + if(istype(held_item, filter_path)) + return FALSE + + target_point.atom_filters += held_item.type + return TRUE + + if("delete_filter") + target_point.atom_filters.Cut(value, value + 1) + return TRUE + + if("toggle_worker_rmb") + target_point.worker_use_rmb = !target_point.worker_use_rmb + return TRUE + + if("toggle_worker_combat") + target_point.worker_combat_mode = !target_point.worker_combat_mode + return TRUE + + if("priority_move_up") + return target_point.move_priority_up_by_index(value) + + if("move_to") + var/button_number = text2num(value["buttonNumber"]) + + var/dx = ((button_number - 1) % 3) - 1 + var/dy = 1 - round((button_number - 1) / 3) + + var/turf/new_turf = locate(x + dx, y + dy, z) + if(!new_turf || isclosedturf(new_turf)) + return FALSE + + target_point.interaction_turf = new_turf + update_hud() + return TRUE + +/// Cycles the given value in the given list. Retuns the next value in the list, or the first one if the list isn't long enough. +/obj/machinery/big_manipulator/proc/cycle_value(current_value, list/possible_values) + var/current_index = possible_values.Find(current_value) + if(current_index == 0) + return possible_values[1] + + var/next_index = (current_index % length(possible_values)) + 1 + return possible_values[next_index] + +/// Begins a new task with the specified type and duration +/obj/machinery/big_manipulator/proc/start_task(task_type, duration) + if(current_task == CURRENT_TASK_STOPPING) + return + + end_current_task() // ends any previous task first (momentarily sets IDLE) + current_task_start_time = world.time + current_task_duration = duration + current_task = task_type + SStgui.update_uis(src) + +/// Ends the current task +/obj/machinery/big_manipulator/proc/end_current_task() + current_task_start_time = 0 + current_task_duration = 0 + if(current_task == CURRENT_TASK_STOPPING) + current_task = CURRENT_TASK_NONE + SStgui.update_uis(src) // Update UI immediately + +/// Completes the stopping task and transitions to TASK_NONE +/obj/machinery/big_manipulator/proc/complete_stopping_task() + on = FALSE + next_cycle_scheduled = FALSE + end_current_task() + SStgui.update_uis(src) + +/obj/machinery/big_manipulator/proc/remove_all_huds() + var/image/main_hud = hud_list[BIG_MANIP_HUD] + if(main_hud) + main_hud.overlays.Cut() + main_hud.loc = null + + hud_points.Cut() + set_hud_image_inactive(BIG_MANIP_HUD) + +/obj/machinery/big_manipulator/proc/update_strategies() + pickup_strategy = create_strategy(pickup_tasking) + dropoff_strategy = create_strategy(dropoff_tasking) + +/obj/machinery/big_manipulator/proc/create_strategy(tasking_mode) + switch(tasking_mode) + if(TASKING_PREFER_FIRST) + return new /datum/tasking_strategy/prefer_first() + if(TASKING_ROUND_ROBIN) + return new /datum/tasking_strategy/round_robin() + if(TASKING_STRICT_ROBIN) + return new /datum/tasking_strategy/strict_robin() + return new /datum/tasking_strategy/prefer_first() diff --git a/code/game/machinery/big_manipulator/big_manipulator_interactions.dm b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm new file mode 100644 index 00000000000..0d62b6b3ff4 --- /dev/null +++ b/code/game/machinery/big_manipulator/big_manipulator_interactions.dm @@ -0,0 +1,462 @@ +/// Selects which atom to pick up from this point for interaction with available dropoff points based on the dropoff points. +/obj/machinery/big_manipulator/proc/find_pickup_candidate_for_pickup_point(datum/interaction_point/pickup_point) + if(!pickup_point) + return null + + var/turf/pickup_turf = pickup_point.interaction_turf + if(!pickup_turf) + return null + + var/list/candidates = list() + for(var/atom/movable/candidate as anything in pickup_turf.contents) + if(candidate.anchored || HAS_TRAIT(candidate, TRAIT_NODROP) || move_resist > MOVE_FORCE_STRONG) + continue + + if(!pickup_point.check_filters_for_atom(candidate)) + continue + + for(var/datum/interaction_point/dest_point as anything in dropoff_points) + if(!dest_point || !dest_point.is_valid()) + continue + if(!dest_point.check_filters_for_atom(candidate)) + continue + if(dest_point.is_available(TRANSFER_TYPE_DROPOFF, candidate)) + candidates += candidate + break + + if(!length(candidates)) + return null + + return pickup_strategy.get_next_candidate(candidates) + +/// Calculates the next interaction point the manipulator should transfer the item to or pick up it from. +/obj/machinery/big_manipulator/proc/find_next_point(transfer_type) + if(!transfer_type) + return NONE + + var/atom/movable/target = held_object?.resolve() + if(isnull(target) && transfer_type == TRANSFER_TYPE_DROPOFF) + return NONE + + var/list/interaction_points = transfer_type == TRANSFER_TYPE_DROPOFF ? dropoff_points : pickup_points + if(!length(interaction_points)) + return NONE + + var/datum/tasking_strategy/strategy = transfer_type == TRANSFER_TYPE_DROPOFF ? dropoff_strategy : pickup_strategy + var/datum/callback/availability_callback = transfer_type == TRANSFER_TYPE_DROPOFF ? CALLBACK(src, PROC_REF(check_dropoff_availability)) : CALLBACK(src, PROC_REF(check_pickup_availability)) + + return strategy.get_next_available(interaction_points, target, transfer_type, availability_callback) + +/// Attempts to launch the work cycle. Should only be ran on pressing the "Run" button. +/obj/machinery/big_manipulator/proc/try_kickstart(mob/user) + if(!on || !anchored || IS_BUSY) + return FALSE + + if(!use_energy(active_power_usage, force = FALSE)) + on = FALSE + balloon_alert_to_viewers("not enough power!") + return FALSE + + next_cycle_scheduled = FALSE + run_pickup_phase() + +/// Safely schedules the next cycle attempt to prevent overlapping. +/obj/machinery/big_manipulator/proc/schedule_next_cycle() + if(next_cycle_scheduled || IS_STOPPING) + return + + // Allow scheduling if manipulator is idle, none, or if we have a held object (need to drop it off) + if(current_task != CURRENT_TASK_IDLE && current_task != CURRENT_TASK_NONE && !held_object) + return + + next_cycle_scheduled = TRUE + if(held_object) + run_dropoff_phase() + else + run_pickup_phase() + +/// Handles the common pattern of waiting and scheduling next cycle when no work can be done. +/obj/machinery/big_manipulator/proc/handle_no_work_available() + // If we're stopping, don't schedule next cycle + if(IS_STOPPING) + complete_stopping_task() + return FALSE + + current_task = CURRENT_TASK_IDLE + + addtimer(CALLBACK(src, PROC_REF(schedule_next_cycle)), CYCLE_SKIP_TIMEOUT) + return FALSE + +/obj/machinery/big_manipulator/proc/check_pickup_availability(datum/interaction_point/point, atom/movable/target, transfer_type) + if(!point) + return FALSE + + var/turf/pickup_turf = point.interaction_turf + if(!pickup_turf) + return FALSE + + for(var/atom/movable/candidate as anything in pickup_turf.contents) + if(!candidate.anchored && !HAS_TRAIT(candidate, TRAIT_NODROP)) + return TRUE + + return FALSE + +/obj/machinery/big_manipulator/proc/check_dropoff_availability(datum/interaction_point/point, atom/movable/target, transfer_type) + return point.is_available(transfer_type, target) + +/// Attempts to run the pickup phase. Selects the next origin point and attempts to pick up an item from it. +/obj/machinery/big_manipulator/proc/run_pickup_phase() + if(!on || IS_STOPPING) + return + + next_cycle_scheduled = FALSE + + var/datum/interaction_point/origin_point = find_next_point(TRANSFER_TYPE_PICKUP) + + if(!origin_point) + return handle_no_work_available() + + rotate_to_point(origin_point, PROC_REF(try_interact_with_origin_point), CURRENT_TASK_MOVING_PICKUP) + return TRUE + +/// Attempts to interact with the origin point (pick up the object) +/obj/machinery/big_manipulator/proc/try_interact_with_origin_point(datum/interaction_point/origin_point, hand_is_empty = FALSE) + // If we're stopping, just finish the task and shut down + if(IS_STOPPING) + complete_stopping_task() + return + + if(!origin_point.interaction_turf) + return handle_no_work_available() + + var/atom/movable/selected = find_pickup_candidate_for_pickup_point(origin_point) // find a suitable item that matches available destinations + if(!selected) + return handle_no_work_available() + + if(selected.anchored || HAS_TRAIT(selected, TRAIT_NODROP)) + return handle_no_work_available() + + if(isitem(selected)) + var/obj/item/selected_item = selected + if(selected_item.item_flags & (ABSTRACT|DROPDEL)) + return handle_no_work_available() + + start_task(CURRENT_TASK_INTERACTING, 0.2 SECONDS) + interact_with_origin_point(selected, hand_is_empty) + return TRUE + +/// Attempts to start a work cycle (pick up the object) +/obj/machinery/big_manipulator/proc/interact_with_origin_point(atom/movable/target, hand_is_empty = FALSE) + if(!hand_is_empty) + target.forceMove(src) + held_object = WEAKREF(target) + manipulator_arm.update_claw(held_object) + + // Schedule the dropoff phase after a successful pickup to avoid overlapping tasks + if(!hand_is_empty) + schedule_next_cycle() + +/obj/machinery/big_manipulator/proc/run_dropoff_phase() + // Find the next available destination point that can accept the held item + var/datum/interaction_point/destination_point = find_next_point(TRANSFER_TYPE_DROPOFF) + + next_cycle_scheduled = FALSE + + if(!destination_point) + return handle_no_work_available() + + rotate_to_point(destination_point, PROC_REF(try_interact_with_destination_point), CURRENT_TASK_MOVING_DROPOFF) + return TRUE + +/// Attempts to interact with the destination point (drop/use/throw the object) +/obj/machinery/big_manipulator/proc/try_interact_with_destination_point(datum/interaction_point/destination_point, hand_is_empty = FALSE) + // If we're stopping, just finish the task and shut down + if(IS_STOPPING) + complete_stopping_task() + return FALSE + + start_task(CURRENT_TASK_INTERACTING, 0.2 SECONDS) + + if(hand_is_empty) + use_thing_with_empty_hand(destination_point) + return TRUE + + var/obj/actual_held_object = held_object?.resolve() + if(actual_held_object.loc != src) + handle_no_work_available() + return FALSE + + switch(destination_point.interaction_mode) + if(INTERACT_DROP) + try_drop_thing(destination_point) + if(INTERACT_USE) + try_use_thing(destination_point) + if(INTERACT_THROW) + throw_thing(destination_point) + + return TRUE + +/// Rotates the manipulator arm to face the target point. +/obj/machinery/big_manipulator/proc/rotate_to_point(datum/interaction_point/target_point, callback, type) + if(IS_STOPPING) + return + + if(!target_point) + return FALSE + + var/target_dir = get_dir(get_turf(src), target_point.interaction_turf) + var/target_angle = dir2angle(target_dir) + var/current_angle = manipulator_arm.transform.get_angle() + var/angle_diff = closer_angle_difference(current_angle, target_angle) + + var/num_rotations = round(abs(angle_diff) / 45) + var/total_rotation_time = num_rotations * BASE_INTERACTION_TIME / speed_multiplier + + start_task(type == CURRENT_TASK_MOVING_PICKUP ? CURRENT_TASK_MOVING_PICKUP : CURRENT_TASK_MOVING_DROPOFF, total_rotation_time) + + // If the next point is on the same tile, we don't need to rotate at all + if(!num_rotations) + addtimer(CALLBACK(src, callback, target_point), BASE_INTERACTION_TIME) + return TRUE + + // Breaking the angle up into 45 degree steps + var/rotation_step = 45 * SIGN(angle_diff) + do_step_rotation(target_point, callback, current_angle, target_angle, rotation_step, 0, total_rotation_time) + + return TRUE + +/// Does a 45 degree step, animating the claw +/obj/machinery/big_manipulator/proc/do_step_rotation(datum/interaction_point/target_point, callback, current_angle, target_angle, rotation_step, elapsed_time, total_time) + if(IS_STOPPING) + return + + // Just making sure we're not there already + var/angle_diff = closer_angle_difference(current_angle, target_angle) + if(abs(angle_diff) < abs(rotation_step)) + // If this is the last step, doing a precise degree turn + var/matrix/final_matrix = matrix() + final_matrix.Turn(target_angle) + animate(manipulator_arm, transform = final_matrix, time = BASE_INTERACTION_TIME / speed_multiplier) + addtimer(CALLBACK(src, callback, target_point), BASE_INTERACTION_TIME / speed_multiplier) + return + + // Animating a single rotation step + + // YES, this has to be done like that because byond or whatever is stupid and `animate`ing a 180+ degree turn + // fucking fails and squashes the icon vertically (or horizontally, whichever it feels like) instead + + var/next_angle = current_angle + rotation_step + var/matrix/next_matrix = matrix() + next_matrix.Turn(next_angle) + animate(manipulator_arm, transform = next_matrix, time = BASE_INTERACTION_TIME / speed_multiplier) + + // Recursively planning the next step (yay recursion :yuppie: call me a madman I LOVE recursion) + elapsed_time += BASE_INTERACTION_TIME / speed_multiplier + addtimer(CALLBACK(src, PROC_REF(do_step_rotation), target_point, callback, next_angle, target_angle, rotation_step, elapsed_time, total_time), BASE_INTERACTION_TIME / speed_multiplier) + +/// Moves the item onto the turf. +/// +/// If the turf has an atom with fitting `atom_storage` that corresponds to the +/// priority settings, it will attempt to insert the held item. +/obj/machinery/big_manipulator/proc/try_drop_thing(datum/interaction_point/destination_point) + var/drop_endpoint = destination_point.find_type_priority() + var/obj/actual_held_object = held_object?.resolve() + + if(isnull(drop_endpoint)) + return FALSE + + var/atom/drop_target = drop_endpoint + if(drop_target.atom_storage && actual_held_object && (!drop_target.atom_storage.attempt_insert(actual_held_object, override = TRUE, messages = FALSE))) + actual_held_object.forceMove(drop_target.drop_location()) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return TRUE + + actual_held_object?.forceMove(drop_endpoint) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return TRUE + +/// Attempts to use the held object on the atoms of the interaction turf. +/// +/// If the interaction turf has an atom that corresponds to the priority settings, +/// it will attempt to use the held item. If it doesn't, it will simply drop the item. +/obj/machinery/big_manipulator/proc/try_use_thing(datum/interaction_point/destination_point, work_done_at_point = FALSE) + if(IS_STOPPING) + return + + var/obj/obj_resolve = held_object?.resolve() + var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve() + var/destination_turf = destination_point.interaction_turf + + if(!obj_resolve || QDELETED(obj_resolve) || obj_resolve.loc != src) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return FALSE + + if(!monkey_resolve || !destination_turf) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return FALSE + + if(!(monkey_resolve.loc == src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return FALSE + + var/obj/item/held_item = obj_resolve + var/atom/type_to_use = destination_point.find_type_priority() + + if(isnull(type_to_use)) + check_for_cycle_end_drop(destination_point, FALSE, work_done_at_point) + return FALSE + + if(isitem(type_to_use) && !destination_point.check_filters_for_atom(type_to_use)) + check_for_cycle_end_drop(destination_point, FALSE, work_done_at_point) + return FALSE + + var/original_loc = held_item.loc + + monkey_resolve.put_in_active_hand(held_item) + if(held_item.GetComponent(/datum/component/two_handed)) + held_item.attack_self(monkey_resolve) + + var/use_rmb = destination_point.worker_use_rmb + var/use_combat = destination_point.worker_combat_mode + + monkey_resolve.combat_mode = use_combat + held_item.melee_attack_chain(monkey_resolve, type_to_use, list(RIGHT_CLICK = use_rmb ? TRUE : FALSE)) + monkey_resolve.combat_mode = FALSE + do_attack_animation(destination_turf) + manipulator_arm.do_attack_animation(destination_turf) + + // if we destroyed the item while using it OR something else uncanny happened to it and it's GONE + if(QDELETED(held_item) || !held_item || (held_item.loc != monkey_resolve && held_item.loc != original_loc)) + held_object = null + manipulator_arm.update_claw(null) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return TRUE + + if(held_item.loc == monkey_resolve) + held_item.forceMove(original_loc) + + check_for_cycle_end_drop(destination_point, TRUE, TRUE) + +/// Checks what should we do with the `held_object` after `USE`-ing it. +/obj/machinery/big_manipulator/proc/check_for_cycle_end_drop(datum/interaction_point/drop_point, item_used_this_iteration, work_done_at_point = FALSE) + var/obj/obj_resolve = held_object?.resolve() + var/turf/drop_turf = drop_point.interaction_turf + + if(!obj_resolve || obj_resolve.loc != src || QDELETED(obj_resolve)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(drop_point.worker_interaction == WORKER_SINGLE_USE && item_used_this_iteration) + obj_resolve.forceMove(drop_turf) + obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(!on || drop_point.interaction_mode != INTERACT_USE) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(item_used_this_iteration) + addtimer(CALLBACK(src, PROC_REF(try_use_thing), drop_point, TRUE), BASE_INTERACTION_TIME * 2) + return + + switch(drop_point.use_post_interaction) + if(POST_INTERACTION_DROP_AT_POINT) + obj_resolve.forceMove(drop_turf) + obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(POST_INTERACTION_DROP_AT_MACHINE) + obj_resolve.forceMove(get_turf(src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(POST_INTERACTION_DROP_NEXT_FITTING) + var/datum/interaction_point/next = find_next_point(TRANSFER_TYPE_DROPOFF) + if(next) + rotate_to_point(next, PROC_REF(try_interact_with_destination_point), CURRENT_TASK_MOVING_DROPOFF) + return + obj_resolve.forceMove(drop_turf) + obj_resolve.dir = get_dir(get_turf(obj_resolve), get_turf(src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(POST_INTERACTION_WAIT) + schedule_next_cycle() + return + + schedule_next_cycle() + +/// Throws the held object in the direction of the interaction point. +/obj/machinery/big_manipulator/proc/throw_thing(datum/interaction_point/drop_point) + var/drop_turf = drop_point.interaction_turf + var/item_throw_range = drop_point.throw_range + var/atom/movable/held_atom = held_object?.resolve() + + held_atom.forceMove(drop_turf) + do_attack_animation(drop_turf) + manipulator_arm.do_attack_animation(drop_turf) + + if(((isliving(held_atom))) && !(obj_flags & EMAGGED)) + held_atom.dir = get_dir(get_turf(held_atom), get_turf(src)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + held_atom.throw_at(get_edge_target_turf(get_turf(src), get_dir(get_turf(src), get_turf(held_atom))), item_throw_range, 2) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + +/// Uses the empty hand to interact with objects +/obj/machinery/big_manipulator/proc/use_thing_with_empty_hand(datum/interaction_point/destination_point) + var/mob/living/carbon/human/species/monkey/monkey_resolve = monkey_worker?.resolve() + if(isnull(monkey_resolve)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + var/atom/type_to_use = destination_point.find_type_priority() + if(isnull(type_to_use)) + check_end_of_use_for_use_with_empty_hand(destination_point, FALSE) + return + + // We don't perform an unarmed attack on items because we pick them up duh + if(isitem(type_to_use)) + var/obj/item/interact_with_item = type_to_use + var/resolve_loc = interact_with_item.loc + monkey_resolve.put_in_active_hand(interact_with_item) + interact_with_item.attack_self(monkey_resolve) + interact_with_item.forceMove(resolve_loc) + else + monkey_resolve.UnarmedAttack(type_to_use) + + var/turf/dest_turf = destination_point.interaction_turf + if(dest_turf) + do_attack_animation(dest_turf) + manipulator_arm.do_attack_animation(dest_turf) + check_end_of_use_for_use_with_empty_hand(destination_point, TRUE) + +/// Checks if we should continue using the empty hand after interaction +/obj/machinery/big_manipulator/proc/check_end_of_use_for_use_with_empty_hand(datum/interaction_point/destination_point, item_was_used = TRUE) + if(!on || (destination_point.worker_interaction != WORKER_EMPTY_USE && destination_point.interaction_mode == INTERACT_USE)) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + if(!item_was_used) + finish_manipulation(TRANSFER_TYPE_DROPOFF) + return + + addtimer(CALLBACK(src, PROC_REF(use_thing_with_empty_hand), destination_point), BASE_INTERACTION_TIME) + +/// Completes the current manipulation action +/obj/machinery/big_manipulator/proc/finish_manipulation(transfer_type = TRANSFER_TYPE_DROPOFF) + held_object = null + manipulator_arm.update_claw(null) + + end_current_task() + + if(IS_STOPPING) + complete_stopping_task() + return + + current_task = CURRENT_TASK_IDLE + + schedule_next_cycle() diff --git a/code/game/machinery/big_manipulator/interaction_points.dm b/code/game/machinery/big_manipulator/interaction_points.dm new file mode 100644 index 00000000000..3d3a8c4b8fb --- /dev/null +++ b/code/game/machinery/big_manipulator/interaction_points.dm @@ -0,0 +1,239 @@ +/datum/interaction_point + var/name = "interaction point" + + /// The turf this interaction point represents. + var/turf/interaction_turf + /// Should we check our filters while interacting with this point? + var/should_use_filters = FALSE + /// How should this point be interacted with? + var/interaction_mode = INTERACT_DROP + /// How should the monkey worker (if there is one) interact with the target point? + var/worker_interaction = WORKER_NORMAL_USE + /// How far should the manipulator throw the object? + var/throw_range = 1 + /// Which items are supposed to be picked up from `interaction_turf` if this is a pickup point + /// or looked for in the `interaction_turf` if this is a dropoff point. + var/list/atom_filters = list() + /// If this is a dropoff point, influences which interaction endpoints are preferred over which + /// by the manipulator. + var/list/interaction_priorities = list() + /// Should the manipulator put items on this point if there are already such items on the turf? + var/overflow_status = POINT_OVERFLOW_ALLOWED + /// Which object category should the filters be looking out for. + var/filtering_mode = TAKE_ITEMS + /// Whether the worker will use combat mode while interacting with this point. + var/worker_combat_mode = FALSE + /// Whether the worker will simulate RMB instead of LMB on interaction. + var/worker_use_rmb = FALSE + /// List of types that can be picked up from this point + var/list/type_filters = list( + /obj/item, + /obj/structure/closet, + ) + /// What should the manipulator do when there's nothing to "USE" the held item on anymore? + var/use_post_interaction = POST_INTERACTION_DROP_AT_POINT + +/datum/interaction_point/New(turf/new_turf, list/new_filters, new_should_use_filters, new_interaction_mode, new_allowed_types, new_overflow_status, manipulator_tier) + if(!new_turf) + stack_trace("New manipulator interaction point created with no valid turf references passed.") + qdel(src) + return + + if(isclosedturf(new_turf)) + qdel(src) + return + + interaction_turf = new_turf + + if(length(new_filters)) + atom_filters = new_filters + + if(new_should_use_filters) + should_use_filters = new_should_use_filters + + if(new_interaction_mode) + interaction_mode = new_interaction_mode + + if(new_allowed_types) + type_filters = new_allowed_types + + if(new_overflow_status) + overflow_status = new_overflow_status + + interaction_priorities = fill_priority_list(manipulator_tier) + +/// Finds the type priority of the interaction point. +/datum/interaction_point/proc/find_type_priority() + var/list/turf_contents = interaction_turf.contents + + var/atom/movable/best_candidate = null + var/best_priority_index = INFINITY + + for(var/atom/movable/thing as anything in turf_contents) + for(var/i in 1 to length(interaction_priorities)) + if(i >= best_priority_index) + break + + var/datum/manipulator_priority/prio = interaction_priorities[i] + + if(!prio.active) + continue + + if(prio.atom_typepath == /turf) + if(i < best_priority_index) + best_candidate = interaction_turf + best_priority_index = i + continue + + if(!istype(thing, prio.atom_typepath)) + continue + + if(isliving(thing)) + var/mob/living/L = thing + if(L.stat == DEAD) + continue + + best_candidate = thing + best_priority_index = i + + if(best_priority_index == 1) + return best_candidate + break + + return best_candidate + +/// Checks if the interaction point is available - if it has items that can be interacted with. +/datum/interaction_point/proc/is_available(transfer_type, atom/movable/target) + if(!is_valid()) + return FALSE + + // All atoms on the turf that can be interacted with. + var/list/atoms_on_the_turf = interaction_turf?.contents + + // For pickup points, we want points that have atoms to pick up + if(transfer_type == TRANSFER_TYPE_PICKUP) + + if(!length(atoms_on_the_turf)) + return FALSE // nothing to pick up + + // If the atom filters are required, we need to check if any atom on the turf fits the filters. If not, the check will only determine whether it fits the category + for(var/atom/movable/movable_atom as anything in atoms_on_the_turf) + if(check_filters_for_atom(movable_atom)) + return TRUE + + // No suitable atoms to pick up - the pickup point is unavailable. + return FALSE + + if(transfer_type == TRANSFER_TYPE_DROPOFF) + // If filters are enabled, the held item itself must match them for any overflow mode + if(!check_filters_for_atom(target) && should_use_filters) + return FALSE + + if(interaction_mode != INTERACT_DROP) // you don't check for overflow if you're not actually putting anything on the turf silly + return TRUE + + switch(overflow_status) + if(POINT_OVERFLOW_ALLOWED) + // If we don't care if there are already things on this turf, then we just check for filters + // Hence if the atom filters are skipped, the point is available for dropoff + if(!should_use_filters) + return TRUE + + if(POINT_OVERFLOW_FILTERS) + // We need to check if there are already items matching the filters on the turf + for(var/atom/movable/movable_atom as anything in atoms_on_the_turf) + if(check_filters_for_atom(movable_atom)) + return FALSE // the item on the turf was in the filters, hence the turf is considered overflowed + + if(POINT_OVERFLOW_HELD) + // We need to check if any of the items on the turf match the item we're holding + for(var/atom/movable/movable_atom as anything in atoms_on_the_turf) + if(istype(movable_atom, target?.type)) + return FALSE // one of the items on the turf was the same as the one we're holding + + if(POINT_OVERFLOW_FORBIDDEN) + // We need to check if there are already ANY items on the turf + if(locate(/obj/item) in atoms_on_the_turf) + return FALSE + + return TRUE + + // Ambiguous interaction type — no interaction is possible, point is unavailable + return FALSE + +/// Checks if the interaction point is valid — is not located on a closed turf. +/datum/interaction_point/proc/is_valid() + if(!interaction_turf) + return FALSE + + if(isclosedturf(interaction_turf)) + return FALSE + return TRUE + +/// Checks if the passed movable `atom` fits the filters. +/datum/interaction_point/proc/check_filters_for_atom(atom/movable/target) + if(!target || target.anchored || HAS_TRAIT(target, TRAIT_NODROP)) + return FALSE + + switch(filtering_mode) + if(TAKE_CLOSETS) + return iscloset(target) + + if(TAKE_HUMANS) + return ishuman(target) + + if(TAKE_ITEMS) + if(!should_use_filters) + return(isitem(target)) + + for(var/filter_path in atom_filters) + if(istype(target, filter_path)) + return TRUE + return FALSE + + return FALSE + +/// Fills the interaction endpoint priority list for the current interaction mode. +/datum/interaction_point/proc/fill_priority_list(manipulator_tier) + var/list/priorities_to_set = new /list((manipulator_tier == 4 ? 5 : 4)) + + switch(interaction_mode) + if(INTERACT_DROP) + priorities_to_set[1] = new /datum/manipulator_priority/drop/in_storage + priorities_to_set[2] = new /datum/manipulator_priority/drop/on_floor + + if(INTERACT_USE) + priorities_to_set[1] = new /datum/manipulator_priority/interact/with_living + priorities_to_set[2] = new /datum/manipulator_priority/interact/with_structure + priorities_to_set[3] = new /datum/manipulator_priority/interact/with_machinery + priorities_to_set[4] = new /datum/manipulator_priority/interact/with_items + + if(manipulator_tier == 4) + priorities_to_set[5] = new /datum/manipulator_priority/interact/with_vehicles + + return priorities_to_set + +/// Moves the priority for a given index 1 step higher. +/datum/interaction_point/proc/move_priority_up_by_index(index) + if(!index) // also handles index being 0 + return FALSE + + interaction_priorities.Swap(index, index + 1) + + return TRUE + +/// Toggles the priority's `active` param. Sets to TRUE if `reset` is TRUE. +/datum/interaction_point/proc/tick_priority_by_index(index, reset = FALSE) + var/datum/manipulator_priority/target_priority = interaction_priorities[index + 1] + + if(reset) + target_priority.active = TRUE + else + target_priority.active = !target_priority.active + + return TRUE + +/datum/interaction_point/Destroy() + interaction_turf = null + QDEL_LIST(interaction_priorities) + return ..() diff --git a/code/game/machinery/big_manipulator/interaction_priorities.dm b/code/game/machinery/big_manipulator/interaction_priorities.dm new file mode 100644 index 00000000000..0540c1f5927 --- /dev/null +++ b/code/game/machinery/big_manipulator/interaction_priorities.dm @@ -0,0 +1,37 @@ +// Prioritizes the type of atom that the manipulator interact with. Interaction lists get built on the points themselves. + +/datum/manipulator_priority + /// The name of the priority for the UI display. + var/name + /// Which typepath does this priority handle. + var/atom_typepath + /// Is this priority active? If not, it will be ignored. + var/active = TRUE + +/datum/manipulator_priority/drop/on_floor + name = "DROP ON FLOOR" + atom_typepath = /turf + +/datum/manipulator_priority/drop/in_storage + name = "DROP IN STORAGE" + atom_typepath = /obj/item/storage + +/datum/manipulator_priority/interact/with_living + name = "USE ON LIVING" + atom_typepath = /mob/living + +/datum/manipulator_priority/interact/with_structure + name = "USE ON STRUCTURE" + atom_typepath = /obj/structure + +/datum/manipulator_priority/interact/with_machinery + name = "USE ON MACHINERY" + atom_typepath = /obj/machinery + +/datum/manipulator_priority/interact/with_items + name = "USE ON ITEM" + atom_typepath = /obj/item + +/datum/manipulator_priority/interact/with_vehicles + name = "USE ON VEHICLES" + atom_typepath = /obj/vehicle diff --git a/code/game/machinery/big_manipulator/manipulator_arm.dm b/code/game/machinery/big_manipulator/manipulator_arm.dm new file mode 100644 index 00000000000..8be5b183525 --- /dev/null +++ b/code/game/machinery/big_manipulator/manipulator_arm.dm @@ -0,0 +1,50 @@ +/// Manipulator hand. Effect we animate to show that the manipulator is working and moving something. +/obj/effect/big_manipulator_arm + name = "mechanical claw" + desc = "Takes and drops objects." + icon = 'icons/obj/machines/big_manipulator_parts/big_manipulator_hand.dmi' + icon_state = "hand" + layer = LOW_ITEM_LAYER + appearance_flags = KEEP_TOGETHER | LONG_GLIDE | TILE_BOUND | PIXEL_SCALE + anchored = TRUE + greyscale_config = /datum/greyscale_config/manipulator_arm + pixel_x = -32 + pixel_y = -32 + /// We get item from big manipulator and takes its icon to create overlay. + var/datum/weakref/item_in_my_claw + /// Var to icon that used as overlay on manipulator claw to show what item it grabs. + var/mutable_appearance/icon_overlay + +/obj/effect/big_manipulator_arm/update_overlays() + . = ..() + . += update_item_overlay() + +/obj/effect/big_manipulator_arm/proc/update_item_overlay() + if(isnull(item_in_my_claw)) + return icon_overlay = null + var/atom/movable/item_data = item_in_my_claw.resolve() + icon_overlay = mutable_appearance(item_data.icon, item_data.icon_state, item_data.layer, src, item_data.plane, item_data.alpha, item_data.appearance_flags) + icon_overlay.color = item_data.color + icon_overlay.appearance = item_data.appearance + icon_overlay.pixel_w = 32 + calculate_item_offset(is_x = TRUE) + icon_overlay.pixel_z = 32 + calculate_item_offset(is_x = FALSE) + return icon_overlay + +/// Updates item that is in the claw. +/obj/effect/big_manipulator_arm/proc/update_claw(clawed_item) + item_in_my_claw = clawed_item + update_appearance() + +/// Calculate x and y coordinates so that the item icon appears in the claw and not somewhere in the corner. +/obj/effect/big_manipulator_arm/proc/calculate_item_offset(is_x = TRUE, pixels_to_offset = 32) + var/offset + switch(dir) + if(NORTH) + offset = is_x ? 0 : pixels_to_offset + if(SOUTH) + offset = is_x ? 0 : -pixels_to_offset + if(EAST) + offset = is_x ? pixels_to_offset : 0 + if(WEST) + offset = is_x ? -pixels_to_offset : 0 + return offset diff --git a/code/game/machinery/big_manipulator/tasking.dm b/code/game/machinery/big_manipulator/tasking.dm new file mode 100644 index 00000000000..90db115d1f6 --- /dev/null +++ b/code/game/machinery/big_manipulator/tasking.dm @@ -0,0 +1,109 @@ +// Handles indexing for the tasking strategy used for the manipulator's point iteraction +/datum/tasking_strategy + var/current_index = 1 + +/// Get the next available point for this tasking strategy +/datum/tasking_strategy/proc/get_next_available(list/points, atom/movable/target, transfer_type, availability_check) + return null + +// Prefers the first point avaliable +/datum/tasking_strategy/prefer_first + +/datum/tasking_strategy/prefer_first/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check) + if(!length(points)) + return null + + for(var/datum/interaction_point/point in points) + if(availability_check.Invoke(point, target, transfer_type)) + return point + + return null + +/datum/tasking_strategy/round_robin + +/datum/tasking_strategy/round_robin/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check) + if(!length(points)) + return null + + if(current_index < 1 || current_index > length(points)) + current_index = 1 + + var/starting_index = current_index + + while(TRUE) + var/datum/interaction_point/point = points[current_index] + + if(point && availability_check.Invoke(point, target, transfer_type)) + advance_index(length(points)) + return point + + advance_index(length(points)) + + if(current_index == starting_index) + break + + return null + +/// Advances the index of the last point the manipulator interacted with to properly iterate in the correct order +/datum/tasking_strategy/round_robin/proc/advance_index(list_length) + current_index++ + if(current_index > list_length) + current_index = 1 + +/datum/tasking_strategy/strict_robin + +/datum/tasking_strategy/strict_robin/get_next_available(list/points, atom/movable/target, transfer_type, datum/callback/availability_check) + if(!length(points)) + return null + + if(current_index < 1 || current_index > length(points)) + current_index = 1 + + var/datum/interaction_point/point = points[current_index] + + if(point && availability_check.Invoke(point, target, transfer_type)) + current_index++ + if(current_index > length(points)) + current_index = 1 + return point + + return null + +/// Picks a next candidate to run the checks for +/datum/tasking_strategy/proc/get_next_candidate(list/candidates) + if(!length(candidates)) + return null + return candidates[1] + +/datum/tasking_strategy/prefer_first/get_next_candidate(list/candidates) + if(!length(candidates)) + return null + return candidates[1] + +/datum/tasking_strategy/round_robin/get_next_candidate(list/candidates) + if(!length(candidates)) + return null + + if(current_index < 1 || current_index > length(candidates)) + current_index = 1 + + var/candidate = candidates[current_index] + current_index++ + if(current_index > length(candidates)) + current_index = 1 + + return candidate + +/datum/tasking_strategy/strict_robin/get_next_candidate(list/candidates) + if(!length(candidates)) + return null + + if(current_index < 1 || current_index > length(candidates)) + current_index = 1 + + var/candidate = candidates[current_index] + current_index++ + if(current_index > length(candidates)) + current_index = 1 + + return candidate diff --git a/icons/effects/interaction_points.dmi b/icons/effects/interaction_points.dmi new file mode 100644 index 00000000000..5523727247c Binary files /dev/null and b/icons/effects/interaction_points.dmi differ diff --git a/tgstation.dme b/tgstation.dme index f6fc9e6042c..1dd545fe10a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2193,7 +2193,6 @@ #include "code\game\machinery\autolathe.dm" #include "code\game\machinery\bank_machine.dm" #include "code\game\machinery\barsigns.dm" -#include "code\game\machinery\big_manipulator.dm" #include "code\game\machinery\botlaunchpad.dm" #include "code\game\machinery\buttons.dm" #include "code\game\machinery\cell_charger.dm" @@ -2263,6 +2262,13 @@ #include "code\game\machinery\wishgranter.dm" #include "code\game\machinery\ai_core\_core.dm" #include "code\game\machinery\ai_core\core_construction.dm" +#include "code\game\machinery\big_manipulator\_defines.dm" +#include "code\game\machinery\big_manipulator\big_manipulator.dm" +#include "code\game\machinery\big_manipulator\big_manipulator_interactions.dm" +#include "code\game\machinery\big_manipulator\interaction_points.dm" +#include "code\game\machinery\big_manipulator\interaction_priorities.dm" +#include "code\game\machinery\big_manipulator\manipulator_arm.dm" +#include "code\game\machinery\big_manipulator\tasking.dm" #include "code\game\machinery\camera\camera.dm" #include "code\game\machinery\camera\camera_construction.dm" #include "code\game\machinery\camera\motion.dm" diff --git a/tgui/packages/tgui/interfaces/BigManipulator.tsx b/tgui/packages/tgui/interfaces/BigManipulator.tsx deleted file mode 100644 index cdb0d7f989a..00000000000 --- a/tgui/packages/tgui/interfaces/BigManipulator.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { - Box, - Button, - Section, - Slider, - Stack, - Table, -} from 'tgui-core/components'; -import type { BooleanLike } from 'tgui-core/react'; - -import { useBackend } from '../backend'; -import { Window } from '../layouts'; - -type ManipulatorData = { - active: BooleanLike; - interaction_delay: number; - worker_interaction: string; - highest_priority: BooleanLike; - worker_combat_mode: BooleanLike; - worker_alt_mode: BooleanLike; - has_worker: BooleanLike; - interaction_mode: string; - settings_list: PrioritySettings[]; - throw_range: number; - item_as_filter: string; - selected_type: string; - delay_step: number; - min_delay: number; - max_delay: number; -}; - -type PrioritySettings = { - name: string; - priority_width: number; -}; - -const MasterControls = () => { - const { act, data } = useBackend(); - const { delay_step, interaction_delay, min_delay, max_delay } = data; - return ( - - Delay: - - {' '} - - - - } - > - - - - - -
- - act('change_mode')} - tooltip="Cycle through interaction modes" - /> - - {interaction_mode === 'throw' && ( - 1 ? 'S' : ''}`} - onClick={() => act('cycle_throw_range')} - tooltip="Cycle the distance an object will travel when thrown" - /> - )} - - act('change_take_item_type')} - tooltip="Cycle through types of items to filter" - /> - {interaction_mode === 'use' && ( - <> - act('worker_interaction_change')} - tooltip={ - worker_interaction === 'normal' - ? 'Interact using the held item' - : worker_interaction === 'single' - ? 'Drop the item after a single cycle' - : 'Interact with an empty hand' - } - /> - act('worker_combat_mode_change')} - tooltip={ - worker_combat_mode - ? 'Disable combat mode' - : 'Enable combat mode' - } - /> - act('worker_alt_mode_change')} - tooltip={ - worker_alt_mode - ? 'Disable alternate mode (right click)' - : 'Enable alternate mode (right click)' - } - /> - - )} - act('add_filter')} - tooltip="Click while holding an item to set filtering type" - /> - - {interaction_mode !== 'throw' && ( - act('highest_priority_change')} - tooltip="Only interact with the highest dropoff point in the list" - selected={!!highest_priority} - /> - )} -
-
- - {interaction_mode !== 'throw' && ( -
- - {settings_list.map((setting) => ( - - -
-
- )} - - - ); -}; diff --git a/tgui/packages/tgui/interfaces/BigManipulator/index.tsx b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx new file mode 100644 index 00000000000..a3ee67ce97a --- /dev/null +++ b/tgui/packages/tgui/interfaces/BigManipulator/index.tsx @@ -0,0 +1,791 @@ +import { useEffect, useState } from 'react'; +import { + BlockQuote, + Box, + Button, + Icon, + Input, + Modal, + Section, + Slider, + Stack, + Table, +} from 'tgui-core/components'; +import type { BooleanLike } from 'tgui-core/react'; + +import { useBackend } from '../../backend'; +import { Window } from '../../layouts'; + +import type { InteractionPoint, ManipulatorData } from './types'; + +const taskingSchedules = ['Round Robin', 'Strict Robin', 'Prefer First']; + +const taskingScheduleIcons = { + 'Round Robin': 'list-ol', + 'Strict Robin': 'arrows-spin', + 'Prefer First': 'arrow-down-1-9', +}; + +const buttonNumberToIcon = { + 1: '', + 2: 'arrow-up', + 3: '', + 4: 'arrow-left', + 5: 'arrows-to-dot', + 6: 'arrow-right', + 7: '', + 8: 'arrow-down', + 9: '', +}; + +const MasterControls = () => { + const { act, data } = useBackend(); + const { + delay_step, + speed_multiplier, + min_speed_multiplier, + max_speed_multiplier, + } = data; + return ( + + + + + + + + + ); +}; + +type ConfigRowProps = { + label: string; + content: string; + onClick: () => void; + tooltip: string; + selected?: BooleanLike; +}; + +const ConfigRow = (props: ConfigRowProps) => { + const { label, content, onClick, ...rest } = props; + const { tooltip = '', selected = false } = rest; + + return ( + + + {label} + + + + + + ); +}; + +const PointSection = (props: { + title: string; + points: InteractionPoint[]; + onAdd: () => void; + act: (action: string, params?: Record) => void; +}) => { + const { data } = useBackend(); + const { title, points, onAdd, act } = props; + const [editingPoint, setEditingPoint] = useState( + null, + ); + const [editingIndex, setEditingIndex] = useState(null); + const [editingNameId, setEditingNameId] = useState(null); + const [newName, setNewName] = useState(''); + + const isPickup = title === 'Pickup Points'; + const currentTasking = isPickup ? data.pickup_tasking : data.dropoff_tasking; + const currentIcon = taskingScheduleIcons[currentTasking] || 'clipboard-list'; + + const cycleTaskingSchedule = () => { + const currentIndex = taskingSchedules.indexOf(currentTasking); + const nextIndex = (currentIndex + 1) % taskingSchedules.length; + const newTasking = taskingSchedules[nextIndex]; + act('cycle_tasking_schedule', { + new_schedule: newTasking, + is_pickup: isPickup, + }); + }; + + const adjustPoint = (pointId: string, param: string, value?: any) => { + act('adjust_point_param', { pointId, param, value }); + }; + + const handleSaveName = (pointId: string) => { + adjustPoint(pointId, 'set_name', newName); + setEditingNameId(null); + setNewName(''); + }; + + const handleEditPoint = (point: InteractionPoint, index: number) => { + setEditingPoint(point); + setEditingIndex(index); + }; + + useEffect(() => { + if (editingPoint && editingIndex !== null) { + const currentPoints = isPickup ? data.pickup_points : data.dropoff_points; + const updatedPoint = currentPoints.find((p) => p.id === editingPoint.id); + if (updatedPoint) { + setEditingPoint(updatedPoint); + } + } + }, [ + data.pickup_points, + data.dropoff_points, + editingPoint?.id, + editingIndex, + isPickup, + ]); + + const handleDirectionClick = (buttonNumber: number) => { + if (!editingPoint || editingIndex === null) return; + + adjustPoint(editingPoint.id, 'move_to', { + buttonNumber, + is_pickup: title === 'Pickup Points', + }); + }; + + const getPointButtonNumber = (point: InteractionPoint): number | null => { + if (!point || !point.turf) return null; + + const [pointX, pointY] = point.turf.split(',').map(Number); + const [baseX, baseY] = data.manipulator_position.split(',').map(Number); + + const dx = pointX - baseX; + const dy = pointY - baseY; + + if (dx === -1 && dy === 1) return 1; + if (dx === 0 && dy === 1) return 2; + if (dx === 1 && dy === 1) return 3; + if (dx === -1 && dy === 0) return 4; + if (dx === 0 && dy === 0) return 5; + if (dx === 1 && dy === 0) return 6; + if (dx === -1 && dy === -1) return 7; + if (dx === 0 && dy === -1) return 8; + if (dx === 1 && dy === -1) return 9; + + return null; + }; + + const getFilteringModeText = (mode: number) => { + switch (mode) { + case 1: + return 'ITEMS'; + case 2: + return 'CLOSETS'; + case 3: + return 'HUMANS'; + default: + return 'UNKNOWN'; + } + }; + + const formatFilters = (filters: string[]) => { + if (!filters || filters.length === 0) return '—'; + if (filters.length <= 2) return filters.join(', '); + const shown = filters.slice(0, 2).join(', '); + const remaining = filters.length - 2; + return `${shown} and ${remaining} more...`; + }; + + return ( + <> +
+ + +
+ + {editingPoint && editingIndex !== null && ( + +
{ + setEditingPoint(null); + setEditingIndex(null); + }} + /> + } + > + + + + {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((buttonNumber) => { + const isCenter = buttonNumber === 5; + const currentButton = getPointButtonNumber(editingPoint); + const isCurrentButton = currentButton === buttonNumber; + + return ( +
+
+ + + adjustPoint(editingPoint.id, 'reset_atom_filters') + } + confirmContent="Reset?" + icon="trash" + /> + + } + > + + {editingPoint.item_filters.map((name: string, index: number) => { + return ( + + +
{name}
+
+ +
+
+ + {editingPoint.settings_list.map((setting, index) => ( + + + {index + 1} + + + + adjustPoint(editingPoint.id, 'toggle_priority', index) + } + checked={setting.active} + fluid + > + {setting.name} + + + +
+
+
+ )} + + ); +}; + +export const BigManipulator = () => { + const { data, act } = useBackend(); + const { current_task, current_task_duration, pickup_points, dropoff_points } = + data; + + const [progressValue, setProgressValue] = useState(0); + const [progressKey, setProgressKey] = useState(0); + + useEffect(() => { + const isTaskActive = current_task !== 'IDLE' && current_task !== 'NO TASK'; + + if (isTaskActive) { + setProgressValue(0); + setProgressKey((prev) => prev + 1); + + setTimeout(() => { + setProgressValue(100); + }, 10); + } else { + setProgressValue(0); + setProgressKey((prev) => prev + 1); + } + }, [current_task, current_task_duration]); + + return ( + + + +
act('run_cycle')} + > + {current_task === 'NO TASK' + ? 'Run' + : current_task === 'STOPPING' + ? 'Stopping' + : 'Stop'} + + } + > + + + +
+ +
+ + + + + Current task: + + {current_task.toUpperCase()} + + +
+ + {/*
+ + + + +
No storage detected.
+
+
+ + + + + + +
+
*/} + + act('create_pickup_point')} + act={act} + /> + + act('create_dropoff_point')} + act={act} + /> +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/BigManipulator/types.ts b/tgui/packages/tgui/interfaces/BigManipulator/types.ts new file mode 100644 index 00000000000..bf74ecd94ff --- /dev/null +++ b/tgui/packages/tgui/interfaces/BigManipulator/types.ts @@ -0,0 +1,48 @@ +import type { BooleanLike } from 'tgui-core/react'; + +export interface PrioritySettings { + name: string; + priority_width: number; + active: BooleanLike; +} + +export type InteractionPoint = { + name: string; + id: string; + turf: string; + mode: string; + filters: string[]; + item_filters: string[]; + filters_status: BooleanLike; + filtering_mode: number; + overflow_status: string; + worker_use_rmb: BooleanLike; + worker_combat_mode: BooleanLike; + settings_list: PrioritySettings[]; + throw_range: number; + worker_interaction: string; + use_post_interaction: string; +}; + +export interface ManipulatorData { + active: BooleanLike; + current_task: string; + current_task_duration: number; + + speed_multiplier: number; + min_speed_multiplier: number; + max_speed_multiplier: number; + highest_priority: BooleanLike; + interaction_mode: string; + settings_list: PrioritySettings[]; + throw_range: number; + item_as_filter: string; + selected_type: string; + delay_step: number; + + pickup_points: InteractionPoint[]; + dropoff_points: InteractionPoint[]; + manipulator_position: string; + pickup_tasking: string; + dropoff_tasking: string; +}