[MIRROR] Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range [MDB IGNORE] (#22745)

* Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range (#77030)

## About The Pull Request

Tries to keep pAIs in range of their owner by moving them closer when
the owner moves, rather than jarringly teleporting every time the owner
gets out of range. Does this by calculating the closest path a nearby
tile and forcefully moving you there. Still a bit janky at times but is
better than teleporting directly onto the owner 100% of the time I feel.
Also prevents you from moving out of range, rather than forcefully
teleporting you back.

Increases the default pAI range to the maximum (9 tiles)

## Why It's Good For The Game

New leashing makes being a leashed pAI significantly less jarring and
obvious. Ideally we would also have a visible max range too.

Default pAI range was pretty small in my testing and I think it's not
unreasonable to think a lot of people won't bother changing it. That
they are leashed at all is the important part.

## Changelog

🆑
qol: pAIs now try to stay within range of their owner, and teleport back
only when necessary
qol: Default max pAI range has been changed to the maximum range you can
choose (9 tiles)
/🆑

---------

Co-authored-by: Jacquerel <hnevard@ gmail.com>

* Add leash component to pAIs that keeps them within range instead of directly teleporting them back, increases default range to max range

* Fixes the leash enable/disable

* A workaround because the procs are private.

* Update card.dm

* Update card.dm

what I get for rushing

* Update card.dm

---------

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Co-authored-by: Jacquerel <hnevard@ gmail.com>
Co-authored-by: Giz <13398309+vinylspiders@users.noreply.github.com>
This commit is contained in:
SkyratBot
2023-07-30 20:44:05 -04:00
committed by nevimer
co-authored by Jacquerel Mothblocks Giz
parent 7fb76545ec
commit e81aa4a1c9
11 changed files with 345 additions and 50 deletions
@@ -0,0 +1,11 @@
/// Called when a /datum/component/leash must forcibly teleport the parent to the owner.
/// Fired on the object with the leash component.
#define COMSIG_LEASH_FORCE_TELEPORT "leash_force_teleport"
/// Called when a /datum/component/leash plans on pathfinding to the target, if out of range.
/// Fired on the object with the leash component.
#define COMSIG_LEASH_PATH_STARTED "leash_path_started"
/// Called when a /datum/component/leash finishes its pathfinding to the target.
/// Fired on the object with the leash component.
#define COMSIG_LEASH_PATH_COMPLETE "leash_path_complete"
+2
View File
@@ -15,6 +15,8 @@
#define HOLOFORM_MAX_RANGE 25 // SKYRAT EDIT CHANGE - ORIGINAL: #define HOLOFORM_MAX_RANGE 9
/// Minimum distance you can set the holoform leash
#define HOLOFORM_MIN_RANGE 3
/// Default holoform leash distance
#define HOLOFORM_DEFAULT_RANGE HOLOFORM_MAX_RANGE
/// UI action to toggle huds
#define PAI_TOGGLE_MEDICAL_HUD 0
+180
View File
@@ -0,0 +1,180 @@
/// Keeps the parent within the distance of its owner as naturally as possible,
/// but teleporting if necessary.
/datum/component/leash
/// The owner of the leash. If this is qdeleted, the leash is as well.
var/atom/movable/owner
/// The maximum distance you can move from your owner
var/distance
/// The object type to create on the old turf when forcibly teleporting out
var/force_teleport_out_effect
/// The object type to create on the new turf when forcibly teleporting out
var/force_teleport_in_effect
VAR_PRIVATE
// Pathfinding can yield, so only move us closer if this is the best one
current_path_tick = 0
last_completed_path_tick = 0
performing_path_move = FALSE
/datum/component/leash/Initialize(
atom/movable/owner,
distance = 3,
force_teleport_out_effect,
force_teleport_in_effect,
)
. = ..()
if (!ismovable(parent))
stack_trace("Parent must be a movable")
return COMPONENT_INCOMPATIBLE
if (!ismovable(owner))
stack_trace("[owner] (owner) is not a movable")
return COMPONENT_INCOMPATIBLE
if (!isnum(distance))
stack_trace("[distance] (distance) must be a number")
return COMPONENT_INCOMPATIBLE
if (!isnull(force_teleport_out_effect) && !ispath(force_teleport_out_effect))
stack_trace("force_teleport_out_effect must be null or a path, not [force_teleport_out_effect]")
return COMPONENT_INCOMPATIBLE
if (!isnull(force_teleport_in_effect) && !ispath(force_teleport_in_effect))
stack_trace("force_teleport_in_effect must be null or a path, not [force_teleport_in_effect]")
return COMPONENT_INCOMPATIBLE
src.owner = owner
src.distance = distance
src.force_teleport_out_effect = force_teleport_out_effect
src.force_teleport_in_effect = force_teleport_in_effect
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(on_owner_qdel))
var/static/list/container_connections = list(
COMSIG_MOVABLE_MOVED = PROC_REF(on_owner_moved),
)
AddComponent(/datum/component/connect_containers, owner, container_connections)
RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved))
RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move))
check_distance()
/datum/component/leash/Destroy()
owner = null
return ..()
/datum/component/leash/proc/set_distance(distance)
ASSERT(isnum(distance))
src.distance = distance
check_distance()
/datum/component/leash/proc/on_owner_qdel()
SIGNAL_HANDLER
PRIVATE_PROC(TRUE)
qdel(src)
/datum/component/leash/proc/on_owner_moved(atom/movable/source)
SIGNAL_HANDLER
PRIVATE_PROC(TRUE)
check_distance()
/datum/component/leash/proc/on_parent_pre_move(atom/movable/source, atom/new_location)
SIGNAL_HANDLER
PRIVATE_PROC(TRUE)
if (performing_path_move)
return NONE
var/turf/new_location_turf = get_turf(new_location)
if (get_dist(new_location_turf, owner) <= distance)
return NONE
if (ismob(source))
source.balloon_alert(source, "too far!")
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
/datum/component/leash/proc/check_distance()
set waitfor = FALSE
PRIVATE_PROC(TRUE)
if (get_dist(parent, owner) <= distance)
return
SEND_SIGNAL(parent, COMSIG_LEASH_PATH_STARTED)
current_path_tick += 1
var/our_path_tick = current_path_tick
var/list/path = get_path_to(parent, owner, mintargetdist = distance)
if (last_completed_path_tick > our_path_tick)
return
last_completed_path_tick = our_path_tick
commit_path(path)
/datum/component/leash/proc/commit_path(list/turf/path)
SHOULD_NOT_SLEEP(TRUE)
PRIVATE_PROC(TRUE)
performing_path_move = TRUE
var/atom/movable/movable_parent = parent
for (var/turf/to_move as anything in path)
// Could be an older path, don't make us teleport back
if (!to_move.Adjacent(parent))
continue
if (!movable_parent.Move(to_move))
force_teleport_back("bad path step")
return
if (get_dist(parent, owner) > distance)
force_teleport_back("incomplete path")
performing_path_move = FALSE
SEND_SIGNAL(parent, COMSIG_LEASH_PATH_COMPLETE)
/datum/component/leash/proc/force_teleport_back(reason)
PRIVATE_PROC(TRUE)
var/atom/movable/movable_parent = parent
SSblackbox.record_feedback("tally", "leash_force_teleport_back", 1, reason)
if (force_teleport_out_effect)
new force_teleport_out_effect(movable_parent.loc)
movable_parent.forceMove(get_turf(owner))
if (force_teleport_in_effect)
new force_teleport_in_effect(movable_parent.loc)
if (ismob(movable_parent))
movable_parent.balloon_alert(movable_parent, "moved out of range!")
SEND_SIGNAL(parent, COMSIG_LEASH_FORCE_TELEPORT)
/// A debug spawner that will create a corgi leashed to a bike horn, plus a beam
/obj/effect/spawner/debug_leash
/obj/effect/spawner/debug_leash/Initialize(mapload)
. = ..()
var/obj/item/bikehorn/bike_horn = new(loc)
var/mob/living/basic/pet/dog/corgi/corgi = new(loc)
corgi.AddComponent(/datum/component/leash, bike_horn)
corgi.Beam(bike_horn)
+1 -12
View File
@@ -67,20 +67,9 @@
/obj/item/pai_card/Initialize(mapload)
. = ..()
var/static/list/containers_connections = list(COMSIG_MOVABLE_MOVED = PROC_REF(card_moved))
AddComponent(/datum/component/connect_containers, tracked = src, connections = containers_connections)
update_appearance()
SSpai.pai_card_list += src
/obj/item/pai_card/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
card_moved()
/// Called when we, our loc, or our loc's loc, or our loc's loc's loc, or etc has moved
/obj/item/pai_card/proc/card_moved()
SIGNAL_HANDLER
pai?.check_distance()
/obj/item/pai_card/suicide_act(mob/living/user)
user.visible_message(span_suicide("[user] is staring sadly at [src]! [user.p_They()] can't keep living without real human intimacy!"))
return OXYLOSS
@@ -128,7 +117,7 @@
name = pai.name,
transmit = pai.can_transmit,
receive = pai.can_receive,
range = pai.leashed_distance,
range = pai.leash.distance,
)
return data
+5 -19
View File
@@ -37,8 +37,6 @@
var/can_transmit = TRUE
/// The card we inhabit
var/obj/item/pai_card/card
/// The maximum distance we can travel away from our pai card
var/leashed_distance = 5
/// The current chasis that will appear when in holoform
var/chassis = "repairbot"
/// Toggles whether the pAI can hold encryption keys or not
@@ -67,6 +65,8 @@
var/ram = 100
/// Toggles whether the Security HUD is active or not
var/secHUD = FALSE
/// The current leash to the owner
var/datum/component/leash/leash
// Onboard Items
/// Atmospheric analyzer
@@ -221,6 +221,7 @@
pai_card.set_personality(src)
card = pai_card
forceMove(pai_card)
leash = AddComponent(/datum/component/leash, pai_card, HOLOFORM_DEFAULT_RANGE, force_teleport_out_effect = /obj/effect/temp_visual/guardian/phase/out)
addtimer(VARSET_WEAK_CALLBACK(src, holochassis_ready, TRUE), HOLOCHASSIS_INIT_TIME)
if(!holoform)
add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), PAI_FOLDED)
@@ -228,19 +229,6 @@
RegisterSignal(src, COMSIG_LIVING_CULT_SACRIFICED, PROC_REF(on_cult_sacrificed))
/mob/living/silicon/pai/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
check_distance()
/// Checks if we're in range of our pai card
/mob/living/silicon/pai/proc/check_distance()
SIGNAL_HANDLER
if (get_dist(get_turf(card), get_turf(src)) <= leashed_distance)
return
to_chat(src, span_warning("You moved out of range of your holotransmitter!"))
new /obj/effect/temp_visual/guardian/phase/out(loc)
forceMove(get_turf(card))
/mob/living/silicon/pai/make_laws()
laws = new /datum/ai_laws/pai()
return TRUE
@@ -453,9 +441,7 @@
/// Updates the distance we can be from our pai card
/mob/living/silicon/pai/proc/increment_range(increment_amount)
var/new_distance = leashed_distance + increment_amount
var/new_distance = leash.distance + increment_amount
if (new_distance < HOLOFORM_MIN_RANGE || new_distance > HOLOFORM_MAX_RANGE)
return
leashed_distance = new_distance
if (increment_amount < 0)
check_distance()
leash.set_distance(new_distance)
+1
View File
@@ -156,6 +156,7 @@
#include "keybinding_init.dm"
#include "knockoff_component.dm"
#include "language_transfer.dm"
#include "leash.dm"
#include "lesserform.dm"
#include "limbsanity.dm"
#include "liver.dm"
+102
View File
@@ -0,0 +1,102 @@
/datum/unit_test/leash
abstract_type = /datum/unit_test/leash
var/atom/movable/owner
var/atom/movable/pet
var/max_distance = 3
var/forcibly_teleported = FALSE
var/datum/leash_wait/leash_wait
/datum/unit_test/leash/New()
. = ..()
owner = allocate(/obj/item/pen)
pet = allocate(/obj/item/pen)
pet.AddComponent(/datum/component/leash, owner, max_distance)
RegisterSignal(pet, COMSIG_LEASH_FORCE_TELEPORT, PROC_REF(on_leash_force_teleport))
RegisterSignal(pet, COMSIG_LEASH_PATH_STARTED, PROC_REF(on_leash_path_started))
RegisterSignal(pet, COMSIG_LEASH_PATH_COMPLETE, PROC_REF(on_leash_path_complete))
/datum/unit_test/leash/Destroy()
QDEL_NULL(owner)
QDEL_NULL(pet)
return ..()
/datum/unit_test/leash/proc/on_leash_force_teleport()
SIGNAL_HANDLER
forcibly_teleported = TRUE
/datum/unit_test/leash/proc/on_leash_path_complete()
SIGNAL_HANDLER
leash_wait?.completed()
/datum/unit_test/leash/proc/on_leash_path_started()
SIGNAL_HANDLER
leash_wait?.started()
/datum/unit_test/leash/proc/move_away(atom/movable/mover, distance)
RETURN_TYPE(/datum/leash_wait)
leash_wait = new
for (var/_ in 1 to distance)
mover.Move(get_step(mover, EAST))
return leash_wait
/datum/leash_wait
var/completed = FALSE
var/started = FALSE
var/timed_out = FALSE
/datum/leash_wait/New()
addtimer(VARSET_CALLBACK(src, timed_out, TRUE), 1 SECONDS)
/datum/leash_wait/proc/completed()
completed = TRUE
/datum/leash_wait/proc/started()
started = TRUE
/datum/leash_wait/proc/assert_unmoved()
ASSERT(!started, "Leash started to move when it should not have")
/datum/leash_wait/proc/wait()
ASSERT(started, "Leash doesn't plan on moving")
UNTIL(completed || timed_out)
ASSERT(!timed_out, "Waiting for leash movement timed out, it didn't want to move")
/// Validates the leash component will keep its parent within range without teleporting
/// when possible.
/datum/unit_test/leash/no_teleport
/datum/unit_test/leash/no_teleport/Run()
move_away(owner, 1).assert_unmoved()
TEST_ASSERT_EQUAL(get_dist(owner, pet), 1, "Pet should not have moved")
move_away(owner, max_distance).wait() // max_distance + 1 = we move closer, but don't teleport
TEST_ASSERT_EQUAL(get_dist(owner, pet), max_distance, "Pet should have stayed directly outside range of owner")
TEST_ASSERT(!forcibly_teleported, "Pet should not have been forcibly teleported")
/// Validates that the leash component will forcibly teleport when necessary
/datum/unit_test/leash/will_teleport
/datum/unit_test/leash/will_teleport/Run()
leash_wait = new
owner.forceMove(locate(1, 1, 1))
leash_wait.wait()
TEST_ASSERT(forcibly_teleported, "Pet should have been forcibly teleported, since they are too far away with no valid path")
/// Validates that the leashed object cannot move outside of the max distance from owner
/datum/unit_test/leash/limit_range
/datum/unit_test/leash/limit_range/Run()
move_away(pet, max_distance + 1)
TEST_ASSERT_EQUAL(get_dist(owner, pet), max_distance, "Pet should not have moved farther than max_distance")
@@ -0,0 +1,30 @@
/datum/component/leash
/// whether the leash is enabled or not
var/enabled = TRUE
/// Stop listening for any signals
/datum/component/leash/proc/disable_leash()
if(!enabled)
return
enabled = FALSE
UnregisterSignal(owner, COMSIG_MOVABLE_MOVED)
UnregisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE)
/// Start listening for signals again
/datum/component/leash/proc/enable_leash()
if(enabled)
return
enabled = TRUE
check_distance() // yoink them back
RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(on_owner_moved))
RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_parent_pre_move))
/// Enables or disables the leash, allowing or forbidding the PAI from leaving a specified range
/datum/component/leash/proc/toggle_leash()
to_chat(owner, span_warning("Your virtual leash has been [enabled ? "activated" : "deactivated"]!"))
if(enabled)
disable_leash()
else
enable_leash()
@@ -1,9 +1,17 @@
/obj/item/pai_card/download_candidate(mob/user, ckey)
. = ..()
if(!.)
return
pai.leash.disable_leash() // leash starts off disabled by default
/obj/item/pai_card/ui_data(mob/user)
. = ..()
if(!pai)
return
.["pai"]["leash_enabled"] = pai.leashed
.["pai"]["leash_enabled"] = pai.leash.enabled
/obj/item/pai_card/ui_act(action, list/params, datum/tgui/ui)
. = ..()
@@ -11,7 +19,7 @@
return TRUE
if(pai && action == "toggle_leash")
pai.toggle_leash()
pai.leash.toggle_leash()
return TRUE
return FALSE
@@ -1,16 +0,0 @@
/mob/living/silicon/pai/
/// Whether or not the PAI is subject to range limitations and able to roam freely, off by default
var/leashed = FALSE
/// Enables or disables the leash, allowing or forbidding the PAI from leaving a specified range
/mob/living/silicon/pai/proc/toggle_leash()
leashed = !leashed
to_chat(src, span_warning("Your virtual leash has been [leashed ? "activated" : "deactivated"]!"))
if(leashed)
check_distance() // yoink them back
/mob/living/silicon/pai/check_distance()
if(!leashed)
return
return ..()
+3 -1
View File
@@ -289,6 +289,7 @@
#include "code\__DEFINES\dcs\signals\signals_janitor.dm"
#include "code\__DEFINES\dcs\signals\signals_key.dm"
#include "code\__DEFINES\dcs\signals\signals_ladder.dm"
#include "code\__DEFINES\dcs\signals\signals_leash.dm"
#include "code\__DEFINES\dcs\signals\signals_lift.dm"
#include "code\__DEFINES\dcs\signals\signals_light_eater.dm"
#include "code\__DEFINES\dcs\signals\signals_material_container.dm"
@@ -1064,6 +1065,7 @@
#include "code\datums\components\keep_me_secure.dm"
#include "code\datums\components\knockoff.dm"
#include "code\datums\components\label.dm"
#include "code\datums\components\leash.dm"
#include "code\datums\components\light_eater.dm"
#include "code\datums\components\lock_on_cursor.dm"
#include "code\datums\components\manual_blinking.dm"
@@ -5575,6 +5577,7 @@
#include "modular_skyrat\master_files\code\datums\bodypart_overlays\mutant_bodypart_overlay.dm"
#include "modular_skyrat\master_files\code\datums\components\crafting.dm"
#include "modular_skyrat\master_files\code\datums\components\fullauto.dm"
#include "modular_skyrat\master_files\code\datums\components\leash.dm"
#include "modular_skyrat\master_files\code\datums\components\shielded_suit.dm"
#include "modular_skyrat\master_files\code\datums\components\tippable.dm"
#include "modular_skyrat\master_files\code\datums\greyscale\config_types\greyscale_configs.dm"
@@ -5784,7 +5787,6 @@
#include "modular_skyrat\master_files\code\modules\mod\modules\modules_antag.dm"
#include "modular_skyrat\master_files\code\modules\modular_computers\computers\item\laptop_presets.dm"
#include "modular_skyrat\master_files\code\modules\pai\card.dm"
#include "modular_skyrat\master_files\code\modules\pai\pai.dm"
#include "modular_skyrat\master_files\code\modules\paperwork\stamps.dm"
#include "modular_skyrat\master_files\code\modules\power\cable.dm"
#include "modular_skyrat\master_files\code\modules\power\powernet.dm"