feat: 🅱️ig Manipulatin' Time: a Smart™ look on big manipulators (#93592)

## About The Pull Request

<img width="1006" height="758" alt="image"
src="https://github.com/user-attachments/assets/01c0dd85-d470-4fda-ba0e-72b8837eaf44"
/>

Big manipulators now operate on so-called "interaction points". You can
actually see those wearing diganostic HUDs:

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/0b158aa2-78dd-4a65-9ae9-ecab49f0c10a"
/>

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.

<img width="463" height="628" alt="image"
src="https://github.com/user-attachments/assets/b9d64bf7-0da0-41a9-bc71-4cd049f36503"
/>

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.

<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/d6de3443-c3b3-4a65-a411-24cd083a3db4"
/>

A demonstration of Round-Robin tasking:

<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/aaa80b08-4e60-48c8-9f51-aad9c3eacb33"
/>

**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.

<img width="835" height="292" alt="image"
src="https://github.com/user-attachments/assets/683b8185-92b9-4eb7-83ea-0e60d5bab93b"
/>

**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;
<img width="429" height="423" alt="image"
src="https://github.com/user-attachments/assets/3ad5c627-9d04-43cb-ba0d-d525ee43b7c7"
/>

- automate restocking machinery;

<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/950254c5-983f-44e7-9c72-4778796ed7f0"
/>

- automate ingridient prep;

<img width="1006" height="758" alt="image"
src="https://github.com/user-attachments/assets/01c0dd85-d470-4fda-ba0e-72b8837eaf44"
/>

- 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

🆑
add: Big Manipulators are now Smart™
/🆑

