diff --git a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
index d8ca28b7b00..2b43ae43fb3 100644
--- a/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
+++ b/_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
@@ -21404,7 +21404,7 @@
codes_txt = "delivery;dir=4";
location = "Cargo Storage4"
},
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
name = "Old Yeller"
},
/turf/open/floor/iron/recharge_floor,
@@ -33958,7 +33958,7 @@
codes_txt = "delivery;dir=4";
location = "Cargo Storage-2"
},
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
name = "Parcel Pete"
},
/obj/item/radio/intercom/directional/west,
diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index 1cb51bb30c0..6b9115f4b99 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -22766,7 +22766,7 @@
name = "warehouse shutters control"
},
/obj/structure/window/reinforced/spawner/directional/north,
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/machinery/navbeacon{
codes_txt = "delivery;dir=4";
location = "QM #1"
@@ -48594,7 +48594,7 @@
/turf/open/floor/iron,
/area/station/maintenance/port/aft)
"lUm" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/machinery/navbeacon{
codes_txt = "delivery;dir=4";
location = "QM #4"
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index 51b0e5c781c..a31a47131c1 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -63657,7 +63657,7 @@
},
/obj/effect/turf_decal/bot,
/obj/machinery/light/small/directional/east,
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/machinery/status_display/supply{
pixel_x = 32
},
@@ -81435,7 +81435,7 @@
location = "QM #1"
},
/obj/effect/turf_decal/bot,
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/turf/open/floor/iron,
/area/station/cargo/storage)
"xaH" = (
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index d8b9a3fbce1..316f1b0f0f2 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -20077,7 +20077,7 @@
/turf/open/floor/engine,
/area/station/engineering/supermatter/room)
"gWz" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/structure/cable,
/obj/machinery/navbeacon{
codes_txt = "delivery;dir=1";
@@ -21868,7 +21868,7 @@
/turf/open/floor/iron/dark,
/area/station/ai/satellite/exterior)
"hBo" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/structure/cable,
/obj/machinery/navbeacon{
codes_txt = "delivery;dir=2";
@@ -30607,7 +30607,7 @@
/obj/effect/decal/cleanable/dirt,
/obj/effect/decal/cleanable/blood/oil/slippery,
/obj/effect/decal/cleanable/blood/gibs/down,
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
name = "Leaping Rabbit"
},
/obj/structure/disposalpipe/segment,
diff --git a/_maps/map_files/NebulaStation/NebulaStation.dmm b/_maps/map_files/NebulaStation/NebulaStation.dmm
index bd3547f33d8..a29aefe5dd3 100644
--- a/_maps/map_files/NebulaStation/NebulaStation.dmm
+++ b/_maps/map_files/NebulaStation/NebulaStation.dmm
@@ -18050,7 +18050,7 @@
/area/station/engineering/storage)
"cII" = (
/obj/effect/turf_decal/bot,
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
home_destination = "QM #2";
suffix = "#2"
},
@@ -75322,7 +75322,7 @@
/area/station/engineering/supermatter/room)
"lcm" = (
/obj/effect/turf_decal/bot,
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
home_destination = "QM #2";
suffix = "#2"
},
@@ -90689,7 +90689,7 @@
/area/station/hallway/primary/fore)
"nvZ" = (
/obj/effect/turf_decal/bot,
-/mob/living/simple_animal/bot/mulebot{
+/mob/living/basic/bot/mulebot{
home_destination = "QM #2";
suffix = "#2"
},
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index c6f10ec5116..7e9d064585a 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -37251,7 +37251,7 @@
/turf/open/floor/iron/dark,
/area/station/security/prison/garden)
"lQz" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/effect/turf_decal/delivery,
/obj/machinery/navbeacon{
location = "QM #2";
@@ -42305,7 +42305,7 @@
/turf/open/floor/iron/dark,
/area/station/medical/virology)
"nIC" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/effect/turf_decal/delivery,
/obj/machinery/navbeacon{
location = "QM #6";
@@ -48524,7 +48524,7 @@
/turf/open/floor/iron/dark,
/area/station/ai/satellite/interior)
"pXq" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/effect/turf_decal/delivery,
/obj/machinery/navbeacon{
location = "QM #3";
@@ -53129,7 +53129,7 @@
/turf/closed/wall,
/area/station/engineering/atmospherics_engine)
"rCs" = (
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/effect/turf_decal/delivery,
/obj/machinery/navbeacon{
location = "QM #1";
diff --git a/_maps/map_files/wawastation/wawastation.dmm b/_maps/map_files/wawastation/wawastation.dmm
index cf1b7a136a7..a723bb3f2a8 100644
--- a/_maps/map_files/wawastation/wawastation.dmm
+++ b/_maps/map_files/wawastation/wawastation.dmm
@@ -3278,7 +3278,7 @@
codes_txt = "delivery;dir=8";
location = "QM #2"
},
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/turf/open/floor/iron,
/area/station/cargo/warehouse)
"beO" = (
@@ -16365,7 +16365,7 @@
codes_txt = "delivery;dir=8";
location = "QM #1"
},
-/mob/living/simple_animal/bot/mulebot,
+/mob/living/basic/bot/mulebot,
/obj/machinery/light/small/directional/east,
/turf/open/floor/iron,
/area/station/cargo/warehouse)
diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm
index f8285b075e4..a6724aeb4aa 100644
--- a/code/__DEFINES/ai/ai.dm
+++ b/code/__DEFINES/ai/ai.dm
@@ -21,6 +21,7 @@
///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on
#define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles
#define AI_BOT_PATH_LENGTH 60
+#define AI_MULEBOT_PATH_LENGTH 150 //we making a pilgramage sometimes...
// How far should we, by default, be looking for interesting things to de-idle?
#define AI_DEFAULT_INTERESTING_DIST 10
diff --git a/code/__DEFINES/ai/bot_keys.dm b/code/__DEFINES/ai/bot_keys.dm
index 0c7758f6134..653bace09d8 100644
--- a/code/__DEFINES/ai/bot_keys.dm
+++ b/code/__DEFINES/ai/bot_keys.dm
@@ -149,3 +149,12 @@
#define BB_ROBOT_TARGET "robot_target"
///key that holds materials we can refill
#define BB_REFILLABLE_TARGET "refillable_target"
+
+
+//mulebots
+///key that holds our delivery destination's name
+#define BB_MULEBOT_DESTINATION_BEACON "mulebot_destination"
+///key that holds our home port's name
+#define BB_MULEBOT_HOME_BEACON "mulebot_home_beacon"
+///key that holds our current delivery target atom
+#define BB_MULEBOT_TRAVEL_TARGET "mulebot_travel_target"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index cf26bbbad24..7fe071ff8a3 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -23,11 +23,11 @@
#define COMSIG_MOB_CANCEL_CLICKON (1<<0)
///from base of mob/alt_click_on_secodary(): (atom/A)
#define COMSIG_MOB_ALTCLICKON_SECONDARY "mob_altclickon_secondary"
-/// From base of /mob/living/simple_animal/bot/proc/bot_step()
+/// From base of /mob/living/basic/bot/proc/bot_step()
#define COMSIG_MOB_BOT_PRE_STEP "mob_bot_pre_step"
/// Should always match COMPONENT_MOVABLE_BLOCK_PRE_MOVE as these are interchangeable and used to block movement.
#define COMPONENT_MOB_BOT_BLOCK_PRE_STEP COMPONENT_MOVABLE_BLOCK_PRE_MOVE
-/// From base of /mob/living/simple_animal/bot/proc/bot_step()
+/// From base of /mob/living/basic/bot/proc/bot_step()
#define COMSIG_MOB_BOT_STEP "mob_bot_step"
/// From base of /mob/proc/update_held_items
diff --git a/code/__DEFINES/dcs/signals/signals_moveloop.dm b/code/__DEFINES/dcs/signals/signals_moveloop.dm
index 8a354f8bfbb..95208dd1e81 100644
--- a/code/__DEFINES/dcs/signals/signals_moveloop.dm
+++ b/code/__DEFINES/dcs/signals/signals_moveloop.dm
@@ -11,3 +11,6 @@
#define COMSIG_MOVELOOP_JPS_REPATH "moveloop_jps_repath"
///from [/datum/move_loop/has_target/jps/on_finish_pathing]
#define COMSIG_MOVELOOP_JPS_FINISHED_PATHING "moveloop_jps_finished_pathing"
+
+///from /datum/move_loop/has_target/jps/frustrations/handle_move_attempt_failure
+#define COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED "moveloop_jps_frustration_incremented"
diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 1b71b003872..4e506977dc3 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -158,6 +158,8 @@
#define HUD_WIZARD_COMPACT_PERKS "wizard_compact_perks"
#define HUD_WIZARD_PERK(slot) "wizard_perk:[slot]"
+#define HUD_MULEBOT_CHARGE "mulebot_charge"
+
/// Converts item slots to hud keys
#define HUD_KEY_ITEM_SLOT(slot) "item_slot:[slot]"
/// Converts item slots to hud keys as a compiler constant
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 28300319502..d314074dc32 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -192,9 +192,7 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list(
#define isrevenant(A) (istype(A, /mob/living/basic/revenant))
-#define isbot(A) (istype(A, /mob/living/simple_animal/bot) || istype(A, /mob/living/basic/bot))
-
-#define isbasicbot(A) (istype(A, /mob/living/basic/bot))
+#define isbot(A) istype(A, /mob/living/basic/bot)
#define ismouse(A) (istype(A, /mob/living/basic/mouse))
diff --git a/code/__DEFINES/robots.dm b/code/__DEFINES/robots.dm
index 2c76b6f3b91..60f6cbbd762 100644
--- a/code/__DEFINES/robots.dm
+++ b/code/__DEFINES/robots.dm
@@ -399,3 +399,10 @@ DEFINE_BITFIELD(janitor_mode_flags, list(
/// Default offsets for riding a cyborg
#define DEFAULT_ROBOT_RIDING_OFFSETS list(TEXT_NORTH = list(0, 4), TEXT_SOUTH = list(0, 4), TEXT_EAST = list(-6, 3), TEXT_WEST = list(6, 3))
+
+
+//mulebots
+#define MULEBOT_MOOD_ANNOYED "ANNOYED"
+#define MULEBOT_MOOD_CHIME "CHIME"
+#define MULEBOT_MOOD_DELIGHT "DELIGHT"
+#define MULEBOT_MOOD_SIGH "SIGH"
diff --git a/code/__HELPERS/names.dm b/code/__HELPERS/names.dm
index acdb1f02133..f3af1b68531 100644
--- a/code/__HELPERS/names.dm
+++ b/code/__HELPERS/names.dm
@@ -68,9 +68,6 @@
/mob/living/basic/bot/generate_random_mob_name(unique)
return generate_random_name(gender, unique, list(/datum/language/machine = 1))
-/mob/living/simple_animal/bot/generate_random_mob_name(unique)
- return generate_random_name(gender, unique, list(/datum/language/machine = 1))
-
GLOBAL_VAR(command_name)
/proc/command_name()
if (GLOB.command_name)
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index 186385debfe..9d8ea11ce86 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -98,7 +98,6 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
/mob/living/basic/drone,
/mob/living/silicon/ai,
/mob/living/silicon/robot,
- /mob/living/simple_animal/bot,
)),
"security" = typecacheof(list(/mob/living/basic/bot/secbot)),
"spiders" = typecacheof(list(
@@ -123,7 +122,7 @@ GLOBAL_LIST_INIT(phobia_mobs, list(
/mob/living/basic/skeleton,
/mob/living/basic/wizard,
/mob/living/basic/zombie,
- /mob/living/simple_animal/bot/mulebot/paranormal,
+ /mob/living/basic/bot/mulebot/paranormal,
/mob/living/basic/voidwalker,
)),
))
diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm
index e83f68d22db..ea1e7f90cda 100644
--- a/code/controllers/subsystem/movement/movement_types.dm
+++ b/code/controllers/subsystem/movement/movement_types.dm
@@ -461,6 +461,94 @@
if(length(movement_path))
movement_path.Cut(1,2)
else
+ return handle_move_attempt_failure()
+
+
+/datum/move_loop/has_target/jps/proc/handle_move_attempt_failure()
+ EVLOG_TEXT(moving, EVLOG_CATEGORY_MOVELOOPS, "Path recalculating due to obstruction")
+ INVOKE_ASYNC(src, PROC_REF(recalculate_path))
+ return MOVELOOP_FAILURE
+
+/datum/move_loop/has_target/jps/frustrations
+ ///maximum amount of frustrations before we recalculate path
+ var/maximum_frustrations
+ ///what is our current frustration?
+ var/current_frustrations = 0
+ ///how long before we're able to increment frustration?
+ var/frustration_delay
+ ///have we drawn our initial path?
+ var/initial_path_drawn = FALSE
+ ///cooldown between frustration increments
+ COOLDOWN_DECLARE(frustration_cooldown)
+
+
+/datum/move_manager/proc/frustrations_move(moving,
+ chasing,
+ delay,
+ timeout,
+ repath_delay,
+ max_path_length,
+ minimum_distance,
+ list/access,
+ simulated_only,
+ turf/avoid,
+ skip_first,
+ subsystem,
+ diagonal_handling,
+ priority,
+ flags,
+ datum/extra_info,
+ initial_path)
+ return add_to_loop(moving,
+ subsystem,
+ /datum/move_loop/has_target/jps/frustrations,
+ priority,
+ flags,
+ extra_info,
+ delay,
+ timeout,
+ chasing,
+ repath_delay,
+ max_path_length,
+ minimum_distance,
+ access,
+ simulated_only,
+ avoid,
+ skip_first,
+ diagonal_handling,
+ initial_path)
+
+/datum/move_loop/has_target/jps/frustrations/setup(delay, timeout, atom/chasing, maximum_frustrations = 10, frustration_delay = 2 SECONDS)
+ . = ..()
+ if(!.)
+ return
+ src.maximum_frustrations = maximum_frustrations
+ src.frustration_delay = frustration_delay
+
+/datum/move_loop/has_target/jps/frustrations/recalculate_path()
+ if(initial_path_drawn && current_frustrations < maximum_frustrations)
+ return
+ return ..()
+
+/datum/move_loop/has_target/jps/frustrations/loop_stopped()
+ . = ..()
+
+/datum/move_loop/has_target/jps/frustrations/on_finish_pathing(list/path)
+ . = ..()
+ if(movement_path)
+ initial_path_drawn = TRUE
+
+/datum/move_loop/has_target/jps/frustrations/handle_move_attempt_failure()
+ if(!initial_path_drawn)
+ INVOKE_ASYNC(src, PROC_REF(recalculate_path))
+ return MOVELOOP_FAILURE
+ if(!COOLDOWN_FINISHED(src, frustration_cooldown))
+ return NONE
+ COOLDOWN_START(src, frustration_cooldown, frustration_delay)
+ current_frustrations++
+ SEND_SIGNAL(src, COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED, current_frustrations)
+ if(current_frustrations >= maximum_frustrations)
+ current_frustrations = 0
EVLOG_TEXT(moving, EVLOG_CATEGORY_MOVELOOPS, "Path recalculating due to obstruction")
INVOKE_ASYNC(src, PROC_REF(recalculate_path))
return MOVELOOP_FAILURE
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
index 1153833a31e..a8980d15b85 100644
--- a/code/datums/ai/movement/ai_movement_jps.dm
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -12,6 +12,16 @@
var/atom/movable/moving = controller.pawn
var/delay = controller.movement_delay
+ var/datum/move_loop/loop = setup_moveloop(controller, current_movement_target, moving, delay)
+
+ RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
+ RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
+ RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming))
+
+ return loop
+
+
+/datum/ai_movement/jps/proc/setup_moveloop(datum/ai_controller/controller, atom/current_movement_target, atom/movable/moving, delay)
var/datum/move_loop/has_target/jps/loop = GLOB.move_manager.jps_move(moving,
current_movement_target,
delay,
@@ -24,11 +34,6 @@
diagonal_handling = diagonal_flags,
extra_info = controller,
)
-
- RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
- RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
- RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming))
-
return loop
/datum/ai_movement/jps/proc/repath_incoming(datum/move_loop/has_target/jps/source)
@@ -47,9 +52,35 @@
var/datum/move_loop/loop = ..()
var/atom/our_pawn = controller.pawn
if(isnull(our_pawn))
- return
+ return null
our_pawn.RegisterSignal(loop, COMSIG_MOVELOOP_JPS_FINISHED_PATHING, TYPE_PROC_REF(/mob/living/basic/bot, generate_bot_path))
+ return loop
/datum/ai_movement/jps/bot/travel_to_beacon
maximum_length = AI_BOT_PATH_LENGTH
max_pathing_attempts = 10
+
+
+/datum/ai_movement/jps/bot/mulebot
+ max_pathing_attempts = 10
+ maximum_length = AI_MULEBOT_PATH_LENGTH
+
+/datum/ai_movement/jps/bot/mulebot/setup_moveloop(datum/ai_controller/controller, atom/current_movement_target, atom/movable/moving, delay)
+ var/datum/move_loop/has_target/jps/frustrations/loop = GLOB.move_manager.frustrations_move(moving,
+ current_movement_target,
+ delay,
+ repath_delay = 0.5 SECONDS,
+ simulated_only = !HAS_TRAIT(controller.pawn, TRAIT_SPACEWALK),
+ max_path_length = maximum_length,
+ minimum_distance = controller.get_minimum_distance(),
+ access = controller.get_access(),
+ subsystem = SSai_movement,
+ diagonal_handling = diagonal_flags,
+ extra_info = controller,
+ )
+ return loop
+
+/datum/ai_movement/jps/bot/mulebot/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
+ var/datum/move_loop/loop = ..()
+ var/atom/our_pawn = controller.pawn
+ our_pawn.RegisterSignal(loop, COMSIG_MOVELOOP_JPS_FRUSTRATION_INCREMENTED, TYPE_PROC_REF(/mob/living/basic/bot/mulebot, handle_buzzing))
diff --git a/code/datums/wires/mulebot.dm b/code/datums/wires/mulebot.dm
index c3b4efe3460..fa22771945d 100644
--- a/code/datums/wires/mulebot.dm
+++ b/code/datums/wires/mulebot.dm
@@ -3,7 +3,7 @@
#define SLOW_MOTOR_SPEED 3
/datum/wires/mulebot
- holder_type = /mob/living/simple_animal/bot/mulebot
+ holder_type = /mob/living/basic/bot/mulebot
proper_name = "Mulebot"
randomize = TRUE
@@ -19,12 +19,12 @@
/datum/wires/mulebot/interactable(mob/user)
if(!..())
return FALSE
- var/mob/living/simple_animal/bot/mulebot/mule = holder
- if(mule.bot_cover_flags & BOT_COVER_MAINTS_OPEN)
+ var/mob/living/basic/bot/mulebot/mule = holder
+ if(mule.bot_access_flags & BOT_COVER_MAINTS_OPEN)
return TRUE
/datum/wires/mulebot/on_cut(wire, mend, source)
- var/mob/living/simple_animal/bot/mulebot/mule = holder
+ var/mob/living/basic/bot/mulebot/mule = holder
switch(wire)
if(WIRE_MOTOR1, WIRE_MOTOR2)
if(is_cut(WIRE_MOTOR1) && is_cut(WIRE_MOTOR2))
@@ -49,7 +49,7 @@
holder.audible_message(span_hear("Something inside [mule] clicks ominously!"), null, 1)
/datum/wires/mulebot/on_pulse(wire)
- var/mob/living/simple_animal/bot/mulebot/mule = holder
+ var/mob/living/basic/bot/mulebot/mule = holder
if(!mule.has_power(TRUE))
return //logically mulebots can't flash and beep if they don't have power.
switch(wire)
@@ -57,7 +57,7 @@
holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The charge light flickers."))
if(WIRE_AVOIDANCE)
holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The external warning lights flash briefly."))
- flick("[mule.base_icon]1", mule)
+ flick("[mule.base_icon_state]1", mule)
if(WIRE_LOADCHECK)
holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] The load platform clunks."))
if(WIRE_MOTOR1, WIRE_MOTOR2)
@@ -65,6 +65,7 @@
else
holder.visible_message(span_notice("[icon2html(mule, viewers(holder))] You hear a radio crackle."))
+
#undef FAST_MOTOR_SPEED
#undef AVERAGE_MOTOR_SPEED
#undef SLOW_MOTOR_SPEED
diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm
index f56633befce..a38ba684758 100644
--- a/code/game/data_huds.dm
+++ b/code/game/data_huds.dm
@@ -455,46 +455,6 @@ Diagnostic HUDs!
else
set_hud_image_state(DIAG_CAMERA_HUD, "hudcamera")
-/*~~~~~~~~~
- Bots!
-~~~~~~~~~~*/
-/mob/living/simple_animal/bot/proc/diag_hud_set_bothealth()
- set_hud_image_state(DIAG_HUD, "huddiag[RoundDiagBar(health/maxHealth)]")
-
-/mob/living/simple_animal/bot/proc/diag_hud_set_botstat() //On (With wireless on or off), Off, EMP'ed
- if(bot_mode_flags & BOT_MODE_ON)
- set_hud_image_state(DIAG_STAT_HUD, "hudstat")
- else if(stat) //Generally EMP causes this
- set_hud_image_state(DIAG_STAT_HUD, "hudoffline")
- else //Bot is off
- set_hud_image_state(DIAG_STAT_HUD, "huddead2")
-
-/mob/living/simple_animal/bot/proc/diag_hud_set_botmode() //Shows a bot's current operation
- if(client) //If the bot is player controlled, it will not be following mode logic!
- set_hud_image_state(DIAG_BOT_HUD, "hudsentient")
- return
-
- switch(mode)
- if(BOT_SUMMON, BOT_RESPONDING) //Responding to PDA or AI summons
- set_hud_image_state(DIAG_BOT_HUD, "hudcalled")
- if(BOT_CLEANING, BOT_HEALING) //Cleanbot cleaning, repairbot fixing, or Medibot Healing
- set_hud_image_state(DIAG_BOT_HUD, "hudworking")
- if(BOT_PATROL, BOT_START_PATROL) //Patrol mode
- set_hud_image_state(DIAG_BOT_HUD, "hudpatrol")
- if(BOT_PREP_ARREST, BOT_ARREST, BOT_HUNT) //STOP RIGHT THERE, CRIMINAL SCUM!
- set_hud_image_state(DIAG_BOT_HUD, "hudalert")
- if(BOT_MOVING, BOT_DELIVER, BOT_GO_HOME, BOT_NAV) //Moving to target for normal bots, moving to deliver or go home for MULES.
- set_hud_image_state(DIAG_BOT_HUD, "hudmove")
- else
- set_hud_image_state(DIAG_BOT_HUD, "")
-
-/mob/living/simple_animal/bot/mulebot/proc/diag_hud_set_mulebotcell()
- if(QDELETED(cell) || (cell.maxcharge == 0))
- set_hud_image_state(DIAG_BATT_HUD, "hudnobatt")
- else
- var/chargelvl = (cell.charge/cell.maxcharge)
- set_hud_image_state(DIAG_BATT_HUD, "hudbatt[RoundDiagBar(chargelvl)]")
-
/*~~~~~~~~~~~~
Airlocks!
~~~~~~~~~~~~~*/
diff --git a/code/game/machinery/botlaunchpad.dm b/code/game/machinery/botlaunchpad.dm
index 857e9278af0..ab949fb4358 100644
--- a/code/game/machinery/botlaunchpad.dm
+++ b/code/game/machinery/botlaunchpad.dm
@@ -65,12 +65,8 @@
user.balloon_alert(user, "no bots sent from the pad!")
return
user.balloon_alert(user, "bot sent back to pad")
- if(isbasicbot(our_bot))
- var/mob/living/basic/bot/basic_bot = our_bot
- basic_bot.summon_bot(src)
- return
- var/mob/living/simple_animal/bot/simple_bot = our_bot
- simple_bot.call_bot(src, get_turf(src))
+ var/mob/living/basic/bot/basic_bot = our_bot
+ basic_bot.summon_bot(src)
/obj/structure/closet/supplypod/transport/botpod
reverse_option_list = list("Mobs"=TRUE,"Objects"=FALSE,"Anchored"=FALSE,"Underfloor"=FALSE,"Wallmounted"=FALSE,"Floors"=FALSE,"Walls"=FALSE,"Mecha"=FALSE)
diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm
index 0c153c59128..81641800968 100644
--- a/code/game/objects/structures/plasticflaps.dm
+++ b/code/game/objects/structures/plasticflaps.dm
@@ -224,7 +224,7 @@
else if(isliving(mover)) // You Shall Not Pass!
var/mob/living/living_mover = mover
- if(istype(living_mover.buckled, /mob/living/simple_animal/bot/mulebot)) // mulebot passenger gets a free pass.
+ if(istype(living_mover.buckled, /mob/living/basic/bot/mulebot)) // mulebot passenger gets a free pass.
return TRUE
if(living_mover.body_position == STANDING_UP && living_mover.mob_size != MOB_SIZE_TINY && !(HAS_TRAIT(living_mover, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(living_mover, TRAIT_VENTCRAWLER_NUDE)))
diff --git a/code/modules/antagonists/malf_ai/malf_ai_modules.dm b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
index 0fdb4251f5f..5f810a1e37c 100644
--- a/code/modules/antagonists/malf_ai/malf_ai_modules.dm
+++ b/code/modules/antagonists/malf_ai/malf_ai_modules.dm
@@ -1097,7 +1097,7 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module/malf))
/obj/structure,
/obj/item/radio/intercom,
/obj/item/modular_computer,
- /mob/living/simple_animal/bot,
+ /mob/living/basic/bot,
/mob/living/silicon,
)
diff --git a/code/modules/antagonists/spy/spy_bounty.dm b/code/modules/antagonists/spy/spy_bounty.dm
index 5344f86be2e..5c65e544dbc 100644
--- a/code/modules/antagonists/spy/spy_bounty.dm
+++ b/code/modules/antagonists/spy/spy_bounty.dm
@@ -679,7 +679,7 @@
/datum/spy_bounty/some_bot/get_dupe_protection_key(atom/movable/stealing)
return bot_type
-/datum/spy_bounty/some_bot/finish_cleanup(mob/living/simple_animal/bot/stealing)
+/datum/spy_bounty/some_bot/finish_cleanup(mob/living/basic/bot/stealing)
if(stealing.client)
to_chat(stealing, span_deadsay("You've been stolen! You are shipped off to the black market and taken apart for spare parts..."))
stealing.investigate_log("stole by a spy (and deleted)", INVESTIGATE_DEATHS)
diff --git a/code/modules/cargo/packs/service.dm b/code/modules/cargo/packs/service.dm
index 4092fcfe25d..3a97e99ee54 100644
--- a/code/modules/cargo/packs/service.dm
+++ b/code/modules/cargo/packs/service.dm
@@ -68,7 +68,7 @@
desc = "Pink-haired Quartermaster not doing her job? Replace her with this tireless worker, today! \
Contains one MULEbot."
cost = CARGO_CRATE_VALUE * 4
- contains = list(/mob/living/simple_animal/bot/mulebot)
+ contains = list(/mob/living/basic/bot/mulebot)
crate_name = "\improper MULEbot Crate"
crate_type = /obj/structure/closet/crate/large
diff --git a/code/modules/events/ion_storm.dm b/code/modules/events/ion_storm.dm
index 555a4b03c14..59caf2b1560 100644
--- a/code/modules/events/ion_storm.dm
+++ b/code/modules/events/ion_storm.dm
@@ -60,7 +60,7 @@
M.post_lawchange()
if(botEmagChance)
- for(var/mob/living/simple_animal/bot/bot in GLOB.alive_mob_list)
+ for(var/mob/living/basic/bot/bot in GLOB.alive_mob_list)
if(prob(botEmagChance))
bot.emag_act()
diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm
index b600c291214..5c643461b15 100644
--- a/code/modules/mob/dead/observer/orbit.dm
+++ b/code/modules/mob/dead/observer/orbit.dm
@@ -302,7 +302,7 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
/mob/eye,
/mob/living/basic/boss,
/mob/living/basic/regal_rat,
- /mob/living/simple_animal/bot,
+ /mob/living/basic/bot,
/mob/living/simple_animal/hostile/megafauna,
))
if(!is_type_in_typecache(potential_mob_poi, mob_allowed_typecache) && !potential_mob_poi.GetComponent(/datum/component/deadchat_control))
diff --git a/code/modules/mob/living/basic/bots/_bots.dm b/code/modules/mob/living/basic/bots/_bots.dm
index 15d983b8745..f7962d9b868 100644
--- a/code/modules/mob/living/basic/bots/_bots.dm
+++ b/code/modules/mob/living/basic/bots/_bots.dm
@@ -196,7 +196,7 @@ GLOBAL_LIST_INIT(command_strings, list(
/mob/living/basic/bot/proc/get_emagged_message()
return get_policy(ROLE_EMAGGED_BOT) || "You are a malfunctioning bot! Disrupt everyone and cause chaos!"
-/mob/living/basic/bot/proc/turn_on()
+/mob/living/basic/bot/proc/turn_on(mob/user)
if(stat == DEAD)
return FALSE
set_mode_flags(bot_mode_flags | BOT_MODE_ON)
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot.dm
new file mode 100644
index 00000000000..55d5632e5e1
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot.dm
@@ -0,0 +1,193 @@
+/mob/living/basic/bot/mulebot
+ name = "\improper MULEbot"
+ desc = "A Multiple Utility Load Effector bot."
+ icon = 'icons/mob/silicon/aibots.dmi'
+ icon_state = "mulebot0"
+ base_icon_state = "mulebot"
+
+ light_color = "#ffcc99"
+ light_power = 0.8
+
+ health = 50
+ maxHealth = 50
+
+ damage_coeff = list(BRUTE = 0.5, BURN = 0.7, TOX = 0, STAMINA = 0, OXY = 0)
+ density = TRUE
+ mob_size = MOB_SIZE_LARGE
+ move_resist = MOVE_FORCE_STRONG
+ animate_movement = SLIDE_STEPS
+ speed = 3
+
+ combat_mode = TRUE
+
+ buckle_lying = 0
+ buckle_prevents_pull = TRUE // No pulling loaded shit
+
+ bot_mode_flags = ~BOT_MODE_ROUNDSTART_POSSESSION
+ req_one_access = list(ACCESS_ROBOTICS, ACCESS_CARGO)
+ radio_key = /obj/item/encryptionkey/headset_cargo
+ radio_channel = RADIO_CHANNEL_SUPPLY
+ pass_flags = PASSFLAPS
+ bot_type = MULE_BOT
+
+ additional_access = /datum/id_trim/job/cargo_technician
+ path_image_color = "#7F5200"
+ hud_type = /datum/hud/living/mulebot
+
+ hackables = "obstacle detection circuits"
+ possessed_message = "You are a MULEbot! Do your best to make sure that packages get to their destination!"
+ ai_controller = /datum/ai_controller/basic_controller/bot/mulebot
+
+ /// unique identifier in case there are multiple mulebots.
+ var/id
+
+ /// what we're transporting
+ var/atom/movable/load
+ /// who's riding us
+ var/mob/living/passenger
+
+ ///flags of mulebot mode
+ var/mulebot_delivery_flags = MULEBOT_RETURN_MODE | MULEBOT_AUTO_PICKUP_MODE | MULEBOT_REPORT_DELIVERY_MODE
+
+ ///Internal Powercell
+ var/obj/item/stock_parts/power_store/cell
+ ///How much power we use when we move.
+ var/cell_move_power_usage = 0.0005 * STANDARD_CELL_CHARGE
+ ///The amount of steps we should take until we rest for a time.
+ var/num_steps = 0
+
+ ///The chance to be deleted and replaced by a different mule
+ var/replacement_chance = 0.666
+ ///home destination, only used by mappers.
+ var/home_destination = ""
+
+/mob/living/basic/bot/mulebot/Initialize(mapload)
+ . = ..()
+
+ if(prob(replacement_chance) && mapload)
+ new /mob/living/basic/bot/mulebot/paranormal(loc)
+ return INITIALIZE_HINT_QDEL
+
+ set_wires(new /datum/wires/mulebot(src))
+ var/obj/item/stock_parts/power_store/cell/upgraded/new_cell = new(src)
+ assign_cell(new_cell)
+ ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, "")
+ AddElement(/datum/element/ridable, /datum/component/riding/creature/mulebot)
+ ADD_TRAIT(src, TRAIT_NOMOBSWAP, INNATE_TRAIT)
+ add_traits(list(TRAIT_NOMOBSWAP, TRAIT_COMBAT_MODE_LOCK), INNATE_TRAIT)
+ RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_pre_move))
+
+ set_id(suffix || assign_random_name())
+ suffix = null
+ if(name == "\improper MULEbot")
+ name = "\improper MULEbot [id]"
+ set_home(get_turf(src))
+ ai_controller.update_able_to_run()
+ update_appearance()
+
+/mob/living/basic/bot/mulebot/Destroy()
+ UnregisterSignal(src, COMSIG_MOVABLE_PRE_MOVE)
+ unload()
+ QDEL_NULL(cell)
+ return ..()
+
+/mob/living/basic/bot/mulebot/proc/assign_cell(atom/new_cell)
+ cell = new_cell
+ var/atom/movable/screen/mob_charge/charge_hud = hud_used?.screen_objects[HUD_MULEBOT_CHARGE]
+ charge_hud?.update_battery_overlay(new_cell)
+ charge_hud?.calculate_charge()
+
+
+/mob/living/basic/bot/mulebot/attack_hand(mob/living/carbon/human/user, list/modifiers)
+ if(bot_access_flags & BOT_COVER_MAINTS_OPEN && !HAS_AI_ACCESS(user))
+ wires.interact(user)
+ return
+ if(wires.is_cut(WIRE_RX) && HAS_AI_ACCESS(user))
+ return
+
+ return ..()
+
+/mob/living/basic/bot/mulebot/examine(mob/user)
+ . = ..()
+ if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
+ if(cell)
+ . += span_notice("It has \a [cell] installed.")
+ . += span_info("You can use a crowbar to remove it.")
+ else
+ . += span_notice("It has an empty compartment where a power cell can be installed.")
+ if(load) //observer check is so we don't show the name of the ghost that's sitting on it to prevent metagaming who's ded.
+ . += span_notice("\A [isobserver(load) ? "ghostly figure" : load] is on its load platform.")
+
+/mob/living/basic/bot/mulebot/get_cell()
+ return cell
+
+/mob/living/basic/bot/mulebot/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
+ if(!can_unarmed_attack())
+ return
+ if(isturf(target) && isturf(loc) && loc.Adjacent(target) && load)
+ unload(get_dir(loc, target))
+ else
+ return ..()
+
+/mob/living/basic/bot/mulebot/turn_on(mob/user)
+ if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
+ if(user)
+ to_chat(user, span_warning("[src]'s maintenance panel is open!"))
+ return FALSE
+ if(!has_power())
+ if(user)
+ to_chat(user, span_warning("[src] has no power!"))
+ return FALSE
+ return ..()
+
+/mob/living/basic/bot/mulebot/update_icon_state() //if you change the icon_state names, please make sure to update /datum/wires/mulebot/on_pulse() as well. <3
+ . = ..()
+ icon_state = "[base_icon_state][(bot_mode_flags & BOT_MODE_ON) ? wires?.is_cut(WIRE_AVOIDANCE) : "0"]"
+
+/mob/living/basic/bot/mulebot/update_overlays()
+ . = ..()
+ if(bot_access_flags & BOT_COVER_MAINTS_OPEN)
+ . += "[base_icon_state]-hatch"
+ if(isnull(load) || ismob(load)) //mob offsets and such are handled by the riding component / buckling
+ return
+ var/mutable_appearance/load_overlay = mutable_appearance(load.icon, load.icon_state, layer + 0.01)
+ load_overlay.pixel_y = initial(load.pixel_y) + 11
+ . += load_overlay
+
+/mob/living/basic/bot/mulebot/proc/handle_buzzing(datum/move_loop/has_target/jps/frustrations/source, frustration_counter)
+ SIGNAL_HANDLER
+
+ update_bot_mode(new_mode = BOT_BLOCKED)
+ var/buzz_mode = frustration_counter >= source.maximum_frustrations ? MULEBOT_MOOD_ANNOYED : MULEBOT_MOOD_SIGH
+ buzz(buzz_mode)
+
+/mob/living/basic/bot/mulebot/handle_loop_movement(atom/movable/source, atom/oldloc, dir, forced) //incase we start moving again after being previously blocked, update our mode
+ . = ..()
+ if(mode != BOT_BLOCKED)
+ return
+ var/obj/machinery/navbeacon/beacon = ai_controller.current_movement_target
+ if(!istype(beacon))
+ return
+ var/intended_mode = beacon.location == ai_controller.blackboard[BB_MULEBOT_HOME_BEACON] ? BOT_GO_HOME : BOT_DELIVER
+ update_bot_mode(new_mode = intended_mode)
+
+///Noises that mulebots make
+/mob/living/basic/bot/mulebot/proc/buzz(type)
+ switch(type)
+ if(MULEBOT_MOOD_SIGH)
+ audible_message(span_hear("[src] makes a sighing buzz."))
+ playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 50, FALSE)
+ if(MULEBOT_MOOD_ANNOYED)
+ audible_message(span_hear("[src] makes an annoyed buzzing sound."))
+ playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE)
+ if(MULEBOT_MOOD_DELIGHT)
+ audible_message(span_hear("[src] makes a delighted ping!"))
+ playsound(src, 'sound/machines/ping.ogg', 50, FALSE)
+ if(MULEBOT_MOOD_CHIME)
+ audible_message(span_hear("[src] makes a chiming sound!"))
+ playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
+ flick("[base_icon_state]1", src)
+
+/// returns true if the bot is fully powered.
+/mob/living/basic/bot/mulebot/proc/has_power()
+ return cell && cell.charge > 0 && (!wires.is_cut(WIRE_POWER1) && !wires.is_cut(WIRE_POWER2))
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm
new file mode 100644
index 00000000000..7898ced148c
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_ai.dm
@@ -0,0 +1,145 @@
+/datum/ai_controller/basic_controller/bot/mulebot
+ blackboard = list(
+ BB_SALUTE_MESSAGES = list(
+ "blinks its light in appreciation towards",
+ )
+ )
+ ai_movement = /datum/ai_movement/jps/bot/mulebot
+ max_target_distance = AI_MULEBOT_PATH_LENGTH
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/respond_to_summon,
+ /datum/ai_planning_subtree/salute_authority,
+ /datum/ai_planning_subtree/attempt_delivery,
+ /datum/ai_planning_subtree/find_delivery_beacon,
+ )
+ reset_keys = list(
+ BB_BOT_SUMMON_TARGET,
+ BB_MULEBOT_DESTINATION_BEACON,
+ BB_MULEBOT_TRAVEL_TARGET,
+ )
+
+/datum/ai_controller/basic_controller/bot/mulebot/get_able_to_run()
+ var/mob/living/basic/bot/mulebot/bot_pawn = pawn
+ if(!bot_pawn.has_power())
+ return AI_UNABLE_TO_RUN
+ return ..()
+
+/datum/ai_controller/basic_controller/bot/mulebot/setup_able_to_run()
+ . = ..()
+ var/mob/living/basic/bot/my_bot = pawn
+ var/static/list/wire_signals = list(
+ COMSIG_MEND_WIRE(WIRE_POWER1), //this framework is insane
+ COMSIG_MEND_WIRE(WIRE_POWER2),
+ COMSIG_CUT_WIRE(WIRE_POWER1),
+ COMSIG_CUT_WIRE(WIRE_POWER2),
+ )
+ RegisterSignals(my_bot.wires, wire_signals, PROC_REF(update_able_to_run))
+ var/static/list/content_signals = list(
+ COMSIG_ATOM_ENTERED,
+ COMSIG_ATOM_EXITED,
+ )
+ RegisterSignals(my_bot, content_signals, PROC_REF(update_able_to_run))
+
+/datum/ai_planning_subtree/find_delivery_beacon
+ ///what behavior do we use to seek beacons
+ var/find_beacon_behaviour = /datum/ai_behavior/find_delivery_beacon
+
+/datum/ai_planning_subtree/find_delivery_beacon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
+ if(bot_pawn.wires.is_cut(WIRE_BEACON))
+ return
+
+ if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET))
+ controller.queue_behavior(find_beacon_behaviour, BB_MULEBOT_TRAVEL_TARGET)
+
+/datum/ai_behavior/find_delivery_beacon
+ behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/find_delivery_beacon/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
+ var/atom/delivery_beacon
+
+ var/beacon_tag = null
+
+ switch(bot_pawn.mode)
+ if(BOT_DELIVER)
+ beacon_tag = controller.blackboard[BB_MULEBOT_DESTINATION_BEACON]
+ if(BOT_GO_HOME)
+ beacon_tag = controller.blackboard[BB_MULEBOT_HOME_BEACON]
+ else
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ for(var/obj/machinery/navbeacon/beacon as anything in GLOB.deliverybeacons)
+ if(beacon.location == beacon_tag)
+ delivery_beacon = beacon
+ break
+
+ if(isnull(delivery_beacon))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ controller.set_blackboard_key(BB_MULEBOT_TRAVEL_TARGET, delivery_beacon)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+/datum/ai_behavior/travel_towards/delivery_beacon
+ new_movement_type = /datum/ai_movement/jps/bot/mulebot
+
+/datum/ai_planning_subtree/attempt_delivery
+ ///behavior we use to unload crates
+ var/delivery_behaviour = /datum/ai_behavior/handle_delivery
+
+/datum/ai_planning_subtree/attempt_delivery/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET))
+ return
+
+ controller.queue_behavior(delivery_behaviour, BB_MULEBOT_TRAVEL_TARGET)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+/datum/ai_behavior/handle_delivery
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/handle_delivery/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/handle_delivery/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ var/obj/machinery/navbeacon/beacon = controller.blackboard[target_key]
+ var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
+
+ var/load_direction = beacon.codes[NAVBEACON_DELIVERY_DIRECTION] // this will be the load/unload dir
+ if(!load_direction)
+ load_direction = beacon.dir // fallback
+
+ load_direction = text2num(load_direction)
+
+ if(bot_pawn.load)
+ if(bot_pawn.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE)
+ bot_pawn.radio_channel = RADIO_CHANNEL_SUPPLY //Supply channel
+ bot_pawn.buzz(MULEBOT_MOOD_CHIME)
+ bot_pawn.speak("Destination [RUNECHAT_BOLD("[beacon.location]")] reached. Unloading [bot_pawn.load].", bot_pawn.radio_channel)
+ bot_pawn.unload(load_direction)
+
+ else
+ if(bot_pawn.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE) // find a crate
+ var/atom/movable/atom_to_pick_up
+ if(bot_pawn.wires.is_cut(WIRE_LOADCHECK)) // if hacked, load first unanchored thing we find
+ for(var/atom/movable/target_atom in get_step(bot_pawn.loc, load_direction))
+ if(!target_atom.anchored)
+ atom_to_pick_up = target_atom
+ break
+ else // otherwise, look for crates only
+ atom_to_pick_up = locate(/obj/structure/closet/crate) in get_step(bot_pawn.loc, load_direction)
+ if(atom_to_pick_up?.Adjacent(bot_pawn))
+ bot_pawn.load(atom_to_pick_up)
+ if(bot_pawn.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE)
+ bot_pawn.speak("Now loading [bot_pawn.load] at [RUNECHAT_BOLD("[get_area_name(bot_pawn)]")].", bot_pawn.radio_channel)
+
+ if((bot_pawn.mulebot_delivery_flags & MULEBOT_RETURN_MODE) && controller.blackboard[BB_MULEBOT_HOME_BEACON] && controller.blackboard[BB_MULEBOT_HOME_BEACON] != beacon.location)
+ bot_pawn.update_bot_mode(new_mode = BOT_GO_HOME)
+ controller.clear_blackboard_key(BB_MULEBOT_TRAVEL_TARGET)
+ else
+ bot_pawn.bot_reset() // otherwise go idle
+
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm
new file mode 100644
index 00000000000..d7bd93be22c
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_control.dm
@@ -0,0 +1,116 @@
+/mob/living/basic/bot/mulebot/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Mule", name)
+ ui.open()
+
+/mob/living/basic/bot/mulebot/ui_data(mob/user)
+ var/list/data = list()
+ data["powerStatus"] = bot_mode_flags & BOT_MODE_ON
+ data["locked"] = bot_access_flags & BOT_COVER_LOCKED
+ data["siliconUser"] = HAS_SILICON_ACCESS(user)
+ data["mode"] = mode ? "[mode]" : "Ready"
+ data["modeStatus"] = ""
+ switch(mode)
+ if(BOT_IDLE, BOT_DELIVER, BOT_GO_HOME)
+ data["modeStatus"] = "good"
+ if(BOT_BLOCKED, BOT_NAV, BOT_WAIT_FOR_NAV)
+ data["modeStatus"] = "average"
+ if(BOT_NO_ROUTE)
+ data["modeStatus"] = "bad"
+ data["load"] = get_load_name()
+ data["destination"] = ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON]
+ data["homeDestination"] = ai_controller.blackboard[BB_MULEBOT_HOME_BEACON]
+ data["destinationsList"] = GLOB.deliverybeacontags
+ data["cellPercent"] = cell?.percent()
+ data["autoReturn"] = mulebot_delivery_flags & MULEBOT_RETURN_MODE
+ data["autoPickup"] = mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE
+ data["reportDelivery"] = mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE
+ data["botId"] = id
+ data["allowPossession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT
+ data["possessionEnabled"] = can_be_possessed
+ data["paiInserted"] = !!paicard
+ return data
+
+/mob/living/basic/bot/mulebot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ var/mob/user = ui.user
+ if(. || (bot_access_flags & BOT_COVER_LOCKED && !HAS_SILICON_ACCESS(user)))
+ return
+
+ bot_control(action, user, params)
+ return TRUE
+
+/mob/living/basic/bot/mulebot/bot_control(command, mob/user, list/params = list(), pda = FALSE)
+ if(pda && wires.is_cut(WIRE_RX)) // MULE wireless is controlled by wires.
+ return
+
+ switch(command)
+ if("stop")
+ if(mode != BOT_IDLE)
+ bot_reset()
+ if("go")
+ if(mode == BOT_IDLE)
+ start()
+ if("home")
+ if(mode == BOT_IDLE || mode == BOT_DELIVER)
+ start_home()
+ if("destination")
+ var/new_dest
+ if(pda)
+ new_dest = tgui_input_list(user, "Enter Destination", "Mulebot Settings", GLOB.deliverybeacontags, ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON])
+ else
+ new_dest = params["value"]
+ if(new_dest)
+ set_destination(new_dest)
+ if("setid")
+ var/new_id = tgui_input_text(user, "Enter ID", "ID Assignment", id, max_length = MAX_NAME_LEN)
+ if(new_id)
+ set_id(new_id)
+ name = "\improper MULEbot [new_id]"
+ if("sethome")
+ var/new_home = tgui_input_list(user, "Enter Home", "Mulebot Settings", GLOB.deliverybeacontags, ai_controller.blackboard[BB_MULEBOT_HOME_BEACON])
+ if(new_home)
+ ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, new_home)
+ if("unload")
+ if(load && mode != BOT_HUNT)
+ unload()
+ if("autoret")
+ mulebot_delivery_flags ^= MULEBOT_RETURN_MODE
+ if("autopick")
+ mulebot_delivery_flags ^= MULEBOT_AUTO_PICKUP_MODE
+ if("report")
+ mulebot_delivery_flags ^= MULEBOT_REPORT_DELIVERY_MODE
+
+/mob/living/basic/bot/mulebot/proc/start()
+ if(!(bot_mode_flags & BOT_MODE_ON))
+ return
+ if(ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON] == ai_controller.blackboard[BB_MULEBOT_HOME_BEACON])
+ update_bot_mode(new_mode = BOT_GO_HOME)
+ else
+ update_bot_mode(new_mode = BOT_DELIVER)
+
+/mob/living/basic/bot/mulebot/proc/start_home()
+ set_destination(ai_controller.blackboard[BB_MULEBOT_HOME_BEACON])
+ update_bot_mode(new_mode = BOT_GO_HOME)
+
+/mob/living/basic/bot/mulebot/proc/set_destination(new_destination)
+ ai_controller.set_blackboard_key(BB_MULEBOT_DESTINATION_BEACON, new_destination)
+
+/mob/living/basic/bot/mulebot/proc/set_home(turf/home_loc)
+ if(home_destination)
+ ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, home_destination)
+ home_destination = null
+ if(!istype(home_loc))
+ CRASH("MULEbot [id] was requested to set a home location to [home_loc ? "an invalid home loc ([home_loc.type])" : "null"]")
+
+ var/obj/machinery/navbeacon/home_beacon = locate() in home_loc
+ if(isnull(home_beacon))
+ ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, "")
+ return
+ ai_controller.set_blackboard_key(BB_MULEBOT_HOME_BEACON, home_beacon.location)
+ log_transport("[id]: MULEbot successfuly set home location to ID [home_beacon.location] at [home_beacon.x], [home_beacon.y], [home_beacon.z]")
+
+///Sets the new ID of the mulebot
+/mob/living/basic/bot/mulebot/proc/set_id(new_id)
+ id = new_id
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm
new file mode 100644
index 00000000000..5fec23e9bbf
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_delivery.dm
@@ -0,0 +1,117 @@
+/mob/living/basic/bot/mulebot/execute_resist()
+ . = ..()
+ if(load)
+ unload()
+
+/mob/living/basic/bot/mulebot/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == load)
+ unload()
+ if(gone == cell)
+ turn_off()
+ assign_cell()
+ cell = null
+ set_cell_hud()
+
+/mob/living/basic/bot/mulebot/Entered(obj/item/stock_parts/power_store/cell/arrived, atom/old_loc, list/atom/old_locs)
+ . = ..()
+ if(istype(arrived) && isnull(cell))
+ assign_cell(arrived)
+
+// mousedrop a crate to load the bot
+// can load anything if hacked
+/mob/living/basic/bot/mulebot/mouse_drop_receive(atom/movable/atom_to_load, mob/user, params)
+ if(!isliving(user))
+ return
+
+ if(!istype(atom_to_load) || isdead(atom_to_load) || iseyemob(atom_to_load) || istype(atom_to_load, /obj/effect/dummy/phased_mob))
+ return
+
+ load(atom_to_load)
+
+/mob/living/basic/bot/mulebot/post_unbuckle_mob(mob/living/M)
+ load = null
+ return ..()
+
+/mob/living/basic/bot/mulebot/relaymove(mob/living/user, direction)
+ if(user.incapacitated)
+ return
+ if(load == user)
+ unload()
+
+/mob/living/basic/bot/mulebot/remove_air(amount) //To prevent riders suffocating
+ return loc ? loc.remove_air(amount) : null
+
+/// Called to load an atom on the mulebot, which is usually a crate, unless if hacked
+/mob/living/basic/bot/mulebot/proc/load(atom/movable/atom_to_load)
+ if(load || atom_to_load.anchored)
+ return
+
+ if(!isturf(atom_to_load.loc)) //To prevent the loading from stuff from someone's inventory or screen icons.
+ return
+
+ var/obj/structure/closet/crate/crate = atom_to_load
+ if(!istype(crate))
+ if(!wires.is_cut(WIRE_LOADCHECK))
+ buzz(MULEBOT_MOOD_SIGH)
+ return // if not hacked, only allow crates to be loaded
+ crate = null
+
+ if(crate || isobj(atom_to_load))
+ var/obj/object_to_load = atom_to_load
+ if(object_to_load.has_buckled_mobs() || (locate(/mob) in atom_to_load)) //can't load non crates objects with mobs buckled to it or inside it.
+ buzz(MULEBOT_MOOD_SIGH)
+ return
+
+ if(crate)
+ crate.close() //make sure the crate is closed
+
+ object_to_load.forceMove(src)
+
+ else if(isliving(atom_to_load))
+ if(!load_mob(atom_to_load)) //forceMove() is handled in buckling
+ return
+
+ load = atom_to_load
+ update_appearance()
+
+///resolves the name to display for the loaded mob. primarily needed for the paranormal subtype since we don't want to show the name of ghosts riding it.
+/mob/living/basic/bot/mulebot/proc/get_load_name()
+ return load ? load.name : null
+
+///Loads a mob onto the mulebot
+/mob/living/basic/bot/mulebot/proc/load_mob(mob/living/mob_to_load)
+ can_buckle = TRUE
+ if(buckle_mob(mob_to_load))
+ passenger = mob_to_load
+ load = mob_to_load
+ can_buckle = FALSE
+ return TRUE
+
+// called to unload the bot
+// argument is optional direction to unload
+// if zero or null, unload at bot's location
+/mob/living/basic/bot/mulebot/proc/unload(dirn)
+ if(QDELETED(load))
+ if(load) //if our thing was qdel'd, there's likely a leftover reference. just clear it and remove the overlay. we'll let the bot keep moving around to prevent it abruptly stopping somewhere.
+ load = null
+ update_appearance()
+ return
+
+ update_bot_mode(new_mode = BOT_IDLE)
+
+ var/atom/movable/cached_load = load //cache the load since unbuckling mobs clears the var.
+
+ unbuckle_all_mobs()
+
+ if(load) //don't have to do any of this for mobs.
+ cached_load.forceMove(loc)
+ cached_load.pixel_y = initial(cached_load.pixel_y)
+ cached_load.layer = initial(cached_load.layer)
+ SET_PLANE_EXPLICIT(cached_load, initial(cached_load.plane), src)
+ load = null
+
+ if(dirn) //move the thing to the delivery point.
+ cached_load.Move(get_step(loc,dirn), dirn)
+
+ update_appearance()
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm
new file mode 100644
index 00000000000..3e1f26c8584
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_hud.dm
@@ -0,0 +1,74 @@
+/mob/living/basic/bot/mulebot/proc/set_cell_hud()
+ if(!has_power())
+ set_hud_image_state(DIAG_BATT_HUD, "hudnobatt")
+ return
+
+ var/atom/movable/screen/mob_charge/charge_hud = hud_used?.screen_objects[HUD_MULEBOT_CHARGE]
+ charge_hud?.calculate_charge()
+ set_hud_image_state(DIAG_BATT_HUD, "hudbatt[RoundDiagBar(cell.charge/cell.maxcharge)]")
+
+/atom/movable/screen/mob_charge
+ icon = 'icons/obj/machines/cell_charger.dmi'
+ icon_state = "ccharger"
+ screen_loc = ui_stamina
+ ///used to find the overlay for charger icon
+ var/current_charge_level = 4
+ ///dynamic, based on what cell our nulebot's using
+ var/image/battery_overlay
+ ///maptext that displays charge in numbers
+ var/image/charge_overlay
+ ///is there a mouse on us
+ var/hovering = FALSE
+
+/atom/movable/screen/mob_charge/proc/update_battery_overlay(atom/target_battery)
+ var/obj/item/stock_parts/power_store/cell/my_cell = target_battery || (locate() in get_mob())
+ if(isnull(my_cell))
+ battery_overlay = null
+ else
+ battery_overlay = image(icon = my_cell.icon, icon_state = my_cell.icon_state, loc = src, layer = src.layer + 0.1)
+ update_appearance(UPDATE_ICON)
+
+/atom/movable/screen/mob_charge/proc/calculate_charge()
+ var/obj/item/stock_parts/power_store/cell/my_battery = locate() in get_mob()
+ var/charge_value = isnull(my_battery) ? 0 : round(my_battery.charge/my_battery.maxcharge * 100 , 1)
+ current_charge_level = round(charge_value * 4 / 100)
+ charge_overlay.maptext = MAPTEXT("
[charge_value]%
")
+ update_appearance(UPDATE_ICON)
+
+/atom/movable/screen/mob_charge/New(loc, ...)
+ . = ..()
+ charge_overlay = image(loc = src, layer = src.layer+0.2, pixel_y = -5)
+ update_battery_overlay()
+
+/atom/movable/screen/mob_charge/Destroy()
+ charge_overlay = null
+ battery_overlay = null
+ return ..()
+
+/atom/movable/screen/mob_charge/update_overlays()
+ . = ..()
+ . += mutable_appearance(icon, "ccharger-o[current_charge_level]")
+ if(battery_overlay)
+ . |= battery_overlay
+ if(hovering)
+ . |= charge_overlay
+
+/atom/movable/screen/mob_charge/MouseEntered(location,control,params)
+ if(usr != get_mob())
+ return
+ . = ..()
+ hovering = TRUE
+ calculate_charge()
+
+/atom/movable/screen/mob_charge/MouseExited(location, control, params)
+ if(usr != get_mob())
+ return
+ . = ..()
+ hovering = FALSE
+ update_appearance(UPDATE_ICON)
+
+/datum/hud/living/mulebot
+
+/datum/hud/living/mulebot/initialize_screen_objects()
+ . = ..()
+ add_screen_object(/atom/movable/screen/mob_charge, HUD_MULEBOT_CHARGE, HUD_GROUP_INFO)
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm
new file mode 100644
index 00000000000..0654477a096
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_movement.dm
@@ -0,0 +1,72 @@
+/mob/living/basic/bot/mulebot/MobBump(mob/bumped_mob) // called when the bot bumps into a mob
+ if(mind || !isliving(bumped_mob)) //if there's a sentience controlling the bot, they aren't allowed to harm folks.
+ return ..()
+ var/mob/living/bumped_living = bumped_mob
+ if(wires.is_cut(WIRE_AVOIDANCE)) // usually just bumps, but if the avoidance wire is cut, knocks them over.
+ if(iscyborg(bumped_living))
+ visible_message(span_danger("[src] bumps into [bumped_living]!"))
+ else if(bumped_living.Knockdown(8 SECONDS))
+ log_combat(src, bumped_living, "knocked down")
+ visible_message(span_danger("[src] knocks over [bumped_living]!"))
+ return ..()
+
+/mob/living/basic/bot/mulebot/on_bot_movement(atom/movable/source, atom/oldloc, dir, forced)
+ cell?.use(cell_move_power_usage)
+ set_cell_hud()
+
+ if(has_gravity())
+ for(var/mob/living/carbon/human/future_pancake in loc)
+ if(future_pancake.body_position == LYING_DOWN)
+ run_over(future_pancake)
+
+ return ..()
+
+///Checks if the bot is on or if it has charge
+/mob/living/basic/bot/mulebot/proc/on_pre_move()
+ SIGNAL_HANDLER
+
+ if(!(bot_mode_flags & BOT_MODE_ON))
+ return COMPONENT_MOB_BOT_BLOCK_PRE_STEP
+
+ if((cell && (cell.charge < cell_move_power_usage)) || !has_power())
+ turn_off()
+ return COMPONENT_MOB_BOT_BLOCK_PRE_STEP
+
+// when mulebot is in the same loc
+/mob/living/basic/bot/mulebot/proc/run_over(mob/living/carbon/human/crushed)
+ if (!(bot_access_flags & BOT_COVER_EMAGGED) && !wires.is_cut(WIRE_AVOIDANCE))
+ if (!has_status_effect(/datum/status_effect/careful_driving))
+ crushed.visible_message(span_notice("[src] slows down to avoid crushing [crushed]."))
+ apply_status_effect(/datum/status_effect/careful_driving)
+ return // Player mules must be emagged before they can trample
+
+ log_combat(src, crushed, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])")
+ crushed.visible_message(
+ span_danger("[src] drives over [crushed]!"),
+ span_userdanger("[src] drives over you!"),
+ )
+
+ playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
+
+ var/damage = rand(5, 15)
+ var/static/list/zone_damages = list(
+ BODY_ZONE_HEAD = 2,
+ BODY_ZONE_CHEST = 2,
+ BODY_ZONE_L_LEG = 0.5,
+ BODY_ZONE_R_LEG = 0.5,
+ BODY_ZONE_L_ARM = 0.5,
+ BODY_ZONE_R_ARM = 0.5,
+ )
+ for(var/body_zone in zone_damages)
+ crushed.apply_damage(zone_damages[body_zone] * damage, BRUTE, body_zone, run_armor_check(body_zone, MELEE))
+
+ add_mob_blood(crushed)
+
+ var/turf/below_us = get_turf(src)
+ below_us.add_mob_blood(crushed)
+
+ AddComponent(/datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/tracks, \
+ target_dir_change = TRUE, \
+ transfer_blood_dna = TRUE, \
+ max_blood = 4)
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm
new file mode 100644
index 00000000000..9a4b20b9e5a
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_paranormal.dm
@@ -0,0 +1,61 @@
+/mob/living/basic/bot/mulebot/paranormal
+ name = "\improper GHOULbot"
+ desc = "A rather ghastly looking... Multiple Utility Load Effector bot? It only seems to accept paranormal forces, and for this reason is fucking useless."
+ icon_state = "paranormalmulebot0"
+ base_icon_state = "paranormalmulebot"
+ ///avoid the utterly miniscule chance of infinite looping
+ replacement_chance = 0
+
+/mob/living/basic/bot/mulebot/paranormal/update_overlays()
+ . = ..()
+ if(!isobserver(load))
+ return
+ var/mutable_appearance/ghost_overlay = mutable_appearance('icons/mob/simple/mob.dmi', "ghost", layer + 0.01) //use a generic ghost icon, otherwise you can metagame who's dead if they have a custom ghost set
+ ghost_overlay.pixel_y = 12
+ . += ghost_overlay
+
+/mob/living/basic/bot/mulebot/paranormal/get_load_name() //Don't reveal the name of ghosts so we can't metagame who died and all that.
+ . = ..()
+ if(. && isobserver(load))
+ return "Unknown"
+
+/mob/living/basic/bot/mulebot/paranormal/load(atom/movable/movable_atom)
+ if(load || movable_atom.anchored)
+ return
+
+ if(!isturf(movable_atom.loc)) //To prevent the loading from stuff from someone's inventory or screen icons.
+ return
+
+ if(isobserver(movable_atom))
+ visible_message(span_warning("A ghostly figure appears on [src]!"))
+ movable_atom.forceMove(src)
+ RegisterSignal(movable_atom, COMSIG_MOVABLE_MOVED, PROC_REF(ghost_moved))
+
+ else if(!wires.is_cut(WIRE_LOADCHECK))
+ buzz(MULEBOT_MOOD_SIGH)
+ return // if not hacked, only allow ghosts to be loaded
+
+ else if(isobj(movable_atom))
+ if(movable_atom.has_buckled_mobs() || (locate(/mob) in movable_atom)) //can't load non crates objects with mobs buckled to it or inside it.
+ buzz(MULEBOT_MOOD_SIGH)
+ return
+
+ if(istype(movable_atom, /obj/structure/closet/crate))
+ var/obj/structure/closet/crate/crate = movable_atom
+ crate.close() //make sure it's closed
+
+ movable_atom.forceMove(src)
+
+ else if(isliving(movable_atom) && !load_mob(movable_atom))
+ return
+
+ load = movable_atom
+ update_bot_mode(new_mode = BOT_IDLE)
+ update_appearance()
+
+///Handles the ghosts moving out from the mule
+/mob/living/basic/bot/mulebot/paranormal/proc/ghost_moved()
+ SIGNAL_HANDLER
+ visible_message(span_notice("The ghostly figure vanishes..."))
+ UnregisterSignal(load, COMSIG_MOVABLE_MOVED)
+ unload()
diff --git a/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm b/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm
new file mode 100644
index 00000000000..d80cc520bbb
--- /dev/null
+++ b/code/modules/mob/living/basic/bots/mulebot/mulebot_tool_interactions.dm
@@ -0,0 +1,46 @@
+/mob/living/basic/bot/mulebot/screwdriver_act(mob/living/user, obj/item/tool)
+ . = ..()
+ update_appearance()
+
+/mob/living/basic/bot/mulebot/crowbar_act(mob/living/user, obj/item/tool)
+ if(!(bot_access_flags & BOT_COVER_MAINTS_OPEN) || user.combat_mode)
+ return
+ if(!cell)
+ to_chat(user, span_warning("[src] doesn't have a power cell!"))
+ return ITEM_INTERACT_BLOCKING
+ cell.add_fingerprint(user)
+ user.visible_message(
+ span_notice("[user] crowbars [cell] out from [src]."),
+ span_notice("You pry [cell] out of [src]."),
+ )
+ if(Adjacent(user) && !issilicon(user))
+ user.put_in_hands(cell)
+ else
+ cell.forceMove(drop_location())
+ return ITEM_INTERACT_SUCCESS
+
+/mob/living/basic/bot/mulebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
+ if(istype(tool, /obj/item/stock_parts/power_store/cell) && (bot_access_flags & BOT_COVER_MAINTS_OPEN))
+ if(cell)
+ to_chat(user, span_warning("[src] already has a power cell!"))
+ return ITEM_INTERACT_BLOCKING
+ if(!user.transferItemToLoc(tool, src))
+ return ITEM_INTERACT_BLOCKING
+ user.visible_message(
+ span_notice("[user] inserts \a [cell] into [src]."),
+ span_notice("You insert [cell] into [src]."),
+ )
+ return ITEM_INTERACT_SUCCESS
+ if(is_wire_tool(tool) && (bot_access_flags & BOT_COVER_MAINTS_OPEN))
+ attack_hand(user)
+ return ITEM_INTERACT_SUCCESS
+ return ..()
+
+
+/mob/living/basic/bot/mulebot/emag_act(mob/user, obj/item/card/emag/emag_card)
+ . = ..()
+ if(!(bot_access_flags & BOT_COVER_EMAGGED))
+ return
+ flick("[base_icon_state]-emagged", src)
+ playsound(src, SFX_SPARKS, 100, FALSE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
diff --git a/code/modules/mob/living/basic/bots/repairbot/repairbot.dm b/code/modules/mob/living/basic/bots/repairbot/repairbot.dm
index 8cc3a90a336..0e3647adb19 100644
--- a/code/modules/mob/living/basic/bots/repairbot/repairbot.dm
+++ b/code/modules/mob/living/basic/bots/repairbot/repairbot.dm
@@ -250,7 +250,7 @@
var/obj/item/stack/rods/new_rods = new()
new_rods.forceMove(src)
-/mob/living/basic/bot/repairbot/turn_on()
+/mob/living/basic/bot/repairbot/turn_on(mob/user)
. = ..()
if(!.)
return
diff --git a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
index 86849f1ffd9..5202159878b 100644
--- a/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
+++ b/code/modules/mob/living/basic/space_fauna/revenant/revenant_abilities.dm
@@ -175,12 +175,6 @@
// A note to future coders: do not replace this with an EMP because it will wreck malf AIs and everyone will hate you.
/datum/action/cooldown/spell/aoe/revenant/malfunction/cast_on_thing_in_aoe(turf/victim, mob/living/basic/revenant/caster)
- for(var/mob/living/simple_animal/bot/bot in victim)
- if(!(bot.bot_cover_flags & BOT_COVER_EMAGGED))
- new /obj/effect/temp_visual/revenant(bot.loc)
- bot.bot_cover_flags &= ~BOT_COVER_LOCKED
- bot.bot_cover_flags |= BOT_COVER_MAINTS_OPEN
- bot.emag_act(caster)
for(var/mob/living/basic/bot/bot in victim)
if(!(bot.bot_access_flags & BOT_COVER_EMAGGED))
new /obj/effect/temp_visual/revenant(bot.loc)
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index aa960b3444a..0849b20eed9 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -541,13 +541,8 @@
var/mob/living/bot = bot_ref?.resolve()
if(!bot)
return
- var/summon_success
- if(isbasicbot(bot))
- var/mob/living/basic/bot/basic_bot = bot
- summon_success = basic_bot.summon_bot(src, waypoint, grant_all_access = TRUE)
- else
- var/mob/living/simple_animal/bot/simple_bot = bot
- summon_success = simple_bot.call_bot(src, waypoint)
+ var/mob/living/basic/bot/basic_bot = bot
+ var/summon_success = basic_bot.summon_bot(src, waypoint, grant_all_access = TRUE)
var/chat_message = summon_success ? "Sending command to bot..." : "Interface error. Unit is already in use."
to_chat(src, span_notice("[chat_message]"))
diff --git a/code/modules/mob/living/silicon/ai/robot_control.dm b/code/modules/mob/living/silicon/ai/robot_control.dm
index 94c99369b6c..1884804afe4 100644
--- a/code/modules/mob/living/silicon/ai/robot_control.dm
+++ b/code/modules/mob/living/silicon/ai/robot_control.dm
@@ -35,37 +35,22 @@
var/turf/ai_current_turf = get_turf(owner)
data["robots"] = list()
- for(var/mob/living/our_bot as anything in GLOB.bots_list)
+ for(var/mob/living/basic/bot/our_bot as anything in GLOB.bots_list)
if(!isbot(our_bot) || !is_valid_z_level(ai_current_turf, get_turf(our_bot)))
continue
- if(isbasicbot(our_bot))
- var/mob/living/basic/bot/basic_bot = our_bot
- if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- continue
- var/list/basic_bot_data = list(
- name = basic_bot.name,
- model = basic_bot.bot_type,
- mode = basic_bot.mode,
- hacked = !!(basic_bot.bot_access_flags & BOT_COVER_HACKED),
- location = get_area_name(basic_bot, TRUE),
- ref = REF(basic_bot),
- )
- data["robots"] += list(basic_bot_data)
+ var/mob/living/basic/bot/basic_bot = our_bot
+ if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
continue
-
- var/mob/living/simple_animal/bot/simple_bot = our_bot
- if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- continue
- var/list/simple_bot_data = list(
- name = simple_bot.name,
- model = simple_bot.bot_type,
- mode = simple_bot.get_mode(),
- hacked = !!(simple_bot.bot_cover_flags & BOT_COVER_HACKED),
- location = get_area_name(simple_bot, TRUE),
- ref = REF(simple_bot),
+ var/list/basic_bot_data = list(
+ name = basic_bot.name,
+ model = basic_bot.bot_type,
+ mode = basic_bot.mode,
+ hacked = !!(basic_bot.bot_access_flags & BOT_COVER_HACKED),
+ location = get_area_name(basic_bot, TRUE),
+ ref = REF(basic_bot),
)
- data["robots"] += list(simple_bot_data)
+ data["robots"] += list(basic_bot_data)
return data
@@ -84,28 +69,19 @@
switch(action)
if("callbot") //Command a bot to move to a selected location.
- if(isbasicbot(bot))
- var/mob/living/basic/bot/basic_bot = bot
- if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- return
- else
- var/mob/living/simple_animal/bot/simple_bot = bot
- if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- return
+ var/mob/living/basic/bot/basic_bot = bot
+ if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
+ return
owner.bot_ref = WEAKREF(bot)
owner.setting_waypoint = TRUE
to_chat(our_user, span_notice("Set your waypoint by clicking on a valid location free of obstructions."))
if("interface") //Remotely connect to a bot!
owner.bot_ref = WEAKREF(bot)
- if(isbasicbot(bot))
- var/mob/living/basic/bot/basic_bot = bot
- if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- return
- else
- var/mob/living/basic/bot/simple_bot = bot
- if(!(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
- return
+ var/mob/living/basic/bot/basic_bot = bot
+ if(!(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED))
+ return
+
bot.attack_ai(our_user)
return TRUE
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
deleted file mode 100644
index 86f50e6246a..00000000000
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ /dev/null
@@ -1,1231 +0,0 @@
-// AI (i.e. game AI, not the AI player) controlled bots
-/mob/living/simple_animal/bot
- abstract_type = /mob/living/simple_animal/bot
- icon = 'icons/mob/silicon/aibots.dmi'
- layer = MOB_LAYER
- gender = NEUTER
- mob_biotypes = MOB_ROBOTIC
- stop_automated_movement = TRUE
- wander = FALSE
- damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, STAMINA = 0, OXY = 0)
- atmos_requirements = null
- unsuitable_atmos_damage = 0
- hud_possible = list(DIAG_STAT_HUD, DIAG_BOT_HUD, DIAG_HUD, DIAG_BATT_HUD, DIAG_PATH_HUD = HUD_LIST_LIST)
- maxbodytemp = INFINITY
- minbodytemp = 0
- sentience_type = SENTIENCE_ARTIFICIAL
- status_flags = NONE //no default canpush
- pass_flags = PASSFLAPS
- verb_say = "states"
- verb_ask = "queries"
- verb_exclaim = "declares"
- verb_yell = "alarms"
- initial_language_holder = /datum/language_holder/synthetic
- bubble_icon = "machine"
- speech_span = SPAN_ROBOT
- faction = list(FACTION_NEUTRAL, FACTION_SILICON, FACTION_TURRET)
- light_system = OVERLAY_LIGHT
- light_range = 3
- light_power = 0.6
- del_on_death = TRUE
- req_one_access = list(ACCESS_ROBOTICS)
- interaction_flags_click = ALLOW_SILICON_REACH
-
- ///Cooldown between salutations for commissioned bots
- COOLDOWN_DECLARE(next_salute_check)
-
- ///The Robot arm attached to this robot - has a 50% chance to drop on death.
- var/robot_arm = /obj/item/bodypart/arm/right/robot
- ///The inserted (if any) pAI in this bot.
- var/obj/item/pai_card/paicard
- ///The type of bot it is, for radio control.
- var/bot_type = NONE
-
- ///Additonal access given to player-controlled bots.
- var/list/player_access = list()
- ///All initial access this bot started with.
- var/list/prev_access = list()
-
- ///Bot-related mode flags on the Bot indicating how they will act. BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION
- var/bot_mode_flags = BOT_MODE_ON | BOT_MODE_REMOTE_ENABLED | BOT_MODE_CAN_BE_SAPIENT | BOT_MODE_ROUNDSTART_POSSESSION
-
- ///Bot-related cover flags on the Bot to deal with what has been done to their cover, including emagging. BOT_COVER_MAINTS_OPEN | BOT_COVER_LOCKED | BOT_COVER_EMAGGED | BOT_COVER_HACKED
- var/bot_cover_flags = BOT_COVER_LOCKED
-
- ///Small name of what the bot gets messed with when getting hacked/emagged.
- var/hackables = "system circuits"
- ///Used by some bots for tracking failures to reach their target.
- var/frustration = 0
- ///The speed at which the bot moves, or the number of times it moves per process() tick.
- var/base_speed = 2
- ///The end point of a bot's path, or the target location.
- var/turf/ai_waypoint
- ///The bot is on a custom set path.
- var/pathset = FALSE
- ///List of turfs through which a bot 'steps' to reach the waypoint, associated with the path image, if there is one.
- var/list/path = list()
- ///List of unreachable targets for an ignore-list enabled bot to ignore.
- var/list/ignore_list = list()
- ///Standardizes the vars that indicate the bot is busy with its function.
- var/mode = BOT_IDLE
- ///Number of times the bot tried and failed to move.
- var/tries = 0
- ///Links a bot to the AI calling it.
- var/mob/living/silicon/ai/calling_ai
- ///The bot's radio, for speaking to people.
- var/obj/item/radio/internal_radio
- ///which channels can the bot listen to
- var/radio_key = null
- ///The bot's default radio channel
- var/radio_channel = RADIO_CHANNEL_COMMON
- ///Turf a bot is summoned to navitage towards.
- var/turf/patrol_target
- ///Turf of a user summoning a bot towards their location.
- var/turf/summon_target
- ///Pending new destination (waiting for beacon response)
- var/new_destination
- ///Destination description tag
- var/destination
- ///The next destination in the patrol route
- var/next_destination
-
- /// the nearest beacon's tag
- var/nearest_beacon
- ///The nearest beacon's location
- var/turf/nearest_beacon_loc
-
- ///The type of data HUD the bot uses. Diagnostic by default.
- var/data_hud_type = TRAIT_DIAGNOSTIC_HUD
- var/datum/atom_hud/data/bot_path/private/path_hud
- var/path_image_icon = 'icons/mob/silicon/aibots.dmi'
- var/path_image_icon_state = "path_indicator"
- var/path_image_color = COLOR_WHITE
- var/reset_access_timer_id
- var/ignorelistcleanuptimer = 1 // This ticks up every automated action, at 300 we clean the ignore list
-
- /// If true we will allow ghosts to control this mob
- var/can_be_possessed = FALSE
- /// If true we will offer this
- COOLDOWN_DECLARE(offer_ghosts_cooldown)
- /// Message to display upon possession
- var/possessed_message = "You're a generic bot. How did one of these even get made?"
- /// List of strings to sound effects corresponding to automated messages the bot can play
- var/list/automated_announcements
- /// Action we use to say voice lines out loud, also we just pass anything we try to say through here just in case it plays a voice line
- var/datum/action/cooldown/bot_announcement/pa_system
- // The faction of the bot before it inherited the pai's faction
- var/list/original_faction
- // The allies of the bot before it inherited the pai's faction
- var/list/original_allies
-
- ///Innate access uses an internal ID card.
- var/obj/item/card/id/access_card = null
-
-/mob/living/simple_animal/bot/proc/get_mode()
- if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player.
- return paicard ? "pAI Controlled" : "Autonomous"
- if(!(bot_mode_flags & BOT_MODE_ON))
- return span_bad("Inactive")
- return span_average("[mode]")
-
-/**
- * Returns a status string about the bot's current status, if it's moving, manually controlled, or idle.
- */
-/mob/living/simple_animal/bot/proc/get_mode_ui()
- if(client) //Player bots do not have modes, thus the override. Also an easy way for PDA users/AI to know when a bot is a player.
- return paicard ? "pAI Controlled" : "Autonomous"
- if(!(bot_mode_flags & BOT_MODE_ON))
- return "Inactive"
- return "[mode]"
-
-/**
- * Returns a string of flavor text for emagged bots as defined by policy.
- */
-/mob/living/simple_animal/bot/proc/get_emagged_message()
- return get_policy(ROLE_EMAGGED_BOT) || "You are a malfunctioning bot! Disrupt everyone and cause chaos!"
-
-/mob/living/simple_animal/bot/proc/turn_on()
- if(stat)
- return FALSE
- bot_mode_flags |= BOT_MODE_ON
- remove_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), POWER_LACK_TRAIT)
- set_light_on(bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE)
- update_appearance()
- balloon_alert(src, "turned on")
- diag_hud_set_botstat()
- return TRUE
-
-/mob/living/simple_animal/bot/proc/turn_off()
- bot_mode_flags &= ~BOT_MODE_ON
- add_traits(list(TRAIT_INCAPACITATED, TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), POWER_LACK_TRAIT)
- set_light_on(bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE)
- bot_reset() //Resets an AI's call, should it exist.
- balloon_alert(src, "turned off")
- update_appearance()
-
-/mob/living/simple_animal/bot/proc/get_bot_flag(checked_mode, checked_flag)
- if(checked_mode & checked_flag)
- return TRUE
- return FALSE
-
-/mob/living/simple_animal/bot/Initialize(mapload)
- . = ..()
- GLOB.bots_list += src
- add_traits(list(TRAIT_SILICON_ACCESS, TRAIT_REAGENT_SCANNER, TRAIT_UNOBSERVANT), INNATE_TRAIT)
- LoadComponent(/datum/component/bloodysoles/bot)
-
- path_hud = new /datum/atom_hud/data/bot_path/private()
- for(var/hud in path_hud.hud_icons) // You get to see your own path
- set_hud_image_active(hud, exclusive_hud = path_hud)
-
- // Give bots a fancy new ID card that can hold any access.
- access_card = new /obj/item/card/id/advanced/simple_bot(src)
- // This access is so bots can be immediately set to patrol and leave Robotics, instead of having to be let out first.
- access_card.set_access(list(ACCESS_ROBOTICS))
- internal_radio = new /obj/item/radio(src)
- if(radio_key)
- internal_radio.keyslot = new radio_key
- internal_radio.subspace_transmission = TRUE
- internal_radio.canhear_range = 0 // anything greater will have the bot broadcast the channel as if it were saying it out loud.
- internal_radio.recalculateChannels()
-
- //Adds bot to the diagnostic HUD system
- prepare_huds()
- var/datum/atom_hud/data/diagnostic/diag_hud = GLOB.huds[DATA_HUD_DIAGNOSTIC]
- diag_hud.add_atom_to_hud(src)
- diag_hud_set_bothealth()
- diag_hud_set_botstat()
- diag_hud_set_botmode()
-
- //If a bot has its own HUD (for player bots), provide it.
- if(!isnull(data_hud_type))
- ADD_TRAIT(src, data_hud_type, INNATE_TRAIT)
- if(path_hud)
- path_hud.add_atom_to_hud(src)
- path_hud.show_to(src)
-
- if(mapload && is_station_level(z) && bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT && bot_mode_flags & BOT_MODE_ROUNDSTART_POSSESSION)
- enable_possession(mapload = mapload)
-
- pa_system = new(src, automated_announcements = automated_announcements)
- pa_system.Grant(src)
- RegisterSignal(src, COMSIG_MOB_TRIED_ACCESS, PROC_REF(attempt_access))
- ADD_TRAIT(src, TRAIT_SILICON_EMOTES_ALLOWED, INNATE_TRAIT)
-
-/mob/living/simple_animal/bot/Destroy()
- GLOB.bots_list -= src
- QDEL_NULL(paicard)
- QDEL_NULL(pa_system)
- QDEL_NULL(internal_radio)
- QDEL_NULL(access_card)
- QDEL_NULL(path_hud)
- return ..()
-
-/// Allows this bot to be controlled by a ghost, who will become its mind
-/mob/living/simple_animal/bot/proc/enable_possession(user, mapload = FALSE)
- if (paicard)
- balloon_alert(user, "already sapient!")
- return
- can_be_possessed = TRUE
- var/can_announce = !mapload && COOLDOWN_FINISHED(src, offer_ghosts_cooldown)
- AddComponent(
- /datum/component/ghost_direct_control, \
- ban_type = ROLE_BOT, \
- poll_candidates = can_announce, \
- poll_ignore_key = POLL_IGNORE_BOTS, \
- assumed_control_message = (bot_cover_flags & BOT_COVER_EMAGGED) ? get_emagged_message() : possessed_message, \
- extra_control_checks = CALLBACK(src, PROC_REF(check_possession)), \
- after_assumed_control = CALLBACK(src, PROC_REF(post_possession)), \
- )
- if (can_announce)
- COOLDOWN_START(src, offer_ghosts_cooldown, 30 SECONDS)
-
- if (user)
- log_silicon("[key_name(user)] enabled sapience for [src] ([initial(src.name)])") // Not technically a silicon but who is counting
-
-/// Disables this bot from being possessed by ghosts
-/mob/living/simple_animal/bot/proc/disable_possession(mob/user)
- if (user)
- log_silicon("[key_name(user)] disabled sapience for [src] ([initial(src.name)])")
- can_be_possessed = FALSE
- qdel(GetComponent(/datum/component/ghost_direct_control))
- if (isnull(key))
- return
- if (user)
- log_combat(user, src, "ejected [key_name(src)] from control of [src] ([initial(src.name)]).")
- to_chat(src, span_warning("You feel yourself fade as your personality matrix is reset!"))
- ghostize(can_reenter_corpse = FALSE)
- playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
- speak("Personality matrix reset!")
- key = null
-
-/// Returns true if this mob can be controlled
-/mob/living/simple_animal/bot/proc/check_possession(mob/potential_possessor)
- if (!can_be_possessed)
- to_chat(potential_possessor, span_warning("The bot's personality download has been disabled!"))
- return can_be_possessed
-
-/// Fired after something takes control of this mob
-/mob/living/simple_animal/bot/proc/post_possession()
- playsound(src, 'sound/machines/ping.ogg', 30, TRUE)
- speak("New personality installed successfully!")
- rename(src)
-
-/// Allows renaming the bot to something else
-/mob/living/simple_animal/bot/proc/rename(mob/user)
- var/new_name = sanitize_name(
- reject_bad_text(tgui_input_text(
- user = user,
- message = "This machine is designated [real_name]. Would you like to update its registration?",
- title = "Name change",
- default = real_name,
- max_length = MAX_NAME_LEN,
- )),
- allow_numbers = TRUE
- )
- if (isnull(new_name) || QDELETED(src))
- return
- if (key && user != src)
- var/accepted = tgui_alert(
- src,
- message = "Do you wish to be renamed to [new_name]?",
- title = "Name change",
- buttons = list("Yes", "No"),
- )
- if (accepted != "Yes" || QDELETED(src))
- return
- fully_replace_character_name(real_name, new_name)
-
-/mob/living/simple_animal/bot/allowed(mob/living/user)
- if(!(bot_cover_flags & BOT_COVER_LOCKED)) // Unlocked.
- return TRUE
- return ..()
-
-/mob/living/simple_animal/bot/bee_friendly()
- return TRUE
-
-/mob/living/simple_animal/bot/death(gibbed)
- if(paicard)
- ejectpai()
- explode()
- return ..()
-
-/mob/living/simple_animal/bot/proc/explode()
- visible_message(span_boldnotice("[src] blows apart!"))
- do_sparks(3, TRUE, src)
- var/atom/location_destroyed = drop_location()
- if(prob(50))
- drop_part(robot_arm, location_destroyed)
-
-/mob/living/simple_animal/bot/emag_act(mob/user, obj/item/card/emag/emag_card)
- . = ..()
- if(bot_cover_flags & BOT_COVER_LOCKED) //First emag application unlocks the bot's interface. Apply a screwdriver to use the emag again.
- bot_cover_flags &= ~BOT_COVER_LOCKED
- balloon_alert(user, "cover unlocked")
- return TRUE
- if(!(bot_cover_flags & BOT_COVER_LOCKED) && bot_cover_flags & BOT_COVER_MAINTS_OPEN) //Bot panel is unlocked by ID or emag, and the panel is screwed open. Ready for emagging.
- bot_cover_flags |= BOT_COVER_EMAGGED
- bot_cover_flags &= ~BOT_COVER_LOCKED //Manually emagging the bot locks out the panel.
- bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED //Manually emagging the bot also locks the AI from controlling it.
- bot_reset()
- turn_on() //The bot automatically turns on when emagged, unless recently hit with EMP.
- to_chat(src, span_userdanger("(#$*#$^^( OVERRIDE DETECTED"))
- to_chat(src, span_boldnotice(get_emagged_message()))
- if(user)
- log_combat(user, src, "emagged")
- return TRUE
- else //Bot is unlocked, but the maint panel has not been opened with a screwdriver (or through the UI) yet.
- balloon_alert(user, "open maintenance panel first!")
- return FALSE
-
-/mob/living/simple_animal/bot/examine(mob/user)
- . = ..()
- if(health < maxHealth)
- if(health > maxHealth/3)
- . += "[src]'s parts look loose."
- else
- . += "[src]'s parts look very loose!"
- else
- . += "[src] is in pristine condition."
- . += span_notice("[p_Their()] maintenance panel is [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "open" : "closed"].")
- . += span_info("You can use a screwdriver to [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "close" : "open"] it.")
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- . += span_notice("[p_Their()] control panel is [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"].")
- var/is_sillycone = HAS_SILICON_ACCESS(user)
- if(!(bot_cover_flags & BOT_COVER_EMAGGED) && (is_sillycone || user.Adjacent(src)))
- . += span_info("Alt-click [is_sillycone ? "" : "or use your ID on "]it to [bot_cover_flags & BOT_COVER_LOCKED ? "un" : ""]lock [p_their()] control panel.")
- if(paicard)
- . += span_notice("[p_They()] has a pAI device installed.")
- if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- . += span_info("You can use a hemostat to remove it.")
- if(access_card)
- . += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]."
-
-/mob/living/simple_animal/bot/adjustHealth(amount, updating_health = TRUE, forced = FALSE)
- if(amount > 0 && prob(10))
- new /obj/effect/decal/cleanable/blood/oil(loc)
- return ..()
-
-/mob/living/simple_animal/bot/updatehealth()
- ..()
- diag_hud_set_bothealth()
-
-/mob/living/simple_animal/bot/med_hud_set_health()
- return //we use a different hud
-
-/mob/living/simple_animal/bot/med_hud_set_status()
- return //we use a different hud
-
-/mob/living/simple_animal/bot/handle_automated_action() //Master process which handles code common across most bots.
- diag_hud_set_botmode()
-
- if (ignorelistcleanuptimer % 300 == 0) // Every 300 actions, clean up the ignore list from old junk
- for(var/ref in ignore_list)
- var/atom/referredatom = locate(ref)
- if (!referredatom || !istype(referredatom) || QDELETED(referredatom))
- ignore_list -= ref
- ignorelistcleanuptimer = 1
- else
- ignorelistcleanuptimer++
-
- if(!(bot_mode_flags & BOT_MODE_ON) || client)
- return FALSE
-
- if(HAS_TRAIT(src, TRAIT_COMMISSIONED) && COOLDOWN_FINISHED(src, next_salute_check))
- COOLDOWN_START(src, next_salute_check, BOT_COMMISSIONED_SALUTE_DELAY)
- for(var/mob/living/simple_animal/bot/nearby_bot in view(5, src))
- if(!HAS_TRAIT(nearby_bot, TRAIT_COMMISSIONED) && nearby_bot.bot_mode_flags & BOT_MODE_ON)
- manual_emote("performs an elaborate salute for [nearby_bot]!")
- break
-
- switch(mode) //High-priority overrides are processed first. Bots can do nothing else while under direct command.
- if(BOT_RESPONDING) //Called by the AI.
- call_mode()
- return FALSE
- if(BOT_SUMMON) //Called to a location
- summon_step()
- return FALSE
- return TRUE //Successful completion. Used to prevent child process() continuing if this one is ended early.
-
-
-/mob/living/simple_animal/bot/attack_hand(mob/living/carbon/human/user, list/modifiers)
- if(!user.combat_mode)
- ui_interact(user)
- else
- return ..()
-
-/mob/living/simple_animal/bot/attack_ai(mob/user)
- if(!topic_denied(user))
- ui_interact(user)
- else
- to_chat(user, span_warning("[src]'s interface is not responding!"))
-
-/mob/living/simple_animal/bot/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "SimpleBot", name)
- ui.open()
-
-/mob/living/simple_animal/bot/click_alt(mob/user)
- unlock_with_id(user)
- return CLICK_ACTION_SUCCESS
-
-/mob/living/simple_animal/bot/proc/unlock_with_id(mob/user)
- if(bot_cover_flags & BOT_COVER_EMAGGED)
- to_chat(user, span_danger("ERROR"))
- return
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- to_chat(user, span_warning("Please close the access panel before [bot_cover_flags & BOT_COVER_LOCKED ? "un" : ""]locking it."))
- return
- if(!allowed(user))
- to_chat(user, span_warning("Access denied."))
- return
- bot_cover_flags ^= BOT_COVER_LOCKED
- to_chat(user, span_notice("Controls are now [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"]."))
- return TRUE
-
-/mob/living/simple_animal/bot/screwdriver_act(mob/living/user, obj/item/tool)
- if(bot_cover_flags & BOT_COVER_LOCKED)
- to_chat(user, span_warning("The maintenance panel is locked!"))
- return ITEM_INTERACT_SUCCESS
-
- tool.play_tool_sound(src)
- bot_cover_flags ^= BOT_COVER_MAINTS_OPEN
- to_chat(user, span_notice("The maintenance panel is now [bot_cover_flags & BOT_COVER_MAINTS_OPEN ? "opened" : "closed"]."))
- return ITEM_INTERACT_SUCCESS
-
-/mob/living/simple_animal/bot/welder_act(mob/living/user, obj/item/tool)
- user.changeNext_move(CLICK_CD_MELEE)
- if(user.combat_mode)
- return FALSE
-
- if(health >= maxHealth)
- to_chat(user, span_warning("[src] does not need a repair!"))
- return ITEM_INTERACT_SUCCESS
- if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- to_chat(user, span_warning("Unable to repair with the maintenance panel closed!"))
- return ITEM_INTERACT_SUCCESS
-
- if(tool.use_tool(src, user, 0 SECONDS, volume=40))
- adjustHealth(-10)
- user.visible_message(span_notice("[user] repairs [src]!"),span_notice("You repair [src]."))
- return ITEM_INTERACT_SUCCESS
-
-/mob/living/simple_animal/bot/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers)
- if(attacking_item.GetID())
- unlock_with_id(user)
- return
- if(istype(attacking_item, /obj/item/pai_card))
- insertpai(user, attacking_item)
- return
- if(attacking_item.tool_behaviour == TOOL_HEMOSTAT && paicard)
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- balloon_alert(user, "open the access panel!")
- else
- balloon_alert(user, "removing pAI...")
- if(!do_after(user, 3 SECONDS, target = src) || !paicard)
- return
- user.visible_message(span_notice("[user] uses [attacking_item] to pull [paicard] out of [initial(src.name)]!"),span_notice("You pull [paicard] out of [initial(src.name)] with [attacking_item]."))
- ejectpai(user)
- return
- return ..()
-
-/mob/living/simple_animal/bot/attack_effects(damage_done, hit_zone, armor_block, obj/item/attacking_item, mob/living/attacker)
- if(damage_done > 0 && attacking_item.damtype != STAMINA && stat != DEAD)
- do_sparks(5, TRUE, src)
- . = TRUE
- return ..() || .
-
-/mob/living/simple_animal/bot/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit = FALSE)
- . = ..()
- if(prob(25) || . != BULLET_ACT_HIT)
- return
- if(hitting_projectile.damage_type != BRUTE && hitting_projectile.damage_type != BURN)
- return
- if(!hitting_projectile.is_hostile_projectile() || hitting_projectile.damage <= 0)
- return
- do_sparks(5, TRUE, src)
-
-/mob/living/simple_animal/bot/emp_act(severity)
- . = ..()
- if(. & EMP_PROTECT_SELF)
- return
- var/was_on = bot_mode_flags & BOT_MODE_ON ? TRUE : FALSE
- stat |= EMPED
- new /obj/effect/temp_visual/emp(loc)
- if(paicard)
- paicard.emp_act(severity)
- src.visible_message(span_notice("[paicard] is flies out of [initial(src.name)]!"), span_warning("You are forcefully ejected from [initial(src.name)]!"))
- ejectpai()
-
- if (QDELETED(src))
- return
-
- if(bot_mode_flags & BOT_MODE_ON)
- turn_off()
- addtimer(CALLBACK(src, PROC_REF(emp_reset), was_on), severity * 30 SECONDS)
- if(!prob(70/severity))
- return
- if (!length(GLOB.uncommon_roundstart_languages))
- return
- remove_all_languages(source = LANGUAGE_EMP)
- grant_random_uncommon_language(source = LANGUAGE_EMP)
-
-/mob/living/simple_animal/bot/proc/emp_reset(was_on)
- stat &= ~EMPED
- if(was_on)
- turn_on()
-
-/**
- * Pass a message to have the bot say() it, passing through our announcement action to potentially also play a sound.
- * Optionally pass a frequency to say it on the radio.
- */
-/mob/living/simple_animal/bot/proc/speak(message, channel)
- if(!message)
- return
- pa_system.announce(message, channel)
-
-/mob/living/simple_animal/bot/radio(message, list/message_mods = list(), list/spans, language)
- . = ..()
- if(.)
- return
-
- if(message_mods[MODE_HEADSET])
- internal_radio.talk_into(src, message, , spans, language, message_mods)
- return REDUCE_RANGE
- else if(message_mods[RADIO_EXTENSION] == MODE_DEPARTMENT)
- internal_radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods)
- return REDUCE_RANGE
- else if(message_mods[RADIO_EXTENSION] in GLOB.default_radio_channels)
- internal_radio.talk_into(src, message, message_mods[RADIO_EXTENSION], spans, language, message_mods)
- return REDUCE_RANGE
-
-/mob/living/simple_animal/bot/proc/drop_part(obj/item/drop_item, dropzone)
- var/obj/item/item_to_drop
- if(ispath(drop_item))
- item_to_drop = new drop_item(dropzone)
- else
- item_to_drop = drop_item
- item_to_drop.forceMove(dropzone)
-
- if(istype(item_to_drop, /obj/item/stock_parts/power_store/cell))
- var/obj/item/stock_parts/power_store/cell/dropped_cell = item_to_drop
- dropped_cell.charge = 0
-
- else if(istype(item_to_drop, /obj/item/storage))
- var/obj/item/storage/storage_to_drop = item_to_drop
- storage_to_drop.contents = list()
-
- else if(istype(item_to_drop, /obj/item/gun/energy))
- var/obj/item/gun/energy/dropped_gun = item_to_drop
- dropped_gun.cell.charge = 0
- dropped_gun.update_appearance()
-
-//Generalized behavior code, override where needed!
-
-GLOBAL_LIST_EMPTY(scan_typecaches)
-/**
- * Attempt to scan tiles near [src], first by checking adjacent, then if a target is still not found, nearby.
- *
- * scan_types - list (of typepaths) that nearby tiles are being scanned for.
- * old_target - what has already been scanned, and will early return at checkscan.
- * scan_range - how far away from [src] will be scanned, if nothing is found directly adjacent.
- */
-/mob/living/simple_animal/bot/proc/scan(list/scan_types, old_target, scan_range = DEFAULT_SCAN_RANGE)
- var/key = scan_types.Join(",")
- var/list/scan_cache = GLOB.scan_typecaches[key]
- if(!scan_cache)
- scan_cache = typecacheof(scan_types)
- GLOB.scan_typecaches[key] = scan_cache
- if(!get_turf(src))
- return
- // Nicer behavior, ensures we don't conflict with other bots quite so often
- var/list/adjacent = list()
- for(var/turf/to_walk in view(1, src))
- adjacent += to_walk
-
- adjacent = shuffle(adjacent)
-
- var/list/turfs_to_walk = list()
- for(var/turf/victim in view(scan_range, src))
- turfs_to_walk += victim
-
- turfs_to_walk = turfs_to_walk - adjacent
- // Now we prepend adjacent since we want to run those first
- turfs_to_walk = adjacent + turfs_to_walk
-
- for(var/turf/scanned as anything in turfs_to_walk)
- // Check bot is inlined here to save cpu time
- //Is there another bot there? Then let's just skip it so we dont all atack on top of eachother.
- var/bot_found = FALSE
- for(var/mob/living/simple_animal/bot/buddy in scanned.contents)
- if(istype(buddy, type) && (buddy != src))
- bot_found = TRUE
- break
- if(bot_found)
- continue
-
- for(var/atom/thing as anything in scanned)
- if(!scan_cache[thing.type]) //Check that the thing we found is the type we want!
- continue //If not, keep searching!
- if(thing == old_target || (REF(thing) in ignore_list)) //Filter for blacklisted elements, usually unreachable or previously processed oness
- continue
-
- var/scan_result = process_scan(thing) //Some bots may require additional processing when a result is selected.
- if(!isnull(scan_result))
- return scan_result
-
-//When the scan finds a target, run bot specific processing to select it for the next step. Empty by default.
-/mob/living/simple_animal/bot/proc/process_scan(scan_target)
- return scan_target
-
-/mob/living/simple_animal/bot/proc/check_bot(targ)
- var/turf/target_turf = get_turf(targ)
- if(!target_turf)
- return FALSE
- for(var/mob/living/simple_animal/bot/buddy in target_turf.contents)
- if(istype(buddy, type) && (buddy != src))
- return TRUE
- return FALSE
-
-/mob/living/simple_animal/bot/proc/add_to_ignore(subject)
- if(ignore_list.len < 50) //This will help keep track of them, so the bot is always trying to reach a blocked spot.
- ignore_list += REF(subject)
- else //If the list is full, insert newest, delete oldest.
- ignore_list.Cut(1,2)
- ignore_list += REF(subject)
-
-/*
-Movement proc for stepping a bot through a path generated through A-star.
-Pass a positive integer as an argument to override a bot's default speed.
-*/
-/mob/living/simple_animal/bot/proc/bot_move(dest, move_speed)
- if(!dest || !path || path.len == 0) //A-star failed or a path/destination was not set.
- set_path(null)
- return FALSE
- dest = get_turf(dest) //We must always compare turfs, so get the turf of the dest var if dest was originally something else.
- var/turf/last_node = get_turf(path[path.len]) //This is the turf at the end of the path, it should be equal to dest.
- if(get_turf(src) == dest) //We have arrived, no need to move again.
- return TRUE
- else if(dest != last_node) //The path should lead us to our given destination. If this is not true, we must stop.
- set_path(null)
- return FALSE
- var/step_count = move_speed ? move_speed : base_speed //If a value is passed into move_speed, use that instead of the default speed var.
-
- if(step_count >= 1 && tries < BOT_STEP_MAX_RETRIES)
- for(var/step_number in 1 to step_count)
- addtimer(CALLBACK(src, PROC_REF(bot_step)), BOT_STEP_DELAY*(step_number-1))
- else
- return FALSE
- return TRUE
-
-/// Performs a step_towards and increments the path if successful. Returns TRUE if the bot moved and FALSE otherwise.
-/mob/living/simple_animal/bot/proc/bot_step()
- if(!length(path))
- return FALSE
-
- if(SEND_SIGNAL(src, COMSIG_MOB_BOT_PRE_STEP) & COMPONENT_MOB_BOT_BLOCK_PRE_STEP)
- return FALSE
-
- if(!step_towards(src, path[1]))
- tries++
- return FALSE
-
- increment_path()
- tries = 0
- SEND_SIGNAL(src, COMSIG_MOB_BOT_STEP)
- return TRUE
-
-
-/mob/living/simple_animal/bot/proc/check_bot_access()
- if(mode != BOT_SUMMON && mode != BOT_RESPONDING)
- access_card.set_access(prev_access)
-
-/mob/living/simple_animal/bot/proc/call_bot(summoner, turf/waypoint, message = TRUE)
- if(isAI(summoner) && calling_ai && calling_ai != src) //Prevents an override if another AI is controlling this bot.
- return FALSE
-
- bot_reset() //Reset a bot before setting it to call mode.
-
- set_path(get_path_to(src, waypoint, max_distance=200, access = REGION_ACCESS_ALL_STATION))
- calling_ai = summoner //Link the AI to the bot!
- ai_waypoint = waypoint
-
- if(path?.len) //Ensures that a valid path is calculated!
- var/end_area = get_area_name(waypoint)
- if(!(bot_mode_flags & BOT_MODE_ON))
- turn_on() //Saves the AI the hassle of having to activate a bot manually.
- access_card.set_access(REGION_ACCESS_ALL_STATION) //Give the bot all-access while under the AI's command.
- if(client)
- reset_access_timer_id = addtimer(CALLBACK (src, PROC_REF(bot_reset)), 60 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) //if the bot is player controlled, they get the extra access for a limited time
- to_chat(src, span_notice("[span_big("Priority waypoint set by [icon2html(calling_ai, src)] [summoner]. Proceed to [end_area].")]
[path.len-1] meters to destination. You have been granted additional door access for 60 seconds."))
- if(message)
- to_chat(calling_ai, span_notice("[icon2html(src, calling_ai)] [name] called to [end_area]. [path.len-1] meters to destination."))
- pathset = TRUE
- mode = BOT_RESPONDING
- tries = 0
- else
- if(message)
- to_chat(calling_ai, span_danger("Failed to calculate a valid route. Ensure destination is clear of obstructions and within range."))
- calling_ai = null
- set_path(null)
-
- return TRUE
-
-/mob/living/simple_animal/bot/proc/call_mode() //Handles preparing a bot for a call, as well as calling the move proc.
-//Handles the bot's movement during a call.
- var/success = bot_move(ai_waypoint, 3)
- if(!success)
- if(calling_ai)
- to_chat(calling_ai, "[icon2html(src, calling_ai)] [get_turf(src) == ai_waypoint ? span_notice("[src] successfully arrived to waypoint.") : span_danger("[src] failed to reach waypoint.")]")
- calling_ai = null
- bot_reset()
-
-/mob/living/simple_animal/bot/proc/bot_reset()
- if(calling_ai) //Simple notification to the AI if it called a bot. It will not know the cause or identity of the bot.
- to_chat(calling_ai, span_danger("Call command to a bot has been reset."))
- calling_ai = null
- if(reset_access_timer_id)
- deltimer(reset_access_timer_id)
- reset_access_timer_id = null
- set_path(null)
- summon_target = null
- pathset = FALSE
- access_card.set_access(prev_access)
- tries = 0
- mode = BOT_IDLE
- ignore_list = list()
- diag_hud_set_botstat()
- diag_hud_set_botmode()
-
-
-
-
-//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-//Patrol and summon code!
-//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-/mob/living/simple_animal/bot/proc/bot_patrol()
- patrol_step()
- addtimer(CALLBACK(src, PROC_REF(do_patrol)), 0.5 SECONDS)
-
-/mob/living/simple_animal/bot/proc/do_patrol()
- if(mode == BOT_PATROL)
- patrol_step()
-
-/mob/living/simple_animal/bot/proc/start_patrol()
-
- if(tries >= BOT_STEP_MAX_RETRIES) //Bot is trapped, so stop trying to patrol.
- bot_mode_flags &= ~BOT_MODE_AUTOPATROL
- tries = 0
- speak("Unable to start patrol.")
-
- return
-
- if(!(bot_mode_flags & BOT_MODE_AUTOPATROL)) //A bot not set to patrol should not be patrolling.
- mode = BOT_IDLE
- return
-
- if(patrol_target) // has patrol target
- INVOKE_ASYNC(src, PROC_REF(target_patrol))
- else // no patrol target, so need a new one
- speak("Engaging patrol mode.")
- find_patrol_target()
- tries++
- return
-
-/mob/living/simple_animal/bot/proc/target_patrol()
- calc_path() // Find a route to it
- if(!path.len)
- patrol_target = null
- return
- mode = BOT_PATROL
-// perform a single patrol step
-
-/mob/living/simple_animal/bot/proc/patrol_step()
-
- if(client) // In use by player, don't actually move.
- return
-
- if(loc == patrol_target) // reached target
- //Find the next beacon matching the target.
- if(!get_next_patrol_target())
- find_patrol_target() //If it fails, look for the nearest one instead.
- return
-
- else if(path.len > 0 && patrol_target) // valid path
- if(path[1] == loc)
- increment_path()
- return
-
-
- var/moved = bot_move(patrol_target)//step_towards(src, next) // attempt to move
- if(!moved) //Couldn't proceed the next step of the path BOT_STEP_MAX_RETRIES times
- addtimer(CALLBACK(src, PROC_REF(patrol_step_not_moved)), 0.2 SECONDS)
-
- else // no path, so calculate new one
- mode = BOT_START_PATROL
-
-/mob/living/simple_animal/bot/proc/patrol_step_not_moved()
- calc_path()
- if(!length(path))
- find_patrol_target()
- tries = 0
-
-// finds the nearest beacon to self
-/mob/living/simple_animal/bot/proc/find_patrol_target()
- nearest_beacon = null
- new_destination = null
- find_nearest_beacon()
- if(nearest_beacon)
- patrol_target = nearest_beacon_loc
- destination = next_destination
- else
- bot_mode_flags &= ~BOT_MODE_AUTOPATROL
- mode = BOT_IDLE
- speak("Disengaging patrol mode.")
-
-/mob/living/simple_animal/bot/proc/get_next_patrol_target()
- // search the beacon list for the next target in the list.
- for(var/obj/machinery/navbeacon/NB in GLOB.navbeacons["[z]"])
- if(NB.location == next_destination) //Does the Beacon location text match the destination?
- destination = new_destination //We now know the name of where we want to go.
- patrol_target = NB.loc //Get its location and set it as the target.
- next_destination = NB.codes[NAVBEACON_PATROL_NEXT] //Also get the name of the next beacon in line.
- return TRUE
-
-/mob/living/simple_animal/bot/proc/find_nearest_beacon()
- for(var/obj/machinery/navbeacon/NB in GLOB.navbeacons["[z]"])
- var/dist = get_dist(src, NB)
- if(nearest_beacon) //Loop though the beacon net to find the true closest beacon.
- //Ignore the beacon if were are located on it.
- if(dist>1 && dist 1) //Begin the search, save this one for comparison on the next loop.
- nearest_beacon = NB.location
- nearest_beacon_loc = NB.loc
- patrol_target = nearest_beacon_loc
- destination = nearest_beacon
-
-//PDA control. Some bots, especially MULEs, may have more parameters.
-/mob/living/simple_animal/bot/proc/bot_control(command, mob/user, list/user_access = list())
- if(!(bot_mode_flags & BOT_MODE_ON) || bot_cover_flags & BOT_COVER_EMAGGED || !(bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Emagged bots do not respect anyone's authority! Bots with their remote controls off cannot get commands.
- return TRUE //ACCESS DENIED
- if(client)
- bot_control_message(command, user)
- // process control input
- switch(command)
- if("patroloff")
- bot_reset() //HOLD IT!! //OBJECTION!!
- bot_mode_flags &= ~BOT_MODE_AUTOPATROL
-
- if("patrolon")
- bot_mode_flags |= BOT_MODE_AUTOPATROL
-
- if("summon")
- bot_reset()
- summon_target = get_turf(user)
- if(user_access.len != 0)
- access_card.set_access(user_access + prev_access) //Adds the user's access, if any.
- mode = BOT_SUMMON
- speak("Responding.", radio_channel)
- if("ejectpai")
- ejectpairemote(user)
- return
-
-
-/mob/living/simple_animal/bot/proc/bot_control_message(command, user)
- switch(command)
- if("patroloff")
- to_chat(src, "STOP PATROL")
- if("patrolon")
- to_chat(src, "START PATROL")
- if("summon")
- to_chat(src, "PRIORITY ALERT:[user] in [get_area_name(user)]!")
- if("stop")
- to_chat(src, "STOP!")
-
- if("go")
- to_chat(src, "GO!")
-
- if("home")
- to_chat(src, "RETURN HOME!")
- if("ejectpai")
- return
- else
- to_chat(src, span_warning("Unidentified control sequence received:[command]"))
-
-// calculates a path to the current destination
-// given an optional turf to avoid
-/mob/living/simple_animal/bot/proc/calc_path(turf/avoid)
- check_bot_access()
- set_path(get_path_to(src, patrol_target, max_distance=120, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL))
-
-/mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid)
- check_bot_access()
- var/datum/callback/path_complete = CALLBACK(src, PROC_REF(on_summon_path_finish))
- SSpathfinder.pathfind(src, summon_target, max_distance=150, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL, on_finish=list(path_complete))
-
-/mob/living/simple_animal/bot/proc/on_summon_path_finish(list/path)
- set_path(path)
- if(!length(path)) //Cannot reach target. Give up and announce the issue.
- speak("Summon command failed, destination unreachable.",radio_channel)
- bot_reset()
-
-/mob/living/simple_animal/bot/proc/summon_step()
-
- if(client) // In use by player, don't actually move.
- return
-
- if(loc == summon_target) // Arrived to summon location.
- bot_reset()
- return
-
- else if(path.len > 0 && summon_target) //Proper path acquired!
- if(path[1] == loc)
- increment_path()
- return
-
- var/moved = bot_move(summon_target, 3) // Move attempt
- if(!moved)
- addtimer(CALLBACK(src, PROC_REF(summon_step_not_moved)), 0.2 SECONDS)
-
- else // no path, so calculate new one
- calc_summon_path()
-
-/mob/living/simple_animal/bot/proc/summon_step_not_moved()
- calc_summon_path()
- tries = 0
-
-/mob/living/simple_animal/bot/proc/attempt_access(mob/bot, obj/door_attempt)
- SIGNAL_HANDLER
-
- if(door_attempt.check_access(access_card))
- frustration = 0
- return ACCESS_ALLOWED
- return ACCESS_DISALLOWED
-
-/mob/living/simple_animal/bot/ui_data(mob/user)
- var/list/data = list()
- data["can_hack"] = HAS_SILICON_ACCESS(user)
- data["custom_controls"] = list()
- data["emagged"] = bot_cover_flags & BOT_COVER_EMAGGED
- data["has_access"] = allowed(user)
- data["locked"] = bot_cover_flags & BOT_COVER_LOCKED
- data["settings"] = list()
- if(!(bot_cover_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user))
- data["settings"]["pai_inserted"] = !!paicard
- data["settings"]["allow_possession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT
- data["settings"]["possession_enabled"] = can_be_possessed
- data["settings"]["airplane_mode"] = !(bot_mode_flags & BOT_MODE_REMOTE_ENABLED)
- data["settings"]["maintenance_lock"] = !(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- data["settings"]["power"] = bot_mode_flags & BOT_MODE_ON
- data["settings"]["patrol_station"] = bot_mode_flags & BOT_MODE_AUTOPATROL
- return data
-
-// Actions received from TGUI
-/mob/living/simple_animal/bot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- if(.)
- return
- var/mob/user = ui.user
- if(!allowed(user))
- to_chat(user, span_warning("Access denied."))
- return
-
- if(action == "lock")
- bot_cover_flags ^= BOT_COVER_LOCKED
-
- switch(action)
- if("power")
- if(bot_mode_flags & BOT_MODE_ON)
- turn_off()
- else
- turn_on()
- if("maintenance")
- bot_cover_flags ^= BOT_COVER_MAINTS_OPEN
- if("patrol")
- bot_mode_flags ^= BOT_MODE_AUTOPATROL
- bot_reset()
- if("airplane")
- bot_mode_flags ^= BOT_MODE_REMOTE_ENABLED
- if("hack")
- if(!HAS_SILICON_ACCESS(user))
- return
- if(!(bot_cover_flags & BOT_COVER_EMAGGED))
- bot_cover_flags |= (BOT_COVER_EMAGGED|BOT_COVER_HACKED|BOT_COVER_LOCKED)
- to_chat(user, span_warning("You overload [src]'s [hackables]."))
- message_admins("Safety lock of [ADMIN_LOOKUPFLW(src)] was disabled by [ADMIN_LOOKUPFLW(user)] in [ADMIN_VERBOSEJMP(src)]")
- user.log_message("disabled safety lock of [src]", LOG_GAME)
- bot_reset()
- to_chat(src, span_userdanger("(#$*#$^^( OVERRIDE DETECTED"))
- to_chat(src, span_boldnotice(get_emagged_message()))
- return
- if(!(bot_cover_flags & BOT_COVER_HACKED))
- to_chat(user, span_bolddanger("You fail to repair [src]'s [hackables]."))
- return
- bot_cover_flags &= ~(BOT_COVER_EMAGGED|BOT_COVER_HACKED)
- to_chat(user, span_notice("You reset the [src]'s [hackables]."))
- user.log_message("re-enabled safety lock of [src]", LOG_GAME)
- bot_reset()
- to_chat(src, span_userdanger("Software restored to standard."))
- to_chat(src, span_boldnotice(possessed_message))
- if("eject_pai")
- if(!paicard)
- return
- to_chat(user, span_notice("You eject [paicard] from [initial(src.name)]."))
- ejectpai(user)
- if("toggle_personality")
- if (can_be_possessed)
- disable_possession(user)
- else
- enable_possession(user)
- if("rename")
- rename(user)
-
-/mob/living/simple_animal/bot/update_icon_state()
- icon_state = "[isnull(base_icon_state) ? initial(icon_state) : base_icon_state][get_bot_flag(bot_mode_flags, BOT_MODE_ON)]"
- return ..()
-
-/// Access check proc for bot topics! Remember to place in a bot's individual Topic if desired.
-/mob/living/simple_animal/bot/proc/topic_denied(mob/user)
- if(!user.can_perform_action(src, ALLOW_SILICON_REACH))
- return TRUE
- // 0 for access, 1 for denied.
- if(bot_cover_flags & BOT_COVER_EMAGGED) //An emagged bot cannot be controlled by humans, silicons can if one hacked it.
- if(!(bot_cover_flags & BOT_COVER_HACKED)) //Manually emagged by a human - access denied to all.
- return TRUE
- else if(!HAS_SILICON_ACCESS(user)) //Bot is hacked, so only silicons and admins are allowed access.
- return TRUE
- return FALSE
-
-/// Places a pAI in control of this mob
-/mob/living/simple_animal/bot/proc/insertpai(mob/user, obj/item/pai_card/card)
- if(paicard)
- balloon_alert(user, "slot occupied!")
- return
- if(key)
- balloon_alert(user, "personality already present!")
- return
- if(bot_cover_flags & BOT_COVER_LOCKED || !(bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- balloon_alert(user, "slot inaccessible!")
- return
- if(!(bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT))
- balloon_alert(user, "incompatible firmware!")
- return
- if(!card.pai || !card.pai.mind)
- balloon_alert(user, "pAI is inactive!")
- return
- if(!user.transferItemToLoc(card, src))
- return
- paicard = card
- disable_possession()
- if(paicard.pai.holoform)
- paicard.pai.fold_in()
- copy_languages(paicard.pai, source_override = LANGUAGE_PAI)
- set_active_language(paicard.pai.get_selected_language())
- user.visible_message(span_notice("[user] inserts [card] into [src]!"), span_notice("You insert [card] into [src]."))
- paicard.pai.mind.transfer_to(src)
- to_chat(src, span_notice("You sense your form change as you are uploaded into [src]."))
- name = paicard.pai.name
- original_faction = get_faction()
- original_allies = allies
- SET_FACTION_AND_ALLIES_FROM(src, user)
- log_combat(user, paicard.pai, "uploaded to [initial(src.name)],")
- return TRUE
-
-/mob/living/simple_animal/bot/ghost()
- if(stat != DEAD) // Only ghost if we're doing this while alive, the pAI probably isn't dead yet.
- return ..()
- if(paicard && (!client || stat == DEAD))
- ejectpai()
-
-/// Ejects a pAI from this bot
-/mob/living/simple_animal/bot/proc/ejectpai(mob/user = null, announce = TRUE)
- if(!paicard)
- return
- if(mind && paicard.pai)
- mind.transfer_to(paicard.pai)
- else if(paicard.pai)
- paicard.pai.PossessByPlayer(key)
- else
- ghostize(FALSE) // The pAI card that just got ejected was dead.
- key = null
- paicard.forceMove(loc)
- if(user)
- log_combat(user, paicard.pai, "ejected from [initial(src.name)],")
- else
- log_combat(src, paicard.pai, "ejected")
- if(announce)
- to_chat(paicard.pai, span_notice("You feel your control fade as [paicard] ejects from [initial(src.name)]."))
- paicard = null
- name = initial(src.name)
- set_faction(original_faction)
- set_allies(original_allies)
- remove_all_languages(source = LANGUAGE_PAI)
- get_selected_language()
-
-/// Ejects the pAI remotely.
-/mob/living/simple_animal/bot/proc/ejectpairemote(mob/user)
- if(!allowed(user) || !paicard)
- return
- speak("Ejecting personality chip.", radio_channel)
- ejectpai(user)
-
-/mob/living/simple_animal/bot/Login()
- . = ..()
- if(!. || !client)
- return FALSE
- // If we have any bonus player accesses, add them to our internal ID card.
- if(length(player_access))
- access_card.add_access(player_access)
- diag_hud_set_botmode()
-
-/mob/living/simple_animal/bot/Logout()
- . = ..()
- bot_reset()
-
-/mob/living/simple_animal/bot/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
- . = ..()
- if(!.)
- return
- update_appearance()
-
-/mob/living/simple_animal/bot/sentience_act()
- remove_faction(FACTION_SILICON)
-
-/mob/living/simple_animal/bot/proc/set_path(list/newpath)
- path = newpath ? newpath : list()
- if(!path_hud)
- return
- var/list/path_huds_watching_me = list(GLOB.huds[DATA_HUD_DIAGNOSTIC], GLOB.huds[DATA_HUD_BOT_PATH])
- if(path_hud)
- path_huds_watching_me += path_hud
- for(var/datum/atom_hud/hud as anything in path_huds_watching_me)
- hud.remove_atom_from_hud(src)
-
- var/list/path_images = active_hud_list[DIAG_PATH_HUD]
- LAZYCLEARLIST(path_images)
- if(length(newpath))
- var/mutable_appearance/path_image = mutable_appearance(path_image_icon, path_image_icon_state, BOT_PATH_LAYER, appearance_flags = RESET_COLOR|RESET_TRANSFORM|KEEP_APART)
- path_image.color = path_image_color
- for(var/i in 1 to newpath.len)
- var/turf/T = newpath[i]
- if(T == loc) //don't bother putting an image if it's where we already exist.
- continue
- var/direction = get_dir(src, T)
- if(i > 1)
- var/turf/prevT = path[i - 1]
- var/image/prevI = path[prevT]
- direction = get_dir(prevT, T)
- if(i > 2 && prevI) // make sure we actually have an image to manipulate at index > 2
- var/turf/prevprevT = path[i - 2]
- var/prevDir = get_dir(prevprevT, prevT)
- var/mixDir = direction|prevDir
- if(ISDIAGONALDIR(mixDir))
- prevI.dir = mixDir
- if(prevDir & (NORTH|SOUTH))
- var/matrix/ntransform = matrix()
- ntransform.Turn(90)
- if((mixDir == NORTHWEST) || (mixDir == SOUTHEAST))
- ntransform.Scale(-1, 1)
- else
- ntransform.Scale(1, -1)
- prevI.transform = ntransform
-
- SET_PLANE(path_image, GAME_PLANE, T)
- path_image.dir = direction
- var/image/I = image(loc = T)
- I.appearance = path_image
- path[T] = I
- path_images += I
-
- for(var/datum/atom_hud/hud as anything in path_huds_watching_me)
- hud.add_atom_to_hud(src)
-
-/mob/living/simple_animal/bot/proc/increment_path()
- if(!length(path))
- return
- var/image/I = path[path[1]]
- if(I)
- animate(I, alpha = 0, time = 3)
- path.Cut(1, 2)
-
- if(!length(path))
- addtimer(CALLBACK(src, PROC_REF(set_path), null), 0.6 SECONDS) // Enough time for the animate to finish
-
-/mob/living/simple_animal/bot/rust_heretic_act()
- adjust_brute_loss(400)
-
-/mob/living/simple_animal/bot/get_hit_area_message(input_area)
- // we just get hit, there's no complexity for hitting an arm (if it exists) or anything.
- // we also need to return an empty string as otherwise it would falsely say that we get hit in the chest or something strange like that (bots don't have "chests")
- return ""
-
-//Will always check hands first, because access_card is internal to the mob and can't be removed or swapped.
-/mob/living/simple_animal/bot/get_idcard(hand_first)
- return (..() || access_card)
diff --git a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
index 0a9f8d8d59c..10161dddfd9 100644
--- a/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot_announcement.dm
@@ -51,7 +51,7 @@
/datum/action/cooldown/bot_announcement/ui_data(mob/user)
var/list/data = list()
- var/mob/living/simple_animal/bot/bot_owner = owner
+ var/mob/living/basic/bot/bot_owner = owner
if(istype(bot_owner))
var/list/channels = list()
for(var/channel in bot_owner.internal_radio.channels)
@@ -149,7 +149,7 @@
/// Speak the provided line on the provided radio channel
/datum/action/cooldown/bot_announcement/proc/announce(line, channel)
- var/mob/living/simple_animal/bot/bot_owner = owner
+ var/mob/living/basic/bot/bot_owner = owner
if (!(bot_owner.bot_mode_flags & BOT_MODE_ON))
return
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
deleted file mode 100644
index 5095a1e4baa..00000000000
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ /dev/null
@@ -1,873 +0,0 @@
-
-
-// Mulebot - carries crates around for Quartermaster
-// Navigates via floor navbeacons
-// Remote Controlled from QM's PDA
-
-#define SIGH 0
-#define ANNOYED 1
-#define DELIGHT 2
-#define CHIME 3
-
-/mob/living/simple_animal/bot/mulebot
- name = "\improper MULEbot"
- desc = "A Multiple Utility Load Effector bot."
- icon_state = "mulebot0"
- light_color = "#ffcc99"
- light_power = 0.8
- density = TRUE
- move_resist = MOVE_FORCE_STRONG
- animate_movement = SLIDE_STEPS
- health = 50
- maxHealth = 50
- speed = 3
- damage_coeff = list(BRUTE = 0.5, BURN = 0.7, TOX = 0, STAMINA = 0, OXY = 0)
- combat_mode = TRUE //No swapping
- buckle_lying = 0
- mob_size = MOB_SIZE_LARGE
- buckle_prevents_pull = TRUE // No pulling loaded shit
- bot_mode_flags = ~BOT_MODE_ROUNDSTART_POSSESSION
- req_one_access = list(ACCESS_ROBOTICS, ACCESS_CARGO)
- radio_key = /obj/item/encryptionkey/headset_cargo
- radio_channel = RADIO_CHANNEL_SUPPLY
- bot_type = MULE_BOT
- path_image_color = "#7F5200"
- possessed_message = "You are a MULEbot! Do your best to make sure that packages get to their destination!"
-
- /// unique identifier in case there are multiple mulebots.
- var/id
-
- var/base_icon = "mulebot" /// icon_state to use in update_icon_state
- var/atom/movable/load /// what we're transporting
- var/mob/living/passenger /// who's riding us
- var/turf/target /// this is turf to navigate to (location of beacon)
- var/loaddir = 0 /// this the direction to unload onto/load from
- var/home_destination = "" /// tag of home delivery beacon
-
- var/reached_target = TRUE ///true if already reached the target
- ///Number of times retried a blocked path
- var/blockcount = 0
-
- ///flags of mulebot mode
- var/mulebot_delivery_flags = MULEBOT_RETURN_MODE | MULEBOT_AUTO_PICKUP_MODE | MULEBOT_REPORT_DELIVERY_MODE
-
- var/obj/item/stock_parts/power_store/cell /// Internal Powercell
- var/cell_move_power_usage = 1///How much power we use when we move.
- var/num_steps = 0 ///The amount of steps we should take until we rest for a time.
-
-
-
-/mob/living/simple_animal/bot/mulebot/Initialize(mapload)
- . = ..()
-
- RegisterSignal(src, COMSIG_MOB_BOT_PRE_STEP, PROC_REF(check_pre_step))
- RegisterSignal(src, COMSIG_MOB_CLIENT_PRE_MOVE, PROC_REF(check_pre_step))
- RegisterSignal(src, COMSIG_MOB_BOT_STEP, PROC_REF(on_bot_step))
- RegisterSignal(src, COMSIG_MOB_CLIENT_MOVED, PROC_REF(on_bot_step))
-
- ADD_TRAIT(src, TRAIT_NOMOBSWAP, INNATE_TRAIT)
-
- if(prob(0.666) && mapload)
- new /mob/living/simple_animal/bot/mulebot/paranormal(loc)
- return INITIALIZE_HINT_QDEL
- set_wires(new /datum/wires/mulebot(src))
-
- // Doing this hurts my soul, but simplebot access reworks are for another day.
- var/datum/id_trim/job/cargo_trim = SSid_access.trim_singletons_by_path[/datum/id_trim/job/cargo_technician]
- access_card.add_access(cargo_trim.access + cargo_trim.wildcard_access)
- prev_access = access_card.access.Copy()
-
- cell = new /obj/item/stock_parts/power_store/cell/upgraded(src, 2000)
-
- AddElement(/datum/element/ridable, /datum/component/riding/creature/mulebot)
- diag_hud_set_mulebotcell()
-
- set_id(suffix || assign_random_name())
- suffix = null
- if(name == "\improper MULEbot")
- name = "\improper MULEbot [id]"
- set_home(get_turf(src))
-
-/mob/living/simple_animal/bot/mulebot/Exited(atom/movable/gone, direction)
- . = ..()
- if(gone == load)
- unload(0)
- if(gone == cell)
- turn_off()
- cell = null
- diag_hud_set_mulebotcell()
-
-/mob/living/simple_animal/bot/mulebot/examine(mob/user)
- . = ..()
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- if(cell)
- . += span_notice("[p_They()] [p_have()] \a [cell] installed.")
- . += span_info("You can use a crowbar to remove it.")
- else
- . += span_notice("[p_They()] [p_have()] an empty compartment where a power cell can be installed.")
- if(load) //observer check is so we don't show the name of the ghost that's sitting on it to prevent metagaming who's ded.
- . += span_notice("\A [isobserver(load) ? "ghostly figure" : load] is on [p_their()] load platform.")
-
-
-/mob/living/simple_animal/bot/mulebot/Destroy()
- UnregisterSignal(src, list(COMSIG_MOB_BOT_PRE_STEP, COMSIG_MOB_CLIENT_PRE_MOVE, COMSIG_MOB_BOT_STEP, COMSIG_MOB_CLIENT_MOVED))
- unload(0)
- QDEL_NULL(cell)
- return ..()
-
-/mob/living/simple_animal/bot/mulebot/get_cell()
- return cell
-
-/mob/living/simple_animal/bot/mulebot/turn_on()
- if(!has_power())
- return
- return ..()
-
-/// returns true if the bot is fully powered.
-/mob/living/simple_animal/bot/mulebot/proc/has_power()
- return cell && cell.charge > 0 && (!wires.is_cut(WIRE_POWER1) && !wires.is_cut(WIRE_POWER2))
-
-/mob/living/simple_animal/bot/mulebot/attack_hand(mob/living/carbon/human/user, list/modifiers)
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN && !HAS_AI_ACCESS(user))
- wires.interact(user)
- return
- if(wires.is_cut(WIRE_RX) && HAS_AI_ACCESS(user))
- return
-
- return ..()
-
-/mob/living/simple_animal/bot/mulebot/proc/set_id(new_id)
- id = new_id
-
-/mob/living/simple_animal/bot/mulebot/proc/set_home(turf/home_loc)
- if(!istype(home_loc))
- CRASH("MULEbot [id] was requested to set a home location to [home_loc ? "an invalid home loc ([home_loc.type])" : "null"]")
-
- var/obj/machinery/navbeacon/home_beacon = locate() in home_loc
- if(!isnull(home_beacon))
- home_destination = home_beacon.location
- log_transport("[id]: MULEbot successfuly set home location to ID [home_destination] at [home_beacon.x], [home_beacon.y], [home_beacon.z]")
- return
-
- log_transport("[id]: MULEbot failed to set home at [home_loc.x], [home_loc.y], [home_loc.z]")
-
-/mob/living/simple_animal/bot/mulebot/bot_reset()
- ..()
- reached_target = FALSE
-
-/mob/living/simple_animal/bot/mulebot/screwdriver_act(mob/living/user, obj/item/tool)
- . = ..()
- update_appearance()
-
-/mob/living/simple_animal/bot/mulebot/crowbar_act(mob/living/user, obj/item/tool)
- if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN) || user.combat_mode)
- return
- if(!cell)
- to_chat(user, span_warning("[src] doesn't have a power cell!"))
- return ITEM_INTERACT_BLOCKING
- cell.add_fingerprint(user)
- user.visible_message(
- span_notice("[user] crowbars [cell] out from [src]."),
- span_notice("You pry [cell] out of [src]."),
- )
- if(Adjacent(user) && !issilicon(user))
- user.put_in_hands(cell)
- else
- cell.forceMove(drop_location())
- return ITEM_INTERACT_SUCCESS
-
-/mob/living/simple_animal/bot/mulebot/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
- if(istype(tool, /obj/item/stock_parts/power_store/cell) && (bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- if(cell)
- to_chat(user, span_warning("[src] already has a power cell!"))
- return ITEM_INTERACT_BLOCKING
- if(!user.transferItemToLoc(tool, src))
- return ITEM_INTERACT_BLOCKING
- cell = tool
- diag_hud_set_mulebotcell()
- user.visible_message(
- span_notice("[user] inserts \a [cell] into [src]."),
- span_notice("You insert [cell] into [src]."),
- )
- return ITEM_INTERACT_SUCCESS
- if(is_wire_tool(tool) && (bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- attack_hand(user)
- return ITEM_INTERACT_SUCCESS
- return ..()
-
-/mob/living/simple_animal/bot/mulebot/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers)
- . = ..()
- if(ismob(load) && prob(1 + attacking_item.force * 2))
- user.visible_message(
- span_danger("[user] knocks [load] off [src] with \the [attacking_item]!"),
- span_danger("You knock [load] off [src] with \the [attacking_item]!"),
- )
- unload(0)
- return TRUE
-
-/mob/living/simple_animal/bot/mulebot/emag_act(mob/user, obj/item/card/emag/emag_card)
- if(!(bot_cover_flags & BOT_COVER_EMAGGED))
- bot_cover_flags |= BOT_COVER_EMAGGED
- if(!(bot_cover_flags & BOT_COVER_MAINTS_OPEN))
- bot_cover_flags ^= BOT_COVER_LOCKED
- balloon_alert(user, "controls [bot_cover_flags & BOT_COVER_LOCKED ? "locked" : "unlocked"]")
- flick("[base_icon]-emagged", src)
- playsound(src, SFX_SPARKS, 100, FALSE, SHORT_RANGE_SOUND_EXTRARANGE)
- return TRUE
-
-/mob/living/simple_animal/bot/mulebot/update_icon_state() //if you change the icon_state names, please make sure to update /datum/wires/mulebot/on_pulse() as well. <3
- . = ..()
- icon_state = "[base_icon][(bot_mode_flags & BOT_MODE_ON) ? wires.is_cut(WIRE_AVOIDANCE) : 0]"
-
-/mob/living/simple_animal/bot/mulebot/update_overlays()
- . = ..()
- if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- . += "[base_icon]-hatch"
- if(!load || ismob(load)) //mob offsets and such are handled by the riding component / buckling
- return
- var/mutable_appearance/load_overlay = mutable_appearance(load.icon, load.icon_state, layer + 0.01)
- load_overlay.pixel_z = initial(load.pixel_z) + 11
- . += load_overlay
-
-/mob/living/simple_animal/bot/mulebot/ex_act(severity)
- unload(0)
- switch(severity)
- if(EXPLODE_DEVASTATE)
- qdel(src)
- if(EXPLODE_HEAVY)
- wires.cut_random()
- wires.cut_random()
- if(EXPLODE_LIGHT)
- wires.cut_random()
-
- return TRUE
-
-
-/mob/living/simple_animal/bot/mulebot/bullet_act(obj/projectile/proj)
- . = ..()
- if(. && !QDELETED(src)) //Got hit and not blown up yet.
- if(prob(50) && !isnull(load))
- unload(0)
- if(prob(25))
- visible_message(span_danger("Something shorts out inside [src]!"))
- wires.cut_random(source = proj.firer)
-
-/mob/living/simple_animal/bot/mulebot/ui_interact(mob/user, datum/tgui/ui)
- ui = SStgui.try_update_ui(user, src, ui)
- if(!ui)
- ui = new(user, src, "Mule", name)
- ui.open()
-
-/mob/living/simple_animal/bot/mulebot/ui_data(mob/user)
- var/list/data = list()
- data["powerStatus"] = bot_mode_flags & BOT_MODE_ON
- data["locked"] = bot_cover_flags & BOT_COVER_LOCKED
- data["siliconUser"] = HAS_SILICON_ACCESS(user)
- data["mode"] = mode ? "[mode]" : "Ready"
- data["modeStatus"] = ""
- switch(mode)
- if(BOT_IDLE, BOT_DELIVER, BOT_GO_HOME)
- data["modeStatus"] = "good"
- if(BOT_BLOCKED, BOT_NAV, BOT_WAIT_FOR_NAV)
- data["modeStatus"] = "average"
- if(BOT_NO_ROUTE)
- data["modeStatus"] = "bad"
- data["load"] = get_load_name()
- data["destination"] = destination
- data["homeDestination"] = home_destination
- data["destinationsList"] = GLOB.deliverybeacontags
- data["cellPercent"] = cell?.percent()
- data["autoReturn"] = mulebot_delivery_flags & MULEBOT_RETURN_MODE
- data["autoPickup"] = mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE
- data["reportDelivery"] = mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE
- data["botId"] = id
- data["allowPossession"] = bot_mode_flags & BOT_MODE_CAN_BE_SAPIENT
- data["possessionEnabled"] = can_be_possessed
- data["paiInserted"] = !!paicard
- return data
-
-/mob/living/simple_animal/bot/mulebot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
- . = ..()
- var/mob/user = ui.user
- if(. || (bot_cover_flags & BOT_COVER_LOCKED && !HAS_SILICON_ACCESS(user)))
- return
-
- switch(action)
- if("lock")
- if(HAS_SILICON_ACCESS(user))
- bot_cover_flags ^= BOT_COVER_LOCKED
- return TRUE
- if("on")
- if(bot_mode_flags & BOT_MODE_ON)
- turn_off()
- else if(bot_cover_flags & BOT_COVER_MAINTS_OPEN)
- to_chat(user, span_warning("[name]'s maintenance panel is open!"))
- return
- else if(cell)
- if(!turn_on())
- to_chat(user, span_warning("You can't switch on [src]!"))
- return
- return TRUE
- else
- bot_control(action, user, params) // Kill this later. // Kill PDAs in general please
- return TRUE
-
-/mob/living/simple_animal/bot/mulebot/bot_control(command, mob/user, list/params = list(), pda = FALSE)
- if(pda && wires.is_cut(WIRE_RX)) // MULE wireless is controlled by wires.
- return
-
- switch(command)
- if("stop")
- if(mode != BOT_IDLE)
- bot_reset()
- if("go")
- if(mode == BOT_IDLE)
- start()
- if("home")
- if(mode == BOT_IDLE || mode == BOT_DELIVER)
- start_home()
- if("destination")
- var/new_dest
- if(pda)
- new_dest = tgui_input_list(user, "Enter Destination", "Mulebot Settings", GLOB.deliverybeacontags, destination)
- else
- new_dest = params["value"]
- if(new_dest)
- set_destination(new_dest)
- if("setid")
- var/new_id = tgui_input_text(user, "Enter ID", "ID Assignment", id, max_length = MAX_NAME_LEN)
- if(new_id)
- set_id(new_id)
- name = "\improper MULEbot [new_id]"
- if("sethome")
- var/new_home = tgui_input_list(user, "Enter Home", "Mulebot Settings", GLOB.deliverybeacontags, home_destination)
- if(new_home)
- home_destination = new_home
- if("unload")
- if(load && mode != BOT_HUNT)
- if(loc == target)
- unload(loaddir)
- else
- unload(0)
- if("autoret")
- mulebot_delivery_flags ^= MULEBOT_RETURN_MODE
- if("autopick")
- mulebot_delivery_flags ^= MULEBOT_AUTO_PICKUP_MODE
- if("report")
- mulebot_delivery_flags ^= MULEBOT_REPORT_DELIVERY_MODE
-
-/mob/living/simple_animal/bot/mulebot/proc/buzz(type)
- switch(type)
- if(SIGH)
- audible_message(span_hear("[src] makes a sighing buzz."))
- playsound(src, 'sound/machines/buzz/buzz-sigh.ogg', 50, FALSE)
- if(ANNOYED)
- audible_message(span_hear("[src] makes an annoyed buzzing sound."))
- playsound(src, 'sound/machines/buzz/buzz-two.ogg', 50, FALSE)
- if(DELIGHT)
- audible_message(span_hear("[src] makes a delighted ping!"))
- playsound(src, 'sound/machines/ping.ogg', 50, FALSE)
- if(CHIME)
- audible_message(span_hear("[src] makes a chiming sound!"))
- playsound(src, 'sound/machines/chime.ogg', 50, FALSE)
- flick("[base_icon]1", src)
-
-
-// mousedrop a crate to load the bot
-// can load anything if hacked
-/mob/living/simple_animal/bot/mulebot/mouse_drop_receive(atom/movable/AM, mob/user, params)
- if(!isliving(user))
- return
-
- if(!istype(AM) || isdead(AM) || iseyemob(AM) || istype(AM, /obj/effect/dummy/phased_mob))
- return
-
- load(AM)
-
-// called to load a crate
-/mob/living/simple_animal/bot/mulebot/proc/load(atom/movable/AM)
- if(load || AM.anchored)
- return
-
- if(!isturf(AM.loc)) //To prevent the loading from stuff from someone's inventory or screen icons.
- return
-
- var/obj/structure/closet/crate/crate = AM
- if(!istype(crate))
- if(!wires.is_cut(WIRE_LOADCHECK))
- buzz(SIGH)
- return // if not hacked, only allow crates to be loaded
- crate = null
-
- if(crate || isobj(AM))
- var/obj/O = AM
- if(O.has_buckled_mobs() || (locate(/mob) in AM)) //can't load non crates objects with mobs buckled to it or inside it.
- buzz(SIGH)
- return
-
- if(crate)
- crate.close() //make sure the crate is closed
-
- O.forceMove(src)
-
- else if(isliving(AM))
- if(!load_mob(AM)) //forceMove() is handled in buckling
- return
-
- load = AM
- mode = BOT_IDLE
- update_appearance()
-
-///resolves the name to display for the loaded mob. primarily needed for the paranormal subtype since we don't want to show the name of ghosts riding it.
-/mob/living/simple_animal/bot/mulebot/proc/get_load_name()
- return load ? load.name : null
-
-/mob/living/simple_animal/bot/mulebot/proc/load_mob(mob/living/M)
- can_buckle = TRUE
- if(buckle_mob(M))
- passenger = M
- load = M
- can_buckle = FALSE
- return TRUE
-
-/mob/living/simple_animal/bot/mulebot/post_unbuckle_mob(mob/living/M)
- load = null
- return ..()
-
-// called to unload the bot
-// argument is optional direction to unload
-// if zero, unload at bot's location
-/mob/living/simple_animal/bot/mulebot/proc/unload(dirn)
- if(QDELETED(load))
- if(load) //if our thing was qdel'd, there's likely a leftover reference. just clear it and remove the overlay. we'll let the bot keep moving around to prevent it abruptly stopping somewhere.
- load = null
- update_appearance()
- return
-
- mode = BOT_IDLE
-
- var/atom/movable/cached_load = load //cache the load since unbuckling mobs clears the var.
-
- unbuckle_all_mobs()
-
- if(load) //don't have to do any of this for mobs.
- load = null
- cached_load.forceMove(loc)
- cached_load.pixel_y = initial(cached_load.pixel_y)
- cached_load.layer = initial(cached_load.layer)
- SET_PLANE_EXPLICIT(cached_load, initial(cached_load.plane), src)
-
- if(dirn) //move the thing to the delivery point.
- cached_load.Move(get_step(loc,dirn), dirn)
-
- update_appearance()
-
-/mob/living/simple_animal/bot/mulebot/get_status_tab_items()
- . = ..()
- if(cell)
- . += "Charge Left: [cell.charge]/[cell.maxcharge]"
- else
- . += "No Cell Inserted!"
- if(load)
- . += "Current Load: [get_load_name()]"
-
-
-/mob/living/simple_animal/bot/mulebot/call_bot()
- ..()
- if(path && length(path))
- target = ai_waypoint //Target is the end point of the path, the waypoint set by the AI.
- destination = get_area_name(target, TRUE)
- pathset = TRUE //Indicates the AI's custom path is initialized.
- start()
-
-/mob/living/simple_animal/bot/mulebot/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
- . = ..()
- if(has_gravity())
- for(var/mob/living/carbon/human/future_pancake in loc)
- if(future_pancake.body_position == LYING_DOWN)
- run_over(future_pancake)
-
- diag_hud_set_mulebotcell()
-
-/mob/living/simple_animal/bot/mulebot/handle_automated_action()
- if(!(bot_mode_flags & BOT_MODE_ON))
- return
- if(!has_power())
- turn_off()
- return
- if(mode == BOT_IDLE)
- return
- if(HAS_TRAIT(src, TRAIT_IMMOBILIZED))
- return
-
- var/speed = (wires.is_cut(WIRE_MOTOR1) ? 0 : 2) + (wires.is_cut(WIRE_MOTOR2) ? 0 : 1)
- if(!speed)//Devide by zero man bad
- return
- num_steps = round(10/speed) //10, 5, or 3 steps, depending on how many wires we have cut
- START_PROCESSING(SSfastprocess, src)
-
-/mob/living/simple_animal/bot/mulebot/process()
- if(!(bot_mode_flags & BOT_MODE_ON) || client || (num_steps <= 0) || !has_power())
- return PROCESS_KILL
- num_steps--
-
- switch(mode)
- if(BOT_IDLE) // idle
- return
-
- if(BOT_DELIVER, BOT_GO_HOME, BOT_BLOCKED) // navigating to deliver,home, or blocked
- if(loc == target) // reached target
- at_target()
- return
-
- else if(length(path) && target) // valid path
- var/turf/next = path[1]
- reached_target = FALSE
- if(next == loc)
- path -= next
- return
- if(isturf(next))
- if(SEND_SIGNAL(src, COMSIG_MOB_BOT_PRE_STEP) & COMPONENT_MOB_BOT_BLOCK_PRE_STEP)
- return
- var/oldloc = loc
- var/moved = step_towards(src, next) // attempt to move
- if(moved && oldloc != loc) // successful move
- SEND_SIGNAL(src, COMSIG_MOB_BOT_STEP)
- blockcount = 0
- path -= loc
- if(destination == home_destination)
- mode = BOT_GO_HOME
- else
- mode = BOT_DELIVER
-
- else // failed to move
-
- blockcount++
- mode = BOT_BLOCKED
- if(blockcount == 3)
- buzz(ANNOYED)
-
- if(blockcount > 10) // attempt 10 times before recomputing
- // find new path excluding blocked turf
- buzz(SIGH)
- mode = BOT_WAIT_FOR_NAV
- blockcount = 0
- addtimer(CALLBACK(src, PROC_REF(process_blocked), next), 2 SECONDS)
- return
- return
- else
- buzz(ANNOYED)
- mode = BOT_NAV
- return
- else
- mode = BOT_NAV
- return
-
- if(BOT_NAV) // calculate new path
- mode = BOT_WAIT_FOR_NAV
- INVOKE_ASYNC(src, PROC_REF(process_nav))
-
-/mob/living/simple_animal/bot/mulebot/proc/process_blocked(turf/next)
- calc_path(avoid=next)
- if(length(path))
- buzz(DELIGHT)
- mode = BOT_BLOCKED
-
-/mob/living/simple_animal/bot/mulebot/proc/process_nav()
- calc_path()
-
- if(length(path))
- blockcount = 0
- mode = BOT_BLOCKED
- buzz(DELIGHT)
-
- else
- buzz(SIGH)
-
- mode = BOT_NO_ROUTE
-
-// calculates a path to the current destination
-// given an optional turf to avoid
-/mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null)
- path = get_path_to(src, target, max_distance=250, access=access_card.GetAccess(), exclude=avoid, diagonal_handling=DIAGONAL_REMOVE_ALL)
-
-// sets the current destination
-// signals all beacons matching the delivery code
-// beacons will return a signal giving their locations
-/mob/living/simple_animal/bot/mulebot/proc/set_destination(new_dest)
- new_destination = new_dest
- get_nav()
-
-// starts bot moving to current destination
-/mob/living/simple_animal/bot/mulebot/proc/start()
- if(!(bot_mode_flags & BOT_MODE_ON))
- return
- if(destination == home_destination)
- mode = BOT_GO_HOME
- else
- mode = BOT_DELIVER
- get_nav()
-
-// starts bot moving to home
-// sends a beacon query to find
-/mob/living/simple_animal/bot/mulebot/proc/start_home()
- if(!(bot_mode_flags & BOT_MODE_ON))
- return
- INVOKE_ASYNC(src, PROC_REF(do_start_home))
-
-/mob/living/simple_animal/bot/mulebot/proc/do_start_home()
- set_destination(home_destination)
- mode = BOT_BLOCKED
-
-// called when bot reaches current target
-/mob/living/simple_animal/bot/mulebot/proc/at_target()
- if(!reached_target)
- radio_channel = RADIO_CHANNEL_SUPPLY //Supply channel
- buzz(CHIME)
- reached_target = TRUE
-
- if(pathset) //The AI called us here, so notify it of our arrival.
- loaddir = dir //The MULE will attempt to load a crate in whatever direction the MULE is "facing".
- if(calling_ai)
- to_chat(calling_ai, span_notice("[icon2html(src, calling_ai)] [src] wirelessly plays a chiming sound!"))
- calling_ai.playsound_local(calling_ai, 'sound/machines/chime.ogg', 40, FALSE)
- calling_ai = null
- radio_channel = RADIO_CHANNEL_AI_PRIVATE //Report on AI Private instead if the AI is controlling us.
-
- if(load) // if loaded, unload at target
- if(mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE)
- speak("Destination [RUNECHAT_BOLD("[destination]")] reached. Unloading [load].",radio_channel)
- unload(loaddir)
- else
- // not loaded
- if(mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE) // find a crate
- var/atom/movable/AM
- if(wires.is_cut(WIRE_LOADCHECK)) // if hacked, load first unanchored thing we find
- for(var/atom/movable/A in get_step(loc, loaddir))
- if(!A.anchored)
- AM = A
- break
- else // otherwise, look for crates only
- AM = locate(/obj/structure/closet/crate) in get_step(loc,loaddir)
- if(AM?.Adjacent(src))
- load(AM)
- if(mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE)
- speak("Now loading [load] at [RUNECHAT_BOLD("[get_area_name(src)]")].", radio_channel)
- // whatever happened, check to see if we return home
-
- if((mulebot_delivery_flags & MULEBOT_RETURN_MODE) && home_destination && destination != home_destination)
- // auto return set and not at home already
- start_home()
- mode = BOT_BLOCKED
- else
- bot_reset() // otherwise go idle
-
-
-/mob/living/simple_animal/bot/mulebot/MobBump(mob/M) // called when the bot bumps into a mob
- if(mind || !isliving(M)) //if there's a sentience controlling the bot, they aren't allowed to harm folks.
- return ..()
- var/mob/living/L = M
- if(wires.is_cut(WIRE_AVOIDANCE)) // usually just bumps, but if the avoidance wire is cut, knocks them over.
- if(iscyborg(L))
- visible_message(span_danger("[src] bumps into [L]!"))
- else if(L.Knockdown(8 SECONDS))
- log_combat(src, L, "knocked down")
- visible_message(span_danger("[src] knocks over [L]!"))
- return ..()
-
-// when mulebot is in the same loc
-/mob/living/simple_animal/bot/mulebot/proc/run_over(mob/living/carbon/human/crushed)
- if (!(bot_cover_flags & BOT_COVER_EMAGGED) && !wires.is_cut(WIRE_AVOIDANCE))
- if (!has_status_effect(/datum/status_effect/careful_driving))
- crushed.visible_message(span_notice("[src] slows down to avoid crushing [crushed]."))
- apply_status_effect(/datum/status_effect/careful_driving)
- return // Player mules must be emagged before they can trample
-
- log_combat(src, crushed, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])")
- crushed.visible_message(
- span_danger("[src] drives over [crushed]!"),
- span_userdanger("[src] drives over you!"),
- )
-
- playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
-
- var/damage = rand(5, 15)
- crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, run_armor_check(BODY_ZONE_HEAD, MELEE))
- crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST, run_armor_check(BODY_ZONE_CHEST, MELEE))
- crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, run_armor_check(BODY_ZONE_L_LEG, MELEE))
- crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, run_armor_check(BODY_ZONE_R_LEG, MELEE))
- crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, run_armor_check(BODY_ZONE_L_ARM, MELEE))
- crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, run_armor_check(BODY_ZONE_R_ARM, MELEE))
-
- add_mob_blood(crushed)
-
- var/turf/below_us = get_turf(src)
- below_us.add_mob_blood(crushed)
-
- AddComponent(/datum/component/blood_walk, \
- blood_type = /obj/effect/decal/cleanable/blood/tracks, \
- target_dir_change = TRUE, \
- transfer_blood_dna = TRUE, \
- max_blood = 4)
-
-// player on mulebot attempted to move
-/mob/living/simple_animal/bot/mulebot/relaymove(mob/living/user, direction)
- if(user.incapacitated)
- return
- if(load == user)
- unload(0)
-
-
-//Update navigation data. Called when commanded to deliver, return home, or a route update is needed...
-/mob/living/simple_animal/bot/mulebot/proc/get_nav()
- if(!(bot_mode_flags & BOT_MODE_ON) || wires.is_cut(WIRE_BEACON))
- return
-
- for(var/obj/machinery/navbeacon/NB in GLOB.deliverybeacons)
- if(NB.location == new_destination) // if the beacon location matches the set destination
- // the we will navigate there
- destination = new_destination
- target = NB.loc
- var/direction = NB.codes[NAVBEACON_DELIVERY_DIRECTION] // this will be the load/unload dir
- if(!direction)
- direction = NB.dir // fallback
- if(direction)
- loaddir = text2num(direction)
- else
- loaddir = 0
- if(destination) // No need to calculate a path if you do not have a destination set!
- calc_path()
-
-/mob/living/simple_animal/bot/mulebot/emp_act(severity)
- . = ..()
- if(cell && !(. & EMP_PROTECT_CONTENTS))
- cell.emp_act(severity)
- if(load)
- load.emp_act(severity)
-
-
-/mob/living/simple_animal/bot/mulebot/explode()
- var/atom/Tsec = drop_location()
-
- new /obj/item/assembly/prox_sensor(Tsec)
- new /obj/item/stack/rods(Tsec)
- new /obj/item/stack/rods(Tsec)
- new /obj/item/stack/cable_coil/cut(Tsec)
- if(cell)
- cell.forceMove(Tsec)
- cell = null
-
- new /obj/effect/decal/cleanable/blood/oil(loc)
- return ..()
-
-/mob/living/simple_animal/bot/mulebot/remove_air(amount) //To prevent riders suffocating
- return loc ? loc.remove_air(amount) : null
-
-/mob/living/simple_animal/bot/mulebot/execute_resist()
- . = ..()
- if(load)
- unload()
-
-/mob/living/simple_animal/bot/mulebot/UnarmedAttack(atom/A, proximity_flag, list/modifiers)
- if(!can_unarmed_attack())
- return
- if(isturf(A) && isturf(loc) && loc.Adjacent(A) && load)
- unload(get_dir(loc, A))
- else
- return ..()
-
-/// Checks whether the bot can complete a step_towards, checking whether the bot is on and has the charge to do the move. Returns COMPONENT_MOB_BOT_CANCELSTEP if the bot should not step.
-/mob/living/simple_animal/bot/mulebot/proc/check_pre_step(datum/source)
- SIGNAL_HANDLER
-
- if(!(bot_mode_flags & BOT_MODE_ON))
- return COMPONENT_MOB_BOT_BLOCK_PRE_STEP
-
- if((cell && (cell.charge < cell_move_power_usage)) || !has_power())
- turn_off()
- return COMPONENT_MOB_BOT_BLOCK_PRE_STEP
-
-/// Uses power from the cell when the bot steps.
-/mob/living/simple_animal/bot/mulebot/proc/on_bot_step(datum/source)
- SIGNAL_HANDLER
-
- cell?.use(cell_move_power_usage)
-
-/mob/living/simple_animal/bot/mulebot/post_possession()
- . = ..()
- visible_message(span_notice("[src]'s safeties are locked on."))
-
-/mob/living/simple_animal/bot/mulebot/paranormal//allows ghosts only unless hacked to actually be useful
- name = "\improper GHOULbot"
- desc = "A rather ghastly looking... Multiple Utility Load Effector bot? It only seems to accept paranormal forces, and for this reason is fucking useless."
- icon_state = "paranormalmulebot0"
- base_icon = "paranormalmulebot"
-
-/mob/living/simple_animal/bot/mulebot/paranormal/mouse_drop_receive(atom/movable/AM, mob/user, params)
- var/mob/living/L = user
-
- if(user.incapacitated || (istype(L) && L.body_position == LYING_DOWN))
- return
-
- if(!istype(AM) || iseyemob(AM) || istype(AM, /obj/effect/dummy/phased_mob)) //allows ghosts!
- return
-
- load(AM)
-
-/mob/living/simple_animal/bot/mulebot/paranormal/load(atom/movable/movable_atom)
- if(load || movable_atom.anchored)
- return
-
- if(!isturf(movable_atom.loc)) //To prevent the loading from stuff from someone's inventory or screen icons.
- return
-
- if(isobserver(movable_atom))
- visible_message(span_warning("A ghostly figure appears on [src]!"))
- movable_atom.forceMove(src)
- RegisterSignal(movable_atom, COMSIG_MOVABLE_MOVED, PROC_REF(ghostmoved))
-
- else if(!wires.is_cut(WIRE_LOADCHECK))
- buzz(SIGH)
- return // if not hacked, only allow ghosts to be loaded
-
- else if(isobj(movable_atom))
- if(movable_atom.has_buckled_mobs() || (locate(/mob) in movable_atom)) //can't load non crates objects with mobs buckled to it or inside it.
- buzz(SIGH)
- return
-
- if(istype(movable_atom, /obj/structure/closet/crate))
- var/obj/structure/closet/crate/crate = movable_atom
- crate.close() //make sure it's closed
-
- movable_atom.forceMove(src)
-
- else if(isliving(movable_atom) && !load_mob(movable_atom))
- return
-
- load = movable_atom
- mode = BOT_IDLE
- update_appearance()
-
-/mob/living/simple_animal/bot/mulebot/paranormal/update_overlays()
- . = ..()
- if(!isobserver(load))
- return
- var/mutable_appearance/ghost_overlay = mutable_appearance('icons/mob/simple/mob.dmi', "ghost", layer + 0.01) //use a generic ghost icon, otherwise you can metagame who's dead if they have a custom ghost set
- ghost_overlay.pixel_z = 12
- . += ghost_overlay
-
-/mob/living/simple_animal/bot/mulebot/paranormal/get_load_name() //Don't reveal the name of ghosts so we can't metagame who died and all that.
- . = ..()
- if(. && isobserver(load))
- return "Unknown"
-
-/mob/living/simple_animal/bot/mulebot/paranormal/proc/ghostmoved()
- SIGNAL_HANDLER
- visible_message(span_notice("The ghostly figure vanishes..."))
- UnregisterSignal(load, COMSIG_MOVABLE_MOVED)
- unload(0)
-
-#undef SIGH
-#undef ANNOYED
-#undef DELIGHT
-#undef CHIME
diff --git a/code/modules/mod/modules/modules_ninja.dm b/code/modules/mod/modules/modules_ninja.dm
index 04df80eb540..1f530e8904d 100644
--- a/code/modules/mod/modules/modules_ninja.dm
+++ b/code/modules/mod/modules/modules_ninja.dm
@@ -547,7 +547,7 @@
return COMPONENT_CANCEL_ATTACK_CHAIN
//BOTS, overloads them and causes a explosion
-/mob/living/simple_animal/bot/ninjadrain_act(mob/living/carbon/human/ninja, obj/item/mod/module/hacker/hacking_module)
+/mob/living/basic/bot/ninjadrain_act(mob/living/carbon/human/ninja, obj/item/mod/module/hacker/hacking_module)
to_chat(src, span_boldwarning("Your circutry suddenly begins heating up!"))
if(!do_after(ninja, 1.5 SECONDS, target = src, hidden = TRUE))
return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/code/modules/modular_computers/file_system/programs/robocontrol.dm b/code/modules/modular_computers/file_system/programs/robocontrol.dm
index 1176ce92114..f7278fa3008 100644
--- a/code/modules/modular_computers/file_system/programs/robocontrol.dm
+++ b/code/modules/modular_computers/file_system/programs/robocontrol.dm
@@ -32,32 +32,32 @@
botcount = 0
- for(var/mob/living/simple_animal/bot/simple_bot as anything in GLOB.bots_list)
- if(!is_valid_z_level(current_turf, get_turf(simple_bot)) || !(simple_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Only non-emagged bots on the same Z-level are detected!
+ for(var/mob/living/basic/bot/basic_bot as anything in GLOB.bots_list)
+ if(!is_valid_z_level(current_turf, get_turf(basic_bot)) || !(basic_bot.bot_mode_flags & BOT_MODE_REMOTE_ENABLED)) //Only non-emagged bots on the same Z-level are detected!
continue
- if(!simple_bot.allowed(user) && !simple_bot.check_access(computer.stored_id)) // Only check Bots we can access
+ if(!basic_bot.allowed(user) && !basic_bot.check_access(computer.stored_id)) // Only check Bots we can access
continue
var/list/newbot = list(
- "name" = simple_bot.name,
- "mode" = simple_bot.get_mode_ui(),
- "model" = simple_bot.bot_type,
- "locat" = get_area(simple_bot),
- "bot_ref" = REF(simple_bot),
+ "name" = basic_bot.name,
+ "mode" = basic_bot.get_mode_ui(),
+ "model" = basic_bot.bot_type,
+ "locat" = get_area(basic_bot),
+ "bot_ref" = REF(basic_bot),
"mule_check" = FALSE,
)
- if(simple_bot.bot_type == MULE_BOT)
- var/mob/living/simple_animal/bot/mulebot/simple_mulebot = simple_bot
+ if(basic_bot.bot_type == MULE_BOT)
+ var/mob/living/basic/bot/mulebot/basic_mulebot = basic_bot
mulelist += list(list(
- "name" = simple_mulebot.name,
- "id" = simple_mulebot.id,
- "dest" = simple_mulebot.destination,
- "power" = simple_mulebot.cell ? simple_mulebot.cell.percent() : 0,
- "home" = simple_mulebot.home_destination,
- "autoReturn" = simple_mulebot.mulebot_delivery_flags & MULEBOT_RETURN_MODE,
- "autoPickup" = simple_mulebot.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE,
- "reportDelivery" = simple_mulebot.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE,
- "mule_ref" = REF(simple_mulebot),
- "load" = simple_mulebot.get_load_name(),
+ "name" = basic_mulebot.name,
+ "id" = basic_mulebot.id,
+ "dest" = basic_mulebot.ai_controller.blackboard[BB_MULEBOT_DESTINATION_BEACON],
+ "power" = basic_mulebot.cell ? basic_mulebot.cell.percent() : 0,
+ "home" = basic_mulebot.ai_controller.blackboard[BB_MULEBOT_HOME_BEACON],
+ "autoReturn" = basic_mulebot.mulebot_delivery_flags & MULEBOT_RETURN_MODE,
+ "autoPickup" = basic_mulebot.mulebot_delivery_flags & MULEBOT_AUTO_PICKUP_MODE,
+ "reportDelivery" = basic_mulebot.mulebot_delivery_flags & MULEBOT_REPORT_DELIVERY_MODE,
+ "mule_ref" = REF(basic_mulebot),
+ "load" = basic_mulebot.get_load_name(),
))
newbot["mule_check"] = TRUE
botlist += list(newbot)
@@ -106,15 +106,15 @@
"report",
"ejectpai",
)
- var/mob/living/simple_animal/bot/simple_bot = locate(params["robot"]) in GLOB.bots_list
+ var/mob/living/basic/bot/basic_bot = locate(params["robot"]) in GLOB.bots_list
if (action in standard_actions)
- simple_bot.bot_control(action, current_user, id_card?.GetAccess())
+ basic_bot.bot_control(action, current_user, id_card?.GetAccess())
if (action in MULE_actions)
- simple_bot.bot_control(action, current_user, id_card?.GetAccess(), TRUE)
+ basic_bot.bot_control(action, current_user, id_card?.GetAccess(), TRUE)
switch(action)
if("summon")
- simple_bot.bot_control(action, current_user, id_card ? id_card.access : id_card?.GetAccess())
+ basic_bot.bot_control(action, current_user, id_card ? id_card.access : id_card?.GetAccess())
if("ejectcard")
if(!computer || !computer.stored_id)
return
diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
index 7ecd0e6b852..c9b84527ebf 100644
--- a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
+++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
@@ -165,9 +165,9 @@
new_shape.add_traits(list(TRAIT_DONT_WRITE_MEMORY, TRAIT_SHAPESHIFTED), SHAPESHIFT_TRAIT)
// Make sure that if you shapechanged into a bot, the AI can't just turn you off.
- var/mob/living/simple_animal/bot/polymorph_bot = new_shape
+ var/mob/living/basic/bot/polymorph_bot = new_shape
if (istype(polymorph_bot))
- polymorph_bot.bot_cover_flags |= BOT_COVER_EMAGGED
+ polymorph_bot.bot_access_flags |= BOT_COVER_EMAGGED
polymorph_bot.bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED
return new_shape
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 5b1794ef33b..dae3132b706 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -5,9 +5,6 @@
// Use the new basic mobs system instead.
// If you are refactoring a simple_animal, REMOVE it from this list
var/list/allowed_types = list(
- /mob/living/simple_animal/bot,
- /mob/living/simple_animal/bot/mulebot,
- /mob/living/simple_animal/bot/mulebot/paranormal,
/mob/living/simple_animal/hostile,
/mob/living/simple_animal/hostile/asteroid,
/mob/living/simple_animal/hostile/asteroid/elite,
diff --git a/tgstation.dme b/tgstation.dme
index afa90adb47e..e17c81ab8be 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -5260,6 +5260,14 @@
#include "code\modules\mob\living\basic\bots\hygienebot\hygienebot_ai.dm"
#include "code\modules\mob\living\basic\bots\medbot\medbot.dm"
#include "code\modules\mob\living\basic\bots\medbot\medbot_ai.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_ai.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_control.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_delivery.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_hud.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_movement.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_paranormal.dm"
+#include "code\modules\mob\living\basic\bots\mulebot\mulebot_tool_interactions.dm"
#include "code\modules\mob\living\basic\bots\repairbot\repairbot.dm"
#include "code\modules\mob\living\basic\bots\repairbot\repairbot_abilities.dm"
#include "code\modules\mob\living\basic\bots\repairbot\repairbot_ai.dm"
@@ -5746,10 +5754,8 @@
#include "code\modules\mob\living\simple_animal\animal_defense.dm"
#include "code\modules\mob\living\simple_animal\damage_procs.dm"
#include "code\modules\mob\living\simple_animal\simple_animal.dm"
-#include "code\modules\mob\living\simple_animal\bot\bot.dm"
#include "code\modules\mob\living\simple_animal\bot\bot_announcement.dm"
#include "code\modules\mob\living\simple_animal\bot\construction.dm"
-#include "code\modules\mob\living\simple_animal\bot\mulebot.dm"
#include "code\modules\mob\living\simple_animal\hostile\hostile.dm"
#include "code\modules\mob\living\simple_animal\hostile\ooze.dm"
#include "code\modules\mob\living\simple_animal\hostile\megafauna\_megafauna.dm"
diff --git a/tools/UpdatePaths/Scripts/96500_mulebots.txt b/tools/UpdatePaths/Scripts/96500_mulebots.txt
new file mode 100644
index 00000000000..1393f74f32a
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/96500_mulebots.txt
@@ -0,0 +1,2 @@
+/mob/living/simple_animal/bot/mulebot : /mob/living/basic/bot/mulebot{@OLD}
+/mob/living/simple_animal/bot/mulebot/paranormal : /mob/living/basic/bot/mulebot/paranormal{@OLD}
\ No newline at end of file