diff --git a/code/datums/pathfinding_mover.dm b/code/datums/pathfinding_mover.dm new file mode 100644 index 00000000000..fe168adc2fc --- /dev/null +++ b/code/datums/pathfinding_mover.dm @@ -0,0 +1,128 @@ +/** + * A generalized datum for pathfinding, and moving to a target. + */ + +/datum/pathfinding_mover + /// Can be a simplemob bot, a drone, or even a pathfinding modsuit module (currently only implemented for drones) + VAR_PRIVATE/atom/movable/owner + /// The target turf we are after + VAR_PRIVATE/turf/target + + /// List of turfs through which a mod 'steps' to reach the waypoint + VAR_PRIVATE/list/path = list() + /// max amount of tries before resetting path to null + var/max_tries = 10 + /// How many times have we tried to move? + VAR_PRIVATE/tries = 0 + /// How many 2-tick delays per move (5 = 1 second) + var/move_speed = 2 + /// A counter for move_speed via modulo + VAR_PRIVATE/move_ticks = 0 + + /// Callback invoked on failure + var/datum/callback/on_set_path_null + /// Callback invoked on success + var/datum/callback/on_success + + /// The delay called as part of Move() + VAR_PRIVATE/atom/movable/owner_move_delay = 0 + /// Do we consider movement delay? Disable for non-mobs + var/consider_movement_delay = TRUE + /// Requires `consider_movement_delay = TRUE`, saves the last movement delay to prevent diagonal weirdness + VAR_PRIVATE/last_movement_delay + + +/datum/pathfinding_mover/New(_owner, _target) + target = _target + + owner = _owner + if(ismob(owner)) + var/mob/M = owner + owner_move_delay = M.movement_delay() + RegisterSignal(owner, COMSIG_PARENT_QDELETING, PROC_REF(signal_qdel)) + +/datum/pathfinding_mover/Destroy(force, ...) + UnregisterSignal(owner, COMSIG_PARENT_QDELETING) + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/pathfinding_mover/proc/generate_path(...) + set_path(get_path_to(arglist(list(owner, target) + args))) + return (length(path) > 0) + +/datum/pathfinding_mover/proc/set_target(atom/new_target) + if(!isatom(new_target)) + target = null + return + target = get_turf(new_target) + +/datum/pathfinding_mover/proc/set_path(list/newpath) + PRIVATE_PROC(TRUE) + + if(newpath == null) + on_set_path_null?.Invoke() // This is seperate to prevent invoking the callback if calling from generate_path + path = newpath ? newpath : list() + if(!length(path)) // Because newpath could be an empty list + STOP_PROCESSING(SSfastprocess, src) + tries = 0 + +/** + * Start moving towards our target, returns false if the path does not lead to the target + */ +/datum/pathfinding_mover/proc/start() + if(!target || !length(path)) // Pathfinding failed or a path/destination was not set. + set_path(null) + return FALSE + + var/turf/last_node = get_turf(path[length(path)]) // This is the turf at the end of the path + if(target != last_node) // The path should lead us to our given destination. If this is not true, we must stop. + set_path(null) + return FALSE + + START_PROCESSING(SSfastprocess, src) + return TRUE + +/** + * Using fast process, see if we should take the next step yet + */ +/datum/pathfinding_mover/process(wait) // 2 on fast process + move_ticks++ + if(move_speed && (move_ticks < move_speed)) + return + if(consider_movement_delay && (last_movement_delay > (move_ticks * wait))) + return + move_ticks = 0 + + if(tries >= max_tries) + set_path(null) + return // PROCESS_KILL called with set_path(null) + + if(get_turf(owner) == target) // We have arrived, no need to move again. + on_success?.Invoke(src) + return PROCESS_KILL + + generalized_step() + +/** + * Take our next step in our pathfinding algorithm + */ +/datum/pathfinding_mover/proc/generalized_step() // Step, increase tries if failed + PRIVATE_PROC(TRUE) + if(!length(path)) + return + + var/targetted_direction = get_dir(owner, path[1]) + + var/delay = owner_move_delay + if(IS_DIR_DIAGONAL(targetted_direction)) + delay *= SQRT_2 + + if(!owner.Move(path[1], targetted_direction, delay)) + tries++ + return + tries = 0 + if(consider_movement_delay) + last_movement_delay = delay + + // Increment the path + path.Cut(1, 2) diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 6b75b8b1a4f..7c079abd904 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1366,8 +1366,12 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays) return 1 /obj/machinery/door/airlock/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id = FALSE) -//Airlock is passable if it is open (!density), bot has access, and is not bolted or welded shut) - return !density || (check_access(ID) && !locked && !welded && arePowerSystemsOn() && !no_id) + if(!density) + return TRUE + if(caller?.checkpass(PASSDOOR) && !locked) + return TRUE + //Airlock is passable if it is open (!density), bot has access, and is not bolted or welded shut) + return check_access(ID) && !locked && !welded && arePowerSystemsOn() && !no_id /obj/machinery/door/airlock/emag_act(mob/user) if(!operating && density && arePowerSystemsOn() && !emagged) diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index fae088cfd08..2ff095788cb 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -502,3 +502,12 @@ /obj/machinery/door/zap_act(power, zap_flags) zap_flags &= ~ZAP_OBJ_DAMAGE . = ..() + +/obj/machinery/door/CanPathfindPass(obj/item/card/id/ID, to_dir, atom/movable/caller, no_id) + if(QDELETED(caller)) + return ..() + if(caller.checkpass(PASSDOOR) && !locked) + return TRUE + if(caller.checkpass(PASSGLASS)) + return !opacity + return ..() diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index 08ad6fd86b4..126142a7e6f 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -78,7 +78,7 @@ /obj/structure/plasticflaps/CanPathfindPass(obj/item/card/id/ID, to_dir, caller, no_id = FALSE) if(isliving(caller)) - if(isbot(caller)) + if(isbot(caller) || isdrone(caller)) return TRUE var/mob/living/M = caller diff --git a/code/game/verbs/suicide.dm b/code/game/verbs/suicide.dm index 21b37d58a74..69252965b5b 100644 --- a/code/game/verbs/suicide.dm +++ b/code/game/verbs/suicide.dm @@ -61,9 +61,6 @@ //put em at -175 adjustOxyLoss(max(maxHealth * 2 - getToxLoss() - getFireLoss() - getBruteLoss() - getOxyLoss(), 0)) -/mob/living/silicon/robot/drone/do_suicide() - shut_down() - /mob/living/silicon/pai/do_suicide() if(mobility_flags & MOBILITY_MOVE) close_up() diff --git a/code/modules/mob/living/silicon/robot/drone/drone_console.dm b/code/modules/mob/living/silicon/robot/drone/drone_console.dm index 3246df91c81..8912fedb0ae 100644 --- a/code/modules/mob/living/silicon/robot/drone/drone_console.dm +++ b/code/modules/mob/living/silicon/robot/drone/drone_console.dm @@ -66,7 +66,8 @@ health = round(D.health / D.maxHealth, 0.1), charge = round(D.cell.charge / D.cell.maxcharge, 0.1), location = "[A] ([T.x], [T.y])", - sync_cd = D.sync_cooldown > world.time ? TRUE : FALSE + sync_cd = D.sync_cooldown > world.time ? TRUE : FALSE, + pathfinding = D.pathfinding ) data["drones"] += list(drone_data) return data @@ -113,13 +114,14 @@ to_chat(usr, "You issue a law synchronization directive for the drone.") D.law_resync() - if("shutdown") + if("recall") var/mob/living/silicon/robot/drone/D = locateUID(params["uid"]) if(D) - to_chat(usr, "You issue a kill command for the unfortunate drone.") + to_chat(usr, "You issue a recall command for the unfortunate drone.") if(D != usr) // Don't need to bug admins about a suicide - message_admins("[key_name_admin(usr)] issued kill order for drone [key_name_admin(D)] from control console.") - log_game("[key_name(usr)] issued kill order for [key_name(D)] from control console.") + message_admins("[key_name_admin(usr)] issued recall order for drone [key_name_admin(D)] from control console.") + log_game("[key_name(usr)] issued recall order for [key_name(D)] from control console.") + // Yes, I know this proc is called shut_down, I've kept the name the same because emagged drones use it for death D.shut_down() /obj/machinery/computer/drone_control/proc/find_fab(mob/user) diff --git a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm index 412423c9286..e4d87181b8b 100644 --- a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm +++ b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm @@ -87,6 +87,16 @@ var/mob/dead/observer/ghost = user ghost.join_as_drone() +/obj/machinery/drone_fabricator/attack_hand(mob/user) + . = ..() + if(isdrone(user) && Adjacent(user)) + if(alert(user, "Would you like to shut down?", null, "Yes", "No") != "Yes") + return + var/mob/living/silicon/robot/drone/D = user + if(!istype(D) || QDELETED(D)) + return + D.cryo_with_dronefab(src) + /mob/dead/verb/join_as_drone() set category = "Ghost" set name = "Join As Drone" diff --git a/code/modules/mob/living/silicon/robot/drone/maint_drone.dm b/code/modules/mob/living/silicon/robot/drone/maint_drone.dm index d02c563d5d5..55bb0f08d2c 100644 --- a/code/modules/mob/living/silicon/robot/drone/maint_drone.dm +++ b/code/modules/mob/living/silicon/robot/drone/maint_drone.dm @@ -45,7 +45,7 @@ ) holder_type = /obj/item/holder/drone -// var/sprite[0] + var/datum/pathfinding_mover/pathfinding /mob/living/silicon/robot/drone/New() @@ -125,6 +125,8 @@ overlays.Cut() if(stat == CONSCIOUS) overlays += "eyes-[icon_state]" + if(pathfinding) + overlays += "eyes-repairbot-pathfinding" else overlays -= "eyes" @@ -279,6 +281,10 @@ to_chat(src, "You feel a system kill order percolate through your tiny brain, but it doesn't seem like a good idea to you.") return + if(!emagged && pathfind_to_dronefab()) + to_chat(src, "You feel a system recall order percolate through your tiny brain, and you return to your drone fabricator.") + return + to_chat(src, "You feel a system kill order percolate through your tiny brain, and you obediently destroy yourself.") death() @@ -391,3 +397,73 @@ qdel(src) return TRUE return ..() + +/mob/living/silicon/robot/drone/do_suicide() + ghostize(TRUE) + shut_down() + +/mob/living/silicon/robot/drone/proc/pathfind_to_dronefab() + if(pathfinding) + return TRUE + + if(istype(get_turf(src), /turf/space)) + return FALSE // Pretty damn hard to path through space + + var/turf/target + for(var/obj/machinery/drone_fabricator/DF in GLOB.machines) + if(DF.z != z) + continue + target = get_turf(DF) + target = get_step(target, EAST) + break + + if(!target) + return FALSE + + // Mimic having the hide-ability activated + layer = TURF_LAYER + 0.2 + pass_flags |= PASSDOOR + + var/datum/pathfinding_mover/pathfind = new(src, target) + + // I originally only wanted to make it use an ID if it couldnt pathfind otherwise, but that means it could take multiple minutes if both searches failed + var/obj/item/card/id/temp_id = new(src) + temp_id.access = get_all_accesses() + set_pathfinding(pathfind) + var/found_path = pathfind.generate_path(150, null, temp_id) + qdel(temp_id) + if(!found_path) + set_pathfinding(null) + return FALSE + + pathfind.on_set_path_null = CALLBACK(src, PROC_REF(pathfind_failed_cleanup)) + pathfind.on_success = CALLBACK(src, PROC_REF(at_dronefab)) + pathfind.start() + return TRUE + +/mob/living/silicon/robot/drone/proc/pathfind_failed_cleanup(pathfind) + set_pathfinding(null) + death() + +/mob/living/silicon/robot/drone/proc/at_dronefab(pathfind) + set_pathfinding(null) + cryo_with_dronefab() + +/mob/living/silicon/robot/drone/proc/cryo_with_dronefab(obj/machinery/drone_fabricator/drone_fab) + if(!drone_fab) + drone_fab = locate() in range(1, src) + if(!drone_fab) + return FALSE + drone_fab.drone_progress = 100 // recycling! + + visible_message("[src] shuts down and enters [drone_fab].") + playsound(loc, 'sound/machines/twobeep.ogg', 50) + qdel(src) + return TRUE + +/mob/living/silicon/robot/drone/proc/set_pathfinding(datum/pathfinding_mover/new_pathfind) + if(isnull(new_pathfind) && istype(pathfinding)) + qdel(pathfinding) + pathfinding = new_pathfind + notransform = istype(new_pathfind) ? TRUE : FALSE // prevent them from moving themselves while pathfinding. + update_icons() diff --git a/code/modules/ruins/ghost_bar.dm b/code/modules/ruins/ghost_bar.dm index b992fcb9ae4..cf016e0f99b 100644 --- a/code/modules/ruins/ghost_bar.dm +++ b/code/modules/ruins/ghost_bar.dm @@ -106,5 +106,9 @@ qdel(mob_to_delete) /proc/dust_if_respawnable(mob/M) + if(isdrone(M)) + var/mob/living/silicon/robot/drone/drone = M + drone.shut_down(TRUE) + return if(HAS_TRAIT_FROM(M, TRAIT_RESPAWNABLE, GHOST_ROLE)) M.dust() diff --git a/icons/mob/robots.dmi b/icons/mob/robots.dmi index d4afcd099bc..f890b05a742 100644 Binary files a/icons/mob/robots.dmi and b/icons/mob/robots.dmi differ diff --git a/paradise.dme b/paradise.dme index 801abdef0b6..bbc68b46f20 100644 --- a/paradise.dme +++ b/paradise.dme @@ -349,6 +349,7 @@ #include "code\datums\mixed.dm" #include "code\datums\movement_detector.dm" #include "code\datums\mutable_appearance.dm" +#include "code\datums\pathfinding_mover.dm" #include "code\datums\periodic_news.dm" #include "code\datums\pipe_datums.dm" #include "code\datums\position_point_vector.dm" diff --git a/tgui/packages/tgui/interfaces/DroneConsole.js b/tgui/packages/tgui/interfaces/DroneConsole.js index e1ac283c165..ed6b6ac2a12 100644 --- a/tgui/packages/tgui/interfaces/DroneConsole.js +++ b/tgui/packages/tgui/interfaces/DroneConsole.js @@ -11,6 +11,7 @@ import { NoticeBox, ProgressBar, Section, + Stack, } from '../components'; import { Window } from '../layouts'; @@ -154,29 +155,39 @@ const DroneList = (props, context) => { key={drone.name} title={toTitleCase(drone.name)} buttons={ - -