---------

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
This commit is contained in:
mcbalaam
2026-01-11 14:32:50 -05:00
committed by GitHub
co-authored by Ghom
parent 9150d86e3b
commit 73459c6d19
16 changed files with 2680 additions and 1296 deletions
+2
View File
@@ -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
+13 -23
View File
@@ -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
+1 -1
View File
@@ -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)
-968
View File
@@ -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
@@ -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)
@@ -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()
@@ -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()
@@ -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 ..()
@@ -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
@@ -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
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+7 -1
View File
@@ -2193,7 +2193,6 @@
#include "code\game\machinery\autolathe.dm"
#include "code\game\machinery\bank_machine.dm"
#include "code\game\machinery\barsigns.dm"
#include "code\game\machinery\big_manipulator.dm"
#include "code\game\machinery\botlaunchpad.dm"
#include "code\game\machinery\buttons.dm"
#include "code\game\machinery\cell_charger.dm"
@@ -2263,6 +2262,13 @@
#include "code\game\machinery\wishgranter.dm"
#include "code\game\machinery\ai_core\_core.dm"
#include "code\game\machinery\ai_core\core_construction.dm"
#include "code\game\machinery\big_manipulator\_defines.dm"
#include "code\game\machinery\big_manipulator\big_manipulator.dm"
#include "code\game\machinery\big_manipulator\big_manipulator_interactions.dm"
#include "code\game\machinery\big_manipulator\interaction_points.dm"
#include "code\game\machinery\big_manipulator\interaction_priorities.dm"
#include "code\game\machinery\big_manipulator\manipulator_arm.dm"
#include "code\game\machinery\big_manipulator\tasking.dm"
#include "code\game\machinery\camera\camera.dm"
#include "code\game\machinery\camera\camera_construction.dm"
#include "code\game\machinery\camera\motion.dm"
@@ -1,303 +0,0 @@
import {
Box,
Button,
Section,
Slider,
Stack,
Table,
} from 'tgui-core/components';
import type { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
import { Window } from '../layouts';
type ManipulatorData = {
active: BooleanLike;
interaction_delay: number;
worker_interaction: string;
highest_priority: BooleanLike;
worker_combat_mode: BooleanLike;
worker_alt_mode: BooleanLike;
has_worker: BooleanLike;
interaction_mode: string;
settings_list: PrioritySettings[];
throw_range: number;
item_as_filter: string;
selected_type: string;
delay_step: number;
min_delay: number;
max_delay: number;
};
type PrioritySettings = {
name: string;
priority_width: number;
};
const MasterControls = () => {
const { act, data } = useBackend<ManipulatorData>();
const { delay_step, interaction_delay, min_delay, max_delay } = data;
return (
<Stack>
<Stack.Item>Delay:</Stack.Item>
<Stack.Item>
{' '}
<Button
icon="backward-step"
onClick={() =>
act('changeDelay', {
new_delay: min_delay,
})
}
/>
</Stack.Item>
<Stack.Item grow>
<Slider
style={{ marginTop: '-5px' }}
step={delay_step}
my={1}
value={interaction_delay}
minValue={min_delay}
maxValue={max_delay}
unit="sec."
onChange={(e, value) =>
act('changeDelay', {
new_delay: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="forward-step"
onClick={() =>
act('changeDelay', {
new_delay: max_delay,
})
}
/>
</Stack.Item>
<Stack.Item>
{' '}
<Button
content="Drop"
icon="eject"
tooltip="Disengage the claws, dropping the held item"
onClick={() => act('drop')}
/>
</Stack.Item>
</Stack>
);
};
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 (
<Table.Row
className="candystripe"
style={{
height: '2em',
padding: '20px',
lineHeight: '2em',
}}
>
<Table.Cell>
<Box style={{ marginLeft: '5px' }}>{label}</Box>
</Table.Cell>
<Table.Cell
style={{
width: 'min-content',
whiteSpace: 'nowrap',
textAlign: 'right',
}}
>
<Button
content={content}
tooltip={tooltip}
onClick={onClick}
selected={!!selected}
/>
</Table.Cell>
</Table.Row>
);
};
export const BigManipulator = () => {
const { data, act } = useBackend<ManipulatorData>();
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 (
<Window title="Manipulator Interface" width={320} height={410}>
<Window.Content>
<Section
title="Action Panel"
buttons={
<>
<Button
icon="power-off"
selected={active}
onClick={() => act('on')}
>
{active ? 'On' : 'Off'}
</Button>
<Button
tooltip="Eject the monkey worker."
disabled={!has_worker}
onClick={() => act('eject_worker')}
>
{has_worker ? 'Eject monkey' : 'No monkey worker'}
</Button>
</>
}
>
<Box
style={{
lineHeight: '1.8em',
marginBottom: '-5px',
}}
>
<MasterControls />
</Box>
</Section>
<Section title="Configuration">
<Table>
<ConfigRow
label="Interaction Mode"
content={interaction_mode.toUpperCase()}
onClick={() => act('change_mode')}
tooltip="Cycle through interaction modes"
/>
{interaction_mode === 'throw' && (
<ConfigRow
label="Throwing Range"
content={`${throw_range} TILE${throw_range > 1 ? 'S' : ''}`}
onClick={() => act('cycle_throw_range')}
tooltip="Cycle the distance an object will travel when thrown"
/>
)}
<ConfigRow
label="Interaction Filter"
content={selected_type.toUpperCase()}
onClick={() => act('change_take_item_type')}
tooltip="Cycle through types of items to filter"
/>
{interaction_mode === 'use' && (
<>
<ConfigRow
label="Worker Interactions"
content={worker_interaction.toUpperCase()}
onClick={() => 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'
}
/>
<ConfigRow
label="Worker Combat Mode"
content={worker_combat_mode ? 'TRUE' : 'FALSE'}
selected={!!worker_combat_mode}
onClick={() => act('worker_combat_mode_change')}
tooltip={
worker_combat_mode
? 'Disable combat mode'
: 'Enable combat mode'
}
/>
<ConfigRow
label="Worker Alt Mode"
content={worker_alt_mode ? 'TRUE' : 'FALSE'}
selected={!!worker_alt_mode}
onClick={() => act('worker_alt_mode_change')}
tooltip={
worker_alt_mode
? 'Disable alternate mode (right click)'
: 'Enable alternate mode (right click)'
}
/>
</>
)}
<ConfigRow
label="Item Filter"
content={item_as_filter ? item_as_filter : 'NONE'}
onClick={() => act('add_filter')}
tooltip="Click while holding an item to set filtering type"
/>
{interaction_mode !== 'throw' && (
<ConfigRow
label="Override List Priority"
content={highest_priority ? 'TRUE' : 'FALSE'}
onClick={() => act('highest_priority_change')}
tooltip="Only interact with the highest dropoff point in the list"
selected={!!highest_priority}
/>
)}
</Table>
</Section>
{interaction_mode !== 'throw' && (
<Section>
<Table>
{settings_list.map((setting) => (
<Table.Row
key={setting.name}
className="candystripe"
style={{
height: '2em',
paddingLeft: '20px',
lineHeight: '2em',
}}
>
<Table.Cell
style={{
paddingLeft: '2px',
width: '2em',
}}
>
<Button
icon="arrow-up"
onClick={() =>
act('change_priority', {
priority: setting.priority_width,
})
}
/>
</Table.Cell>
<Table.Cell>{setting.name}</Table.Cell>
<Table.Cell>{setting.priority_width}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
)}
</Window.Content>
</Window>
);
};
@@ -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<ManipulatorData>();
const {
delay_step,
speed_multiplier,
min_speed_multiplier,
max_speed_multiplier,
} = data;
return (
<Stack>
<Stack.Item>
<Button
icon="backward-step"
onClick={() =>
act('adjust_interaction_speed', {
new_speed: min_speed_multiplier,
})
}
/>
</Stack.Item>
<Stack.Item grow>
<Slider
style={{ marginTop: '-5px' }}
lineHeight={1}
step={0.1}
my={1}
value={speed_multiplier}
minValue={min_speed_multiplier}
maxValue={max_speed_multiplier}
unit="x"
stepPixelSize={20}
onDrag={(value) =>
act('adjust_interaction_speed', {
new_speed: value,
})
}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="forward-step"
onClick={() =>
act('adjust_interaction_speed', {
new_speed: max_speed_multiplier,
})
}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="eject"
tooltip="Disengage the claws, dropping the held item"
onClick={() => act('drop_held_atom')}
>
Drop
</Button>
</Stack.Item>
<Stack.Item>
<Button
icon="eject"
tooltip="Unbuckle the worker"
onClick={() => act('unbuckle')}
>
Unbuckle
</Button>
</Stack.Item>
</Stack>
);
};
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 (
<Table.Row
className="candystripe"
style={{
height: '2em',
padding: '20px',
lineHeight: '2em',
}}
>
<Table.Cell>
<Box style={{ marginLeft: '5px' }}>{label}</Box>
</Table.Cell>
<Table.Cell
style={{
width: 'min-content',
whiteSpace: 'nowrap',
textAlign: 'right',
}}
>
<Button tooltip={tooltip} onClick={onClick} selected={!!selected}>
{content}
</Button>
</Table.Cell>
</Table.Row>
);
};
const PointSection = (props: {
title: string;
points: InteractionPoint[];
onAdd: () => void;
act: (action: string, params?: Record<string, any>) => void;
}) => {
const { data } = useBackend<ManipulatorData>();
const { title, points, onAdd, act } = props;
const [editingPoint, setEditingPoint] = useState<InteractionPoint | null>(
null,
);
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editingNameId, setEditingNameId] = useState<string | null>(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 (
<>
<Section
title={title}
buttons={
<>
<Button
tooltip="Cycle tasking schedule"
onClick={cycleTaskingSchedule}
icon={currentIcon}
color="transparent"
>
{currentTasking}
</Button>
<Button
icon="arrows-spin"
color="transparent"
onClick={() => act(`reset_tasking_${title}`)}
>
Reset
</Button>
<Button icon="plus" color="transparent" onClick={onAdd} />
</>
}
>
<Stack vertical>
{points.map((point, index) => (
<Stack.Item
key={index}
style={{
padding: '5px',
}}
className="candystripe"
>
<Box>
<Stack>
<Stack.Item grow>
<Box>
<Box bold>
{editingNameId === point.id ? (
<Stack>
<Stack.Item>
<Input
value={newName}
onChange={(value) => setNewName(value)}
onEnter={() => handleSaveName(point.id)}
maxLength={20}
autoFocus
/>
</Stack.Item>
<Stack.Item>
<Button
icon="check"
onClick={() => handleSaveName(point.id)}
/>
</Stack.Item>
</Stack>
) : (
<>
{point.name}{' '}
<Button
icon="edit"
color="transparent"
onClick={() => {
setEditingNameId(point.id);
setNewName(point.name);
}}
/>
</>
)}
</Box>
<Box color="label">Mode: {point.mode.toUpperCase()}</Box>
<Box
color="label"
style={{
maxWidth: '280px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: 'block',
}}
>
Filters: {formatFilters(point.item_filters)}
</Box>
</Box>
</Stack.Item>
<Stack vertical>
<Stack.Item>
<Button
icon="gear"
color="transparent"
onClick={() => handleEditPoint(point, index)}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="trash"
color="transparent"
onClick={() =>
adjustPoint(
point.id,
'remove_point',
isPickup && 'SOMETHING',
)
}
/>
</Stack.Item>
</Stack>
</Stack>
</Box>
</Stack.Item>
))}
</Stack>
</Section>
{editingPoint && editingIndex !== null && (
<Modal
style={{
padding: '6px',
width: '340px',
boxSizing: 'initial',
}}
>
<Section
title="Point Properties"
buttons={
<Button
icon="xmark"
color="bad"
onClick={() => {
setEditingPoint(null);
setEditingIndex(null);
}}
/>
}
>
<Stack>
<Stack.Item>
<Box
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: '1fr 1fr 1fr',
height: '60px',
width: '60px',
gap: '2px',
rowGap: '2px',
marginRight: '10px',
}}
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((buttonNumber) => {
const isCenter = buttonNumber === 5;
const currentButton = getPointButtonNumber(editingPoint);
const isCurrentButton = currentButton === buttonNumber;
return (
<Button
key={buttonNumber}
disabled={isCenter}
color={isCurrentButton ? 'good' : 'default'}
onClick={() =>
!isCenter && handleDirectionClick(buttonNumber)
}
style={{
margin: '0px',
textAlign: 'center',
padding: '0px',
}}
icon={buttonNumberToIcon[buttonNumber]}
/>
);
})}
</Box>
</Stack.Item>
<Stack.Item grow>
<Table>
<ConfigRow
label="Object Type"
content={getFilteringModeText(editingPoint.filtering_mode)}
onClick={() =>
adjustPoint(editingPoint.id, 'cycle_pickup_point_type')
}
tooltip="Cycle the pickup type"
/>
{title === 'Pickup Points' ? (
<ConfigRow
label="Use Item Filters"
content={editingPoint.filters_status ? 'TRUE' : 'FALSE'}
onClick={() =>
adjustPoint(editingPoint.id, 'toggle_filter_skip')
}
tooltip="Toggle filter usage"
/>
) : (
<>
<ConfigRow
label="Mode"
content={editingPoint.mode.toUpperCase()}
onClick={() =>
adjustPoint(
editingPoint.id,
'cycle_dropoff_point_interaction',
)
}
tooltip="Change dropoff mode"
/>
<ConfigRow
label="Overflow"
content={editingPoint.overflow_status}
onClick={() =>
adjustPoint(editingPoint.id, 'cycle_overflow_status')
}
tooltip="Cycle overflow status"
/>
<ConfigRow
label="Use Item Filters"
content={editingPoint.filters_status ? 'TRUE' : 'FALSE'}
onClick={() =>
adjustPoint(editingPoint.id, 'toggle_filter_skip')
}
tooltip="Toggle filter usage"
/>
{editingPoint.mode.toUpperCase() === 'THROW' && (
<ConfigRow
label="Throw Range"
content={`${editingPoint.throw_range} TILES`}
onClick={() =>
adjustPoint(editingPoint.id, 'cycle_throw_range')
}
tooltip="Cycle throwing range"
/>
)}
{editingPoint.mode.toUpperCase() === 'USE' && (
<>
<ConfigRow
label="Alt Worker Action"
content={
editingPoint.worker_use_rmb ? 'TRUE' : 'FALSE'
}
onClick={() =>
adjustPoint(editingPoint.id, 'toggle_worker_rmb')
}
tooltip="Toggle RMB-like attack"
/>
<ConfigRow
label="Combat Stance"
content={
editingPoint.worker_combat_mode ? 'TRUE' : 'FALSE'
}
onClick={() =>
adjustPoint(
editingPoint.id,
'toggle_worker_combat',
)
}
tooltip="Toggle using Combat Mode for interactions"
/>
<ConfigRow
label="Alt Worker Action"
content={editingPoint.worker_interaction}
onClick={() =>
adjustPoint(
editingPoint.id,
'cycle_worker_interaction',
)
}
tooltip="Cycle interaction type"
/>
<ConfigRow
label="No Uses Left"
content={editingPoint.use_post_interaction}
onClick={() =>
adjustPoint(
editingPoint.id,
'cycle_post_interaction',
)
}
tooltip="Cycle what to do when no interaction is avaliable"
/>
</>
)}
</>
)}
</Table>
</Stack.Item>
</Stack>
</Section>
<Section
title="Item Filters"
buttons={
<>
<Button
icon="plus"
onClick={() =>
adjustPoint(editingPoint.id, 'add_atom_filter_from_held')
}
>
Add held
</Button>
<Button.Confirm
onClick={() =>
adjustPoint(editingPoint.id, 'reset_atom_filters')
}
confirmContent="Reset?"
icon="trash"
/>
</>
}
>
<Stack vertical>
{editingPoint.item_filters.map((name: string, index: number) => {
return (
<Stack key={index}>
<Stack.Item grow>
<BlockQuote>{name}</BlockQuote>
</Stack.Item>
<Stack.Item>
<Button
color="transparent"
icon="xmark"
onClick={() =>
adjustPoint(editingPoint.id, 'delete_filter', index)
}
/>
</Stack.Item>
</Stack>
);
})}
</Stack>
</Section>
<Section title="Interaction Priorities">
<Table>
{editingPoint.settings_list.map((setting, index) => (
<Table.Row className="candystripe" key={setting.name}>
<Table.Cell style={{ padding: '4px 4px' }}>
<Icon name="hashtag" /> {index + 1}
</Table.Cell>
<Table.Cell>
<Button.Checkbox
onClick={() =>
adjustPoint(editingPoint.id, 'toggle_priority', index)
}
checked={setting.active}
fluid
>
{setting.name}
</Button.Checkbox>
</Table.Cell>
<Table.Cell width="1em">
<Button
icon="arrow-up"
disabled={index === 0}
onClick={() =>
index > 0 &&
adjustPoint(editingPoint.id, 'priority_move_up', index)
}
/>
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
</Modal>
)}
</>
);
};
export const BigManipulator = () => {
const { data, act } = useBackend<ManipulatorData>();
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 (
<Window title="Manipulator Interface" width={420} height={610}>
<Window.Content overflowY="auto">
<Box
style={{
height: '100%',
overflowY: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
<Section
title="Action Panel"
buttons={
<Button
icon={
current_task === 'NO TASK'
? 'play'
: current_task === 'STOPPING'
? 'hourglass-start'
: 'stop'
}
color={
current_task === 'NO TASK'
? 'good'
: current_task === 'STOPPING'
? 'blue'
: 'bad'
}
onClick={() => act('run_cycle')}
>
{current_task === 'NO TASK'
? 'Run'
: current_task === 'STOPPING'
? 'Stopping'
: 'Stop'}
</Button>
}
>
<Box
style={{
lineHeight: '1.8em',
marginBottom: '-5px',
}}
>
<MasterControls />
</Box>
</Section>
<Section>
<Box
key={progressKey}
style={{
position: 'relative',
height: '1.8em',
border: '1px solid #40668c',
overflow: 'hidden',
borderRadius: '2px',
}}
>
<Box
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: `${progressValue}%`,
backgroundColor: '#40668c',
transition: `width ${current_task_duration / 10}s linear`,
zIndex: 1,
}}
/>
<Box
style={{
position: 'relative',
zIndex: 2,
height: '100%',
display: 'flex',
alignItems: 'center',
padding: '0 8px',
color: '#fff',
fontSize: '12px',
}}
>
<Box style={{ marginRight: '8px', marginLeft: '-2px' }}>
Current task:
</Box>
<Box style={{ flexGrow: 1 }}>{current_task.toUpperCase()}</Box>
</Box>
</Box>
</Section>
{/* <Section>
<Stack>
<Stack.Item lineHeight="1.8" grow>
<Box
style={{
padding: '2px',
backgroundColor: '#444444',
}}
>
<Button fluid icon="eject">
data disk
</Button>
<BlockQuote>No storage detected.</BlockQuote>
</Box>
</Stack.Item>
<Stack.Item style={{ alignContent: 'center' }}>
<Button lineHeight="2" icon="floppy-disk">
Read
</Button>
</Stack.Item>
<Stack.Item style={{ alignContent: 'center' }}>
<Button lineHeight="2" icon="circle">
Write
</Button>
</Stack.Item>
</Stack>
</Section> */}
<PointSection
title="Pickup Points"
points={pickup_points}
onAdd={() => act('create_pickup_point')}
act={act}
/>
<PointSection
title="Dropoff Points"
points={dropoff_points}
onAdd={() => act('create_dropoff_point')}
act={act}
/>
</Box>
</Window.Content>
</Window>
);
};
@@ -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;
}