From 73459c6d1908967f195b3d79c19d2caf168393d1 Mon Sep 17 00:00:00 2001
From: mcbalaam <104003807+mcbalaam@users.noreply.github.com>
Date: Mon, 12 Jan 2026 02:32:50 +0700
Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=85=B1=EF=B8=8Fig=20Manipulatin'?=
=?UTF-8?q?=20Time:=20a=20Smart=E2=84=A2=20look=20on=20big=20manipulators?=
=?UTF-8?q?=20(#93592)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## About The Pull Request
Big manipulators now operate on so-called "interaction points". You can
actually see those wearing diganostic HUDs:
There are two kinds of points — pickup and dropoff. Pickup points
represent turfs where the manipulator will pick something up from and
dropoff points — where it will deliver the thing or interact with.
Points can occupy any of the 8 turfs surrounding the manipulator and can
be stacked on the same tile.
Contrary to the old system, *each* point now has unique properties —
unique filters, interaction methods and much more, which means more ways
to interact with them!
## Interaction Filters: Overflow
Each point has two types of filters: object type — item/closet/living
and an **item filter**. Pickup filters dictate which items can be picked
up from this point, while dropoff filters — which items can be delivered
to this point.
Overflow is a dropoff point mechanic which allows you to mark as "busy"
points that either 1) already have the item you're holding, 2) have any
items that match the filters or 3) have ANY items. If set up correctly,
the manipulator will not put more than 1 item on the same point.
## Tasking System
A tasking system has been implemented to keep the interaction order,
with a total of 3 presets: `Round-Robin`, `Strict-Robin` and `Prefer
First`
### Round-Robin
Iterates through the points in ascending order. If a point cannot be
interacted with (say, due to overflow), it will not hesitate to skip the
point.
A demonstration of Round-Robin tasking:
**Proposed usage**: flow normalisation. When one of the threads
(machines/conveyor belts) is working faster than the others, that thread
will receive an additional load.
### Strict-Robin
Iterates through the points in ascending order. If a point cannot be
interacted with, it will wait for as long as it's needed untill the
point is avaliable and will then continue.
**Proposed usage**: flow equalisation. Even if all threads are working
at different speeds, the load will be equally distributed, even if that
causes downtime on the faster threads.
### Prefer First
As shrimple as that — selects the first point in list that's avaliable.
## Post-Interaction Shenanigans
After the interaction (the `USE` mode involving a monkey) has been
complete and there's nothing else to interact with
if the used item wasn't wasted, you can either 1) drop it on the point
we were just interacting with, 2) drop it under the machine, 3) wait for
something to interact with (will lock out any other interactions) or 4)
find another fitting point to put the tool in.
## Why It's Good For The Game
Now you can...
- sort any items/materials — fish, ores, trash;
- automate restocking machinery;
- automate ingridient prep;
- give stuff out (botany/bar/med/sci);
- automate harvesting ang processing (juicing/grinding/storing in the
smartfridge) of crops;
- automatically wrap and scan goods for cargo shipping;
- automate crafring stuff you have to assemble by hand (maxcaps? hell,
why not);
- set up shops/exchangers that only accept specific things;
...and whatever else comes to your mind.
## Changelog
:cl:
add: Big Manipulators are now Smart™
/:cl:
---------
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
---
code/__DEFINES/atom_hud.dm | 2 +
code/datums/wires/big_manipulator.dm | 36 +-
code/game/data_huds.dm | 2 +-
code/game/machinery/big_manipulator.dm | 968 ------------------
.../machinery/big_manipulator/_defines.dm | 67 ++
.../big_manipulator/big_manipulator.dm | 854 +++++++++++++++
.../big_manipulator_interactions.dm | 462 +++++++++
.../big_manipulator/interaction_points.dm | 239 +++++
.../big_manipulator/interaction_priorities.dm | 37 +
.../big_manipulator/manipulator_arm.dm | 50 +
.../game/machinery/big_manipulator/tasking.dm | 109 ++
icons/effects/interaction_points.dmi | Bin 0 -> 2199 bytes
tgstation.dme | 8 +-
.../tgui/interfaces/BigManipulator.tsx | 303 ------
.../tgui/interfaces/BigManipulator/index.tsx | 791 ++++++++++++++
.../tgui/interfaces/BigManipulator/types.ts | 48 +
16 files changed, 2680 insertions(+), 1296 deletions(-)
delete mode 100644 code/game/machinery/big_manipulator.dm
create mode 100644 code/game/machinery/big_manipulator/_defines.dm
create mode 100644 code/game/machinery/big_manipulator/big_manipulator.dm
create mode 100644 code/game/machinery/big_manipulator/big_manipulator_interactions.dm
create mode 100644 code/game/machinery/big_manipulator/interaction_points.dm
create mode 100644 code/game/machinery/big_manipulator/interaction_priorities.dm
create mode 100644 code/game/machinery/big_manipulator/manipulator_arm.dm
create mode 100644 code/game/machinery/big_manipulator/tasking.dm
create mode 100644 icons/effects/interaction_points.dmi
delete mode 100644 tgui/packages/tgui/interfaces/BigManipulator.tsx
create mode 100644 tgui/packages/tgui/interfaces/BigManipulator/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/BigManipulator/types.ts
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 0000000000000000000000000000000000000000..5523727247c7d1ea3424c8bf29bc29472d28bf45
GIT binary patch
literal 2199
zcmbtWX*3jU8=gTkWDQ|#GZR@_USvI%2}zhlrXo{@kTAB_p4ki%iHd_nMwThwY=uNd
zvV4||eGMaxUB=*T8isG)_uu#X`|;fOxzBl?bDeWt&vl(U#reu5@uP}I0RVuwy`7B<
zzf=A>!XSQqME5((ZLDN1x6<^7gEv6p3N-Yi2
z9U97y*#860)*5yb2`RX~+IlK2aSJ}9VB51@k-yZQ*vuNom5JJJNn#zm&_|nGKAcJl
z)QVV)X)%i#S#NcGC#d;V;+BC~=)n5H#?4Hx)$<`dkXv{6h1&SAF8~0z#NNj0TFlf&
z{>`XwCQ?G(`rWRzks7_Owf!S%^~v||N$+I~9oyC4K4T;xqHbL6yk;%c=7uEf$HeVz
zV}ERKFX0~u@ho;FgDg1<+j_A5Ein#fR}vSnUQD-arAZRM0yilOkkI|O)AOv&FVTvu
zt-TF8_h-mNqzEE%g!sU$v*e?eNxFjooXS@=pZ=wC977_%{#!Pu@W3BZ0F+ZBpyYEQn=2qn_e
zf6pU2v1)gvuH+n8LUH>GA*kPa>r1fIon(&MvZNv_*7-9amqsrD@E_r(H~-XV?V4?@
z%%qeaND*au6|oHc7LTR|*QMdoAw2+`gH)a(K=OGp;KPXK|PPcp1+<{02HS4d^~Yww{Vxc7p6paVuuYcokubL%gx<
z7VOqG-2A~D8;*
zd>Es$FFEXSsr~ZZ73o?|t&=>v
zlhc1#tpnEsnb(I=Ppm8+x7(GL`|kJY{~@KzkYcKJ#UM891i6*e-VDm;$!TR^dT9-V
z8A)7HYJaa2pcEMOw`vbUwpx$z5M)wYHIRJSNOZ7Rv2{H;6>W9C@H=*ET!8`2w&HE%
zlA?QSit06%GSe6@0O75DR&!u(%#m7^g#gKxInrLc<8z5R@d+chJz_(AUBOHXnsM;L
z8_kjtGl{ic?)8~Cu_Km!ZY7ib4#m!4&T4Osy)*F@uy&Q?Y=I0kBeET>sPXR{$$|x75{OVvB&kiGKE;K|J;BtH?8-tk2Ig{
zJg?Z0m2L0(rz5OAMuzFVFDtS#i0(>sS(q(AA*M+S)DVsoedk`bo8AT3CVFhA)eY}nK5BS2Z4KuUB
zP0sj_Uc1TV-gRSzUmBJ%7)EccdNhM`sHftV-8({6@}TkklmdK=_)CXn;nI`PsNwm9
z?b*S-&KwMODXjiRN|3iZGgf5^#E-tY4;koiGNSxW81bWIaYMh6*T$BR&l4VvY4#CC
zu8vdn_jDJKc9lS#8|nqV3LKaJO&0#V;>LHlw97p5cgYXF%_p;jFg<=~4Q?rSGB=%{
zIS%0zehz%MS)`a93Mk5ZBI;i4_9vlGe>
zN0LLYKV83u(1FVnMQS2H*j9_Rox6NouvV(i`h6ei-~Z<-n5Uu%A^8ZU03G4=FqPDZ
zzcfJV@T|mZjlc%j$(LEUH^AEkcTrL=p5-a(%JOAKmyM9>?LED@x42A@0hm0boVEBw
l!C6ICGq2g?n3ZfNv9f&ZA}*}Io4;uQ>@QxiskHVa{} {
- const { act, data } = useBackend();
- const { delay_step, interaction_delay, min_delay, max_delay } = data;
- return (
-
- Delay:
-
- {' '}
-
-
-
- act('changeDelay', {
- new_delay: value,
- })
- }
- />
-
-
-
-
- {' '}
-
-
- );
-};
-
-type ConfigRowProps = {
- label: string;
- content: string;
- onClick: () => void;
- tooltip: string;
- selected?: BooleanLike;
-};
-
-const ConfigRow = (props: ConfigRowProps) => {
- const { label, content, onClick, tooltip, selected = false } = props;
-
- return (
-
-
- {label}
-
-
-
-
-
- );
-};
-
-export const BigManipulator = () => {
- const { data, act } = useBackend();
- const {
- active,
- interaction_mode,
- settings_list,
- has_worker,
- worker_interaction,
- worker_combat_mode,
- worker_alt_mode,
- highest_priority,
- throw_range,
- item_as_filter,
- selected_type,
- } = data;
-
- return (
-
-
-
-
-
- >
- }
- >
-
-
-
-
-
-
-
- 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) => (
-
-
-
- {setting.name}
- {setting.priority_width}
-
- ))}
-
-
- )}
-
-
- );
-};
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 (
+
+
+
+
+
+ act('adjust_interaction_speed', {
+ new_speed: value,
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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, 'cycle_pickup_point_type')
+ }
+ tooltip="Cycle the pickup type"
+ />
+ {title === 'Pickup Points' ? (
+
+ adjustPoint(editingPoint.id, 'toggle_filter_skip')
+ }
+ tooltip="Toggle filter usage"
+ />
+ ) : (
+ <>
+
+ adjustPoint(
+ editingPoint.id,
+ 'cycle_dropoff_point_interaction',
+ )
+ }
+ tooltip="Change dropoff mode"
+ />
+
+ adjustPoint(editingPoint.id, 'cycle_overflow_status')
+ }
+ tooltip="Cycle overflow status"
+ />
+
+ adjustPoint(editingPoint.id, 'toggle_filter_skip')
+ }
+ tooltip="Toggle filter usage"
+ />
+ {editingPoint.mode.toUpperCase() === 'THROW' && (
+
+ adjustPoint(editingPoint.id, 'cycle_throw_range')
+ }
+ tooltip="Cycle throwing range"
+ />
+ )}
+ {editingPoint.mode.toUpperCase() === 'USE' && (
+ <>
+
+ adjustPoint(editingPoint.id, 'toggle_worker_rmb')
+ }
+ tooltip="Toggle RMB-like attack"
+ />
+
+ adjustPoint(
+ editingPoint.id,
+ 'toggle_worker_combat',
+ )
+ }
+ tooltip="Toggle using Combat Mode for interactions"
+ />
+
+ adjustPoint(
+ editingPoint.id,
+ 'cycle_worker_interaction',
+ )
+ }
+ tooltip="Cycle interaction type"
+ />
+
+ adjustPoint(
+ editingPoint.id,
+ 'cycle_post_interaction',
+ )
+ }
+ tooltip="Cycle what to do when no interaction is avaliable"
+ />
+ >
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+ 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;
+}