Dog AI 2.0 (#57186)

Now that JPS and AI movement datums are fully merged, I'm here to take another (my third actually) crack at giving dogs fun AI. Here's a video demonstration (somewhat WIP), and a quick rundown of everything dogs will be able to do. Basically all of these behaviors are generic and can be extended to other simple mobs,
Commands and Friendship
Fetching
Attack/Harass
Heel
Play Dead
This commit is contained in:
Ryll Ryll
2021-03-06 21:19:38 -05:00
committed by GitHub
parent 5b822cd315
commit ced28d5cec
18 changed files with 711 additions and 71 deletions

View File

@@ -9,8 +9,8 @@
#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE))
#define IS_DEAD_OR_INCAP(source) (HAS_TRAIT(source, TRAIT_INCAPACITATED) || HAS_TRAIT(source, TRAIT_HANDS_BLOCKED) || IS_IN_STASIS(source) || source.stat)
///Max pathing attempts before auto-fail
#define MAX_PATHING_ATTEMPTS 16
///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
///Flags for ai_behavior new()
#define AI_CONTROLLER_INCOMPATIBLE (1<<0)
@@ -35,7 +35,6 @@
#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown"
///Haunted item controller defines
///Chance for haunted item to haunt someone
@@ -69,3 +68,40 @@
#define BB_CUSTOMER_EATING "BB_customer_eating"
#define BB_CUSTOMER_ATTENDING_VENUE "BB_customer_attending_avenue"
#define BB_CUSTOMER_LEAVING "BB_customer_leaving"
///Dog AI controller blackboard keys
#define BB_SIMPLE_CARRY_ITEM "BB_SIMPLE_CARRY_ITEM"
#define BB_FETCH_TARGET "BB_FETCH_TARGET"
#define BB_FETCH_IGNORE_LIST "BB_FETCH_IGNORE_LISTlist"
#define BB_FETCH_DELIVER_TO "BB_FETCH_DELIVER_TO"
#define BB_DOG_FRIENDS "BB_DOG_FRIENDS"
#define BB_DOG_ORDER_MODE "BB_DOG_ORDER_MODE"
#define BB_DOG_PLAYING_DEAD "BB_DOG_PLAYING_DEAD"
#define BB_DOG_HARASS_TARGET "BB_DOG_HARASS_TARGET"
/// Basically, what is our vision/hearing range for picking up on things to fetch/
#define AI_DOG_VISION_RANGE 10
/// What are the odds someone petting us will become our friend?
#define AI_DOG_PET_FRIEND_PROB 15
/// After this long without having fetched something, we clear our ignore list
#define AI_FETCH_IGNORE_DURATION 30 SECONDS
/// After being ordered to heel, we spend this long chilling out
#define AI_DOG_HEEL_DURATION 20 SECONDS
/// After either being given a verbal order or a pointing order, ignore further of each for this duration
#define AI_DOG_COMMAND_COOLDOWN 2 SECONDS
// dog command modes (what pointing at something/someone does depending on the last order the dog heard)
/// Don't do anything (will still react to stuff around them though)
#define DOG_COMMAND_NONE 0
/// Will try to pick up and bring back whatever you point to
#define DOG_COMMAND_FETCH 1
/// Will get within a few tiles of whatever you point at and continually growl/bark. If the target is a living mob who gets too close, the dog will attack them with bites
#define DOG_COMMAND_ATTACK 2
//enumerators for parsing dog command speech
#define COMMAND_HEEL "Heel"
#define COMMAND_FETCH "Fetch"
#define COMMAND_ATTACK "Attack"
#define COMMAND_DIE "Play Dead"

View File

@@ -32,6 +32,8 @@
#define COMSIG_GLOB_PRE_RANDOM_EVENT "!pre_random_event"
/// Do not allow this random event to continue.
#define CANCEL_PRE_RANDOM_EVENT (1<<0)
/// a person somewhere has thrown something : (mob/living/carbon/carbon_thrower, target)
#define COMSIG_GLOB_CARBON_THROW_THING "!throw_thing"
/// signals from globally accessible objects
@@ -305,6 +307,7 @@
#define COMSIG_CLICK_CTRL "ctrl_click"
///from base of atom/AltClick(): (/mob)
#define COMSIG_CLICK_ALT "alt_click"
#define COMPONENT_CANCEL_CLICK_ALT (1<<0)
///from base of atom/CtrlShiftClick(/mob)
#define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click"
///from base of atom/MouseDrop(): (/atom/over, /mob/user)
@@ -376,6 +379,8 @@
#define COMPONENT_CANCEL_THROW (1<<0)
///from base of atom/movable/throw_at(): (datum/thrownthing, spin)
#define COMSIG_MOVABLE_POST_THROW "movable_post_throw"
///from base of datum/thrownthing/finalize(): (obj/thrown_object, datum/thrownthing) used for when a throw is finished
#define COMSIG_MOVABLE_THROW_LANDED "movable_throw_landed"
///from base of atom/movable/onTransitZ(): (old_z, new_z)
#define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit"
///called when the movable is placed in an unaccessible area, used for stationloving: ()
@@ -467,9 +472,11 @@
///from base of mob/swap_hand(): (obj/item)
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
#define COMPONENT_BLOCK_SWAP (1<<0)
///from base of /mob/verb/pointed: (atom/A)
#define COMSIG_MOB_POINTED "mob_pointed"
///from /obj/structure/door/crush(): (mob/living/crushed, /obj/machinery/door/crushing_door)
#define COMSIG_LIVING_DOORCRUSHED "living_doorcrush"
///from base of mob/living/resist() (/mob/living)
#define COMSIG_LIVING_RESIST "living_resist"
///from base of mob/living/IgniteMob() (/mob/living)

View File

@@ -31,8 +31,6 @@
qdel(pathfind_datum)
SSpathfinder.mobs.found(l)
if(!path)
path = list()
return path
/**
@@ -128,7 +126,7 @@
src.simulated_only = simulated_only
src.avoid = avoid
/// The proc you use to run the search, returns FALSE if it's invalid, an empty list if no path could be found, or a valid path to the target
/// The proc you use to run the search, returns a list with the steps to the destination if one is available, or nothing if one couldn't be found
/datum/pathfind/proc/search()
start = get_turf(caller)
if(!start || !end)

View File

@@ -461,6 +461,8 @@ Turf and target are separate in case you want to teleport some distance from a t
/proc/can_see(atom/source, atom/target, length=5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate.
var/turf/current = get_turf(source)
var/turf/target_turf = get_turf(target)
if(get_dist(source, target) > length)
return FALSE
var/steps = 1
if(current != target_turf)
current = get_step_towards(current, target_turf)

View File

@@ -346,7 +346,8 @@
A.AltClick(src)
/atom/proc/AltClick(mob/user)
SEND_SIGNAL(src, COMSIG_CLICK_ALT, user)
if(SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) & COMPONENT_CANCEL_CLICK_ALT)
return
var/turf/T = get_turf(src)
if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T))
user.listed_turf = T

View File

@@ -188,6 +188,9 @@ SUBSYSTEM_DEF(throwing)
if(T && thrownthing.has_gravity(T))
T.zFall(thrownthing)
if(thrownthing)
SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src)
qdel(src)
/datum/thrownthing/proc/hit_atom(atom/A)

View File

@@ -30,6 +30,10 @@ have ways of interacting with a specific atom and control it. They posses a blac
COOLDOWN_DECLARE(movement_cooldown)
///Delay between movements. This is on the controller so we can keep the movement datum singleton
var/movement_delay = 0.1 SECONDS
///A list for the path we're currently following, if we're using JPS pathing
var/list/movement_path
///Cooldown for JPS movement, how often we're allowed to try making a new path
COOLDOWN_DECLARE(repath_cooldown)
/datum/ai_controller/New(atom/new_pawn)
ai_movement = SSai_movement.movement_types[ai_movement]
@@ -160,3 +164,7 @@ have ways of interacting with a specific atom and control it. They posses a blac
UnregisterSignal(pawn, COMSIG_MOB_LOGOUT)
set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected
RegisterSignal(pawn, COMSIG_MOB_LOGIN, .proc/on_sentience_gained)
/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant
/datum/ai_controller/proc/get_access()
return

View File

@@ -0,0 +1,208 @@
/datum/ai_behavior/battle_screech/dog
screeches = list("barks","howls")
/// Fetching makes the pawn chase after whatever it's targeting and pick it up when it's in range, with the dog_equip behavior
/datum/ai_behavior/fetch
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/fetch/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/mob/living/living_pawn = controller.pawn
var/obj/item/fetch_thing = controller.blackboard[BB_FETCH_TARGET]
if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing)) //either we can't pick it up, or we'd rather eat it, so stop trying.
finish_action(controller, FALSE)
return
if(in_range(living_pawn, fetch_thing))
finish_action(controller, TRUE)
return
finish_action(controller, FALSE)
/datum/ai_behavior/fetch/finish_action(datum/ai_controller/controller, success)
. = ..()
if(!success) //Don't try again on this item if we failed
var/obj/item/target = controller.blackboard[BB_FETCH_TARGET]
if(target)
controller.blackboard[BB_FETCH_IGNORE_LIST][target] = TRUE
controller.blackboard[BB_FETCH_TARGET] = null
controller.blackboard[BB_FETCH_DELIVER_TO] = null
/// This is simply a behaviour to pick up a fetch target
/datum/ai_behavior/simple_equip/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/obj/item/fetch_target = controller.blackboard[BB_FETCH_TARGET]
if(!isturf(fetch_target?.loc)) // someone picked it up or something happened to it
finish_action(controller, FALSE)
return
if(in_range(controller.pawn, fetch_target))
pickup_item(controller, fetch_target)
finish_action(controller, TRUE)
else
finish_action(controller, FALSE)
/datum/ai_behavior/simple_equip/finish_action(datum/ai_controller/controller, success)
. = ..()
controller.blackboard[BB_FETCH_TARGET] = null
/datum/ai_behavior/simple_equip/proc/pickup_item(datum/ai_controller/controller, obj/item/target)
var/atom/pawn = controller.pawn
drop_item(controller)
pawn.visible_message("<span class='notice'>[pawn] picks up [target] in [pawn.p_their()] mouth.</span>")
target.forceMove(pawn)
controller.blackboard[BB_SIMPLE_CARRY_ITEM] = target
return TRUE
/datum/ai_behavior/simple_equip/proc/drop_item(datum/ai_controller/controller)
var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
if(!carried_item)
return
var/atom/pawn = controller.pawn
pawn.visible_message("<span class='notice'>[pawn] drops [carried_item].</span>")
carried_item.forceMove(get_turf(pawn))
controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null
return TRUE
/// This behavior involves dropping off a carried item to a specified person (or place)
/datum/ai_behavior/deliver_item
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/deliver_item/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/mob/living/return_target = controller.blackboard[BB_FETCH_DELIVER_TO]
if(!return_target)
finish_action(controller, FALSE)
if(in_range(controller.pawn, return_target))
deliver_item(controller)
finish_action(controller, TRUE)
/datum/ai_behavior/deliver_item/finish_action(datum/ai_controller/controller, success)
. = ..()
controller.blackboard[BB_FETCH_DELIVER_TO] = null
/// Actually drop the fetched item to the target
/datum/ai_behavior/deliver_item/proc/deliver_item(datum/ai_controller/controller)
var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
var/atom/movable/return_target = controller.blackboard[BB_FETCH_DELIVER_TO]
if(!carried_item || !return_target)
finish_action(controller, FALSE)
return
if(ismob(return_target))
controller.pawn.visible_message("<span class='notice'>[controller.pawn] delivers [carried_item] at [return_target]'s feet.</span>")
else // not sure how to best phrase this
controller.pawn.visible_message("<span class='notice'>[controller.pawn] delivers [carried_item] to [return_target].</span>")
carried_item.forceMove(get_turf(return_target))
controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null
return TRUE
/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
/datum/ai_behavior/eat_snack
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/eat_snack/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/obj/item/snack = controller.current_movement_target
if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc)))
finish_action(controller, FALSE)
var/mob/living/living_pawn = controller.pawn
if(!in_range(living_pawn, snack))
return
if(isturf(snack.loc))
snack.attack_animal(living_pawn) // snack attack!
else if(iscarbon(snack.loc) && DT_PROB(10, delta_time))
living_pawn.manual_emote("stares at [snack.loc]'s [snack.name] with a sad puppy-face.")
if(QDELETED(snack)) // we ate it!
finish_action(controller, TRUE)
/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
/datum/ai_behavior/play_dead
behavior_flags = NONE
/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/mob/living/simple_animal/simple_pawn = controller.pawn
if(!istype(simple_pawn))
return
if(!controller.blackboard[BB_DOG_PLAYING_DEAD])
controller.blackboard[BB_DOG_PLAYING_DEAD] = TRUE
simple_pawn.emote("deathgasp", intentional=FALSE)
simple_pawn.icon_state = simple_pawn.icon_dead
if(simple_pawn.flip_on_death)
simple_pawn.transform = simple_pawn.transform.Turn(180)
simple_pawn.density = FALSE
if(DT_PROB(10, delta_time))
finish_action(controller, TRUE)
/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
var/mob/living/simple_animal/simple_pawn = controller.pawn
if(!istype(simple_pawn) || simple_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :(
return
controller.blackboard[BB_DOG_PLAYING_DEAD] = FALSE
simple_pawn.visible_message("<span class='notice'>[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!</span>")
simple_pawn.icon_state = simple_pawn.icon_living
if(simple_pawn.flip_on_death)
simple_pawn.transform = simple_pawn.transform.Turn(180)
simple_pawn.density = initial(simple_pawn.density)
/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
/datum/ai_behavior/harass
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 3
/datum/ai_behavior/harass/perform(delta_time, datum/ai_controller/controller)
. = ..()
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return
var/atom/movable/harass_target = controller.blackboard[BB_DOG_HARASS_TARGET]
if(!harass_target || !can_see(living_pawn, harass_target, length=AI_DOG_VISION_RANGE))
finish_action(controller, FALSE)
return
if(controller.blackboard[BB_DOG_FRIENDS][harass_target])
living_pawn.visible_message("<span class='danger'>[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression.</span>")
finish_action(controller, FALSE)
return
var/mob/living/living_target = harass_target
if(istype(living_target) && (living_target.stat || HAS_TRAIT(living_target, TRAIT_FAKEDEATH)))
finish_action(controller, TRUE)
return
// subtypes of this behavior can change behavior for how eager/averse the pawn is to attack the target as opposed to falling back/making noise/getting help
if(in_range(living_pawn, living_target))
attack(controller, living_target)
else if(DT_PROB(50, delta_time))
living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!")
/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.blackboard[BB_DOG_HARASS_TARGET] = null
/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior
/datum/ai_behavior/harass/proc/attack(datum/ai_controller/controller, mob/living/living_target)
var/mob/living/living_pawn = controller.pawn
if(!istype(living_pawn))
return
living_pawn.do_attack_animation(living_target, ATTACK_EFFECT_BITE)
living_target.visible_message("<span class='danger'>[living_pawn] bites at [living_target]!</span>", "<span class='userdanger'>[living_pawn] bites at you!</span>", vision_distance = COMBAT_MESSAGE_RANGE)
if(istype(living_target))
living_target.take_bodypart_damage(rand(5, 10))
log_combat(living_pawn, living_target, "bit (AI)")

View File

@@ -0,0 +1,314 @@
/datum/ai_controller/dog
blackboard = list(\
BB_SIMPLE_CARRY_ITEM = null,\
BB_FETCH_TARGET = null,\
BB_FETCH_DELIVER_TO = null,\
BB_DOG_FRIENDS = list(),\
BB_FETCH_IGNORE_LIST = list(),\
BB_DOG_ORDER_MODE = DOG_COMMAND_NONE,\
BB_DOG_PLAYING_DEAD = FALSE,\
BB_DOG_HARASS_TARGET = null)
ai_movement = /datum/ai_movement/jps
COOLDOWN_DECLARE(heel_cooldown)
COOLDOWN_DECLARE(command_cooldown)
COOLDOWN_DECLARE(reset_ignore_cooldown)
/datum/ai_controller/dog/process(delta_time)
if(ismob(pawn))
var/mob/living/living_pawn = pawn
movement_delay = living_pawn.cached_multiplicative_slowdown
return ..()
/datum/ai_controller/dog/TryPossessPawn(atom/new_pawn)
if(!isliving(new_pawn))
return AI_CONTROLLER_INCOMPATIBLE
RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)
RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, .proc/on_examined)
RegisterSignal(new_pawn, COMSIG_CLICK_ALT, .proc/check_altclicked)
RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, .proc/listened_throw)
return ..() //Run parent at end
/datum/ai_controller/dog/UnpossessPawn(destroy)
UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_GLOB_CARBON_THROW_THING, COMSIG_CLICK_ALT))
return ..() //Run parent at end
/datum/ai_controller/dog/able_to_run()
var/mob/living/living_pawn = pawn
if(IS_DEAD_OR_INCAP(living_pawn))
return FALSE
return ..()
/datum/ai_controller/dog/get_access()
var/mob/living/simple_animal/simple_pawn = pawn
if(!istype(simple_pawn))
return
return simple_pawn.access_card
/datum/ai_controller/dog/SelectBehaviors(delta_time)
current_behaviors = list()
var/mob/living/living_pawn = pawn
// occasionally reset our ignore list
if(COOLDOWN_FINISHED(src, reset_ignore_cooldown) && length(blackboard[BB_FETCH_IGNORE_LIST]))
COOLDOWN_START(src, reset_ignore_cooldown, AI_FETCH_IGNORE_DURATION)
blackboard[BB_FETCH_IGNORE_LIST] = list()
// if we were just ordered to heel, chill out for a bit
if(!COOLDOWN_FINISHED(src, heel_cooldown))
return
// if we're not already carrying something and we have a fetch target (and we're not already doing something with it), see if we can eat/equip it
if(!blackboard[BB_SIMPLE_CARRY_ITEM] && blackboard[BB_FETCH_TARGET])
var/atom/movable/interact_target = blackboard[BB_FETCH_TARGET]
if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc)))
current_movement_target = interact_target
if(IS_EDIBLE(interact_target))
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/eat_snack)
else
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/simple_equip)
return
// if we're carrying something and we have a destination to deliver it, do that
if(blackboard[BB_SIMPLE_CARRY_ITEM] && blackboard[BB_FETCH_DELIVER_TO])
var/atom/return_target = blackboard[BB_FETCH_DELIVER_TO]
if(!can_see(pawn, return_target, length=AI_DOG_VISION_RANGE))
// if the return target isn't in sight, we'll just forget about it and carry the thing around
blackboard[BB_FETCH_DELIVER_TO] = null
return
current_movement_target = return_target
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/deliver_item)
return
// occasionally see if there's any loose snacks in sight nearby
if(DT_PROB(40, delta_time))
for(var/obj/item/potential_snack in oview(living_pawn,2))
if(IS_EDIBLE(potential_snack) && (isturf(potential_snack.loc) || ishuman(potential_snack.loc)))
current_movement_target = potential_snack
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/eat_snack)
return
/datum/ai_controller/dog/PerformIdleBehavior(delta_time)
var/mob/living/living_pawn = pawn
if(!isturf(living_pawn.loc) || living_pawn.pulledby)
return
// if we were just ordered to heel, chill out for a bit
if(!COOLDOWN_FINISHED(src, heel_cooldown))
return
// if we're just ditzing around carrying something, occasionally print a message so people know we have something
if(blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time))
var/obj/item/carry_item = blackboard[BB_SIMPLE_CARRY_ITEM]
living_pawn.visible_message("<span class='notice'>[living_pawn] gently teethes on \the [carry_item] in [living_pawn.p_their()] mouth.</span>", vision_distance=COMBAT_MESSAGE_RANGE)
if(DT_PROB(5, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE))
var/move_dir = pick(GLOB.alldirs)
living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
else if(DT_PROB(10, delta_time))
living_pawn.manual_emote(pick("dances around.","chases [living_pawn.p_their()] tail!"))
living_pawn.AddComponent(/datum/component/spinny)
/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands
/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower)
SIGNAL_HANDLER
if(blackboard[BB_FETCH_TARGET] || blackboard[BB_FETCH_DELIVER_TO] || blackboard[BB_DOG_PLAYING_DEAD]) // we're already busy
return
if(!COOLDOWN_FINISHED(src, heel_cooldown))
return
if(!can_see(pawn, carbon_thrower, length=AI_DOG_VISION_RANGE))
return
var/obj/item/thrown_thing = carbon_thrower.get_active_held_item()
if(!isitem(thrown_thing))
return
if(blackboard[BB_FETCH_IGNORE_LIST][thrown_thing])
return
RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, .proc/listen_throw_land)
/// A throw we were listening to has finished, see if it's in range for us to try grabbing it
/datum/ai_controller/dog/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum)
SIGNAL_HANDLER
UnregisterSignal(thrown_thing, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_THROW_LANDED))
if(!istype(thrown_thing) || !isturf(thrown_thing.loc) || !can_see(pawn, thrown_thing, length=AI_DOG_VISION_RANGE))
return
current_movement_target = thrown_thing
blackboard[BB_FETCH_TARGET] = thrown_thing
blackboard[BB_FETCH_DELIVER_TO] = throwing_datum.thrower
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/fetch)
/// Someone's interacting with us by hand, see if they're being nice or mean
/datum/ai_controller/dog/proc/on_attack_hand(datum/source, mob/living/user)
SIGNAL_HANDLER
if(user.combat_mode)
unfriend(user)
else
if(prob(AI_DOG_PET_FRIEND_PROB))
befriend(user)
// if the dog has something in their mouth that they're not bringing to someone for whatever reason, have them drop it when pet by a friend
var/list/friends = blackboard[BB_DOG_FRIENDS]
if(blackboard[BB_SIMPLE_CARRY_ITEM] && !current_movement_target && friends[user])
var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
pawn.visible_message("<span='danger'>[pawn] drops [carried_item] at [user]'s feet!</span>")
// maybe have a dedicated proc for dropping things
carried_item.forceMove(get_turf(user))
blackboard[BB_SIMPLE_CARRY_ITEM] = null
/// Someone is being nice to us, let's make them a friend!
/datum/ai_controller/dog/proc/befriend(mob/living/new_friend)
var/list/friends = blackboard[BB_DOG_FRIENDS]
if(friends[new_friend])
return
if(in_range(pawn, new_friend))
new_friend.visible_message("<b>[pawn]</b> licks at [new_friend] in a friendly manner!", "<span class='notice'>[pawn] licks at you in a friendly manner!</span>")
friends[new_friend] = TRUE
RegisterSignal(new_friend, COMSIG_MOB_POINTED, .proc/check_point)
RegisterSignal(new_friend, COMSIG_MOB_SAY, .proc/check_verbal_command)
/// Someone is being mean to us, take them off our friends (add actual enemies behavior later)
/datum/ai_controller/dog/proc/unfriend(mob/living/ex_friend)
var/list/friends = blackboard[BB_DOG_FRIENDS]
friends[ex_friend] = null
UnregisterSignal(ex_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY))
/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend
/datum/ai_controller/dog/proc/on_examined(datum/source, mob/user, list/examine_text)
SIGNAL_HANDLER
var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
if(carried_item)
examine_text += "<span class='notice'>[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth.</span>"
if(blackboard[BB_DOG_FRIENDS][user])
examine_text += "<span class='notice'>[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!</span>"
/// If we died, drop anything we were carrying
/datum/ai_controller/dog/proc/on_death(mob/living/ol_yeller)
SIGNAL_HANDLER
var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
if(!carried_item)
return
ol_yeller.visible_message("<span='danger'>[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].</span>")
carried_item.forceMove(get_turf(ol_yeller))
blackboard[BB_SIMPLE_CARRY_ITEM] = null
// next section is regarding commands
/// Someone alt clicked us, see if they're someone we should show the radial command menu to
/datum/ai_controller/dog/proc/check_altclicked(datum/source, mob/living/clicker)
SIGNAL_HANDLER
if(!COOLDOWN_FINISHED(src, command_cooldown))
return
if(!istype(clicker) || !blackboard[BB_DOG_FRIENDS][clicker])
return
. = COMPONENT_CANCEL_CLICK_ALT
INVOKE_ASYNC(src, .proc/command_radial, clicker)
/// Show the command radial menu
/datum/ai_controller/dog/proc/command_radial(mob/living/clicker)
var/list/commands = list(
COMMAND_HEEL = image(icon = 'icons/Testing/turf_analysis.dmi', icon_state = "red_arrow"),
COMMAND_FETCH = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"),
COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"),
COMMAND_DIE = image(icon = 'icons/mob/pets.dmi', icon_state = "puppy_dead")
)
var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, .proc/check_menu, clicker), tooltips = TRUE)
if(!choice || !check_menu(clicker))
return
set_command_mode(clicker, choice)
/datum/ai_controller/dog/proc/check_menu(mob/user)
if(!istype(user))
CRASH("A non-mob is trying to issue an order to [pawn].")
if(user.incapacitated() || !can_see(user, pawn))
return FALSE
return TRUE
/// One of our friends said something, see if it's a valid command, and if so, take action
/datum/ai_controller/dog/proc/check_verbal_command(mob/speaker, speech_args)
SIGNAL_HANDLER
if(!blackboard[BB_DOG_FRIENDS][speaker])
return
if(!COOLDOWN_FINISHED(src, command_cooldown))
return
var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words
var/command
if(findtext(spoken_text, "heel") || findtext(spoken_text, "sit") || findtext(spoken_text, "stay"))
command = COMMAND_HEEL
else if(findtext(spoken_text, "fetch") || findtext(spoken_text, "get it"))
command = COMMAND_FETCH
else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic"))
command = COMMAND_ATTACK
else if(findtext(spoken_text, "play dead"))
command = COMMAND_DIE
else
return
if(!can_see(pawn, speaker, length=AI_DOG_VISION_RANGE))
return
set_command_mode(speaker, command)
/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be
/datum/ai_controller/dog/proc/set_command_mode(mob/commander, command)
COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN)
switch(command)
// heel: stop what you're doing, relax and try not to do anything for a little bit
if(COMMAND_HEEL)
pawn.visible_message("<span class='notice'>[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] sit[pawn.p_s()] down obediently, awaiting further orders.</span>")
blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE
COOLDOWN_START(src, heel_cooldown, AI_DOG_HEEL_DURATION)
CancelActions()
// fetch: whatever the commander points to, try and bring it back
if(COMMAND_FETCH)
pawn.visible_message("<span class='notice'>[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] bounce[pawn.p_s()] slightly in anticipation.</span>")
blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_FETCH
// attack: harass whoever the commander points to
if(COMMAND_ATTACK)
pawn.visible_message("<span class='danger'>[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.</span>") // imagine getting intimidated by a corgi
blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_ATTACK
if(COMMAND_DIE)
blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE
CancelActions()
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/play_dead)
/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them)
/datum/ai_controller/dog/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable)
SIGNAL_HANDLER
if(!COOLDOWN_FINISHED(src, command_cooldown))
return
if(pointed_movable == pawn || blackboard[BB_FETCH_TARGET] || !istype(pointed_movable) || blackboard[BB_DOG_ORDER_MODE] == DOG_COMMAND_NONE) // busy or no command
return
if(!can_see(pawn, pointing_friend, length=AI_DOG_VISION_RANGE) || !can_see(pawn, pointed_movable, length=AI_DOG_VISION_RANGE))
return
COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN)
switch(blackboard[BB_DOG_ORDER_MODE])
if(DOG_COMMAND_FETCH)
if(ismob(pointed_movable) || pointed_movable.anchored)
return
pawn.visible_message("<span class='notice'>[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and barks excitedly!</span>")
current_movement_target = pointed_movable
blackboard[BB_FETCH_TARGET] = pointed_movable
blackboard[BB_FETCH_DELIVER_TO] = pointing_friend
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/fetch)
if(DOG_COMMAND_ATTACK)
pawn.visible_message("<span class='notice'>[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!</span>")
current_movement_target = pointed_movable
blackboard[BB_DOG_HARASS_TARGET] = pointed_movable
current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/harass)

View File

@@ -4,6 +4,8 @@
var/list/moving_controllers = list()
///Does this type require processing?
var/requires_processing = TRUE
///How many times a given controller can fail on their route before they just give up
var/max_pathing_attempts
/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target)
controller.pathing_attempts = 0

View File

@@ -1,6 +1,6 @@
///The most braindead type of movement, bee-line to the target with no concern of whats infront of us.
/datum/ai_movement/dumb
max_pathing_attempts = 16
///Put your movement behavior in here!
/datum/ai_movement/dumb/process(delta_time)
@@ -23,5 +23,5 @@
if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move?
controller.pathing_attempts++
if(controller.pathing_attempts >= MAX_PATHING_ATTEMPTS)
if(controller.pathing_attempts >= max_pathing_attempts)
controller.CancelActions()

View File

@@ -0,0 +1,60 @@
/**
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
max_pathing_attempts = 4
///Put your movement behavior in here!
/datum/ai_movement/jps/process(delta_time)
for(var/datum/ai_controller/controller as anything in moving_controllers)
if(!COOLDOWN_FINISHED(controller, movement_cooldown))
continue
COOLDOWN_START(controller, movement_cooldown, controller.movement_delay)
var/atom/movable/movable_pawn = controller.pawn
if(!isturf(movable_pawn.loc)) //No moving if not on a turf
continue
var/minimum_distance = controller.max_target_distance
// right now I'm just taking the shortest minimum distance of our current behaviors, at some point in the future
// we should let whatever sets the current_movement_target also set the min distance and max path length
// (or at least cache it on the controller)
for(var/datum/ai_behavior/iter_behavior as anything in controller.current_behaviors)
if(iter_behavior.required_distance < minimum_distance)
minimum_distance = iter_behavior.required_distance
if(get_dist(movable_pawn, controller.current_movement_target) <= minimum_distance)
continue
var/generate_path = FALSE // set to TRUE when we either have no path, or we failed a step
if(length(controller.movement_path))
var/turf/next_step = controller.movement_path[1]
movable_pawn.Move(next_step)
// this check if we're on exactly the next tile may be overly brittle for dense pawns who may get bumped slightly
// to the side while moving but could maybe still follow their path without needing a whole new path
if(get_turf(movable_pawn) == next_step)
controller.movement_path.Cut(1,2)
else
generate_path = TRUE
else
generate_path = TRUE
if(generate_path)
if(!COOLDOWN_FINISHED(controller, repath_cooldown))
continue
controller.pathing_attempts++
if(controller.pathing_attempts >= max_pathing_attempts)
controller.CancelActions()
continue
COOLDOWN_START(controller, repath_cooldown, 2 SECONDS)
controller.movement_path = get_path_to(movable_pawn, controller.current_movement_target, AI_MAX_PATH_LENGTH, minimum_distance, id=controller.get_access())
/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target)
controller.movement_path = null
return ..()
/datum/ai_movement/jps/stop_moving_towards(datum/ai_controller/controller)
controller.movement_path = null
return ..()

View File

@@ -0,0 +1,33 @@
/**
* spinny.dm
*
* It's a component that spins things a whole bunch, like [proc/dance_rotate] but without the sleeps
*/
/datum/component/spinny
dupe_mode = COMPONENT_DUPE_UNIQUE
/// How many turns are left?
var/steps_left
/// Turns clockwise by default, or counterclockwise if the reverse argument is TRUE
var/turn_degrees = 90
/datum/component/spinny/Initialize(steps = 12, reverse = FALSE)
if(!isatom(parent))
return COMPONENT_INCOMPATIBLE
steps_left = steps
turn_degrees = (reverse ? -90 : 90)
START_PROCESSING(SSfastprocess, src)
/datum/component/spinny/Destroy(force, silent)
STOP_PROCESSING(SSfastprocess, src)
return ..()
/datum/component/spinny/process(delta_time)
steps_left--
var/atom/spinny_boy = parent
if(!istype(spinny_boy) || steps_left <= 0)
qdel(src)
return
// 25% chance to make 2 turns instead of 1 since the old dance_rotate wasn't strictly clockwise/counterclockwise
spinny_boy.setDir(turn(spinny_boy.dir, turn_degrees * (prob(25) ? 2 : 1)))

View File

@@ -138,6 +138,7 @@
/mob/proc/throw_item(atom/target)
SEND_SIGNAL(src, COMSIG_MOB_THROW, target)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CARBON_THROW_THING, src, target)
return
/mob/living/carbon/throw_item(atom/target)

View File

@@ -19,8 +19,8 @@
can_be_held = TRUE
pet_bonus = TRUE
pet_bonus_emote = "woofs happily!"
var/turns_since_scan = 0
var/obj/movement_target
ai_controller = /datum/ai_controller/dog
stop_automated_movement = TRUE
footstep_type = FOOTSTEP_MOB_CLAW
@@ -28,63 +28,6 @@
. = ..()
add_cell_sample()
/mob/living/simple_animal/pet/dog/Life(delta_time = SSMOBS_DT, times_fired)
..()
//Feeding, chasing food, FOOOOODDDD
if(stat || resting || buckled)
return
turns_since_scan++
if(turns_since_scan > 5)
turns_since_scan = 0
if((movement_target) && !(isturf(movement_target.loc) || ishuman(movement_target.loc)))
movement_target = null
stop_automated_movement = FALSE
if(!movement_target || !(movement_target.loc in oview(src, 3)))
movement_target = null
stop_automated_movement = FALSE
for(var/obj/item/potential_snack in oview(src,3))
if(IS_EDIBLE(potential_snack) && (isturf(potential_snack.loc) || ishuman(potential_snack.loc)))
movement_target = potential_snack
break
if(movement_target)
stop_automated_movement = TRUE
step_to(src, movement_target, 1)
sleep(3)
step_to(src, movement_target, 1)
sleep(3)
step_to(src, movement_target, 1)
if(movement_target) //Not redundant due to sleeps, Item can be gone in 6 decisecomds
var/turf/T = get_turf(movement_target)
if(!T)
return
if(T.x < src.x)
setDir(WEST)
else if (T.x > src.x)
setDir(EAST)
else if (T.y < src.y)
setDir(SOUTH)
else if (T.y > src.y)
setDir(NORTH)
else
setDir(SOUTH)
if(!Adjacent(movement_target)) //can't reach food through windows.
return
if(isturf(movement_target.loc))
movement_target.attack_animal(src)
else if(ishuman(movement_target.loc))
if(DT_PROB(10, delta_time))
manual_emote("stares at [movement_target.loc]'s [movement_target] with a sad puppy-face")
if(DT_PROB(0.5, delta_time))
manual_emote(pick("dances around.","chases its tail!"))
INVOKE_ASYNC(GLOBAL_PROC, .proc/dance_rotate, src)
//Corgis and pugs are now under one dog subtype
/mob/living/simple_animal/pet/dog/corgi
@@ -198,6 +141,7 @@
dat += "<tr><td><B>Head:</B></td><td><A href='?src=[REF(src)];[inventory_head ? "remove_inv=head'>[inventory_head]" : "add_inv=head'><font color=grey>Empty</font>"]</A></td></tr>"
dat += "<tr><td><B>Back:</B></td><td><A href='?src=[REF(src)];[inventory_back ? "remove_inv=back'>[inventory_back]" : "add_inv=back'><font color=grey>Empty</font>"]</A></td></tr>"
dat += "<tr><td><B>Collar:</B></td><td><A href='?src=[REF(src)];[pcollar ? "remove_inv=collar'>[pcollar]" : "add_inv=collar'><font color=grey>Empty</font>"]</A></td></tr>"
dat += "<tr><td><B>ID Card:</B></td><td><A href='?src=[REF(src)];[access_card ? "remove_inv=card'>[access_card]" : "add_inv=card'><font color=grey>Empty</font>"]</A></td></tr>"
dat += {"</table>
<A href='?src=[REF(user)];mach_close=mob[REF(src)]'>Close</A>
"}
@@ -281,6 +225,10 @@
pcollar = null
update_corgi_fluff()
regenerate_icons()
if("card")
if(access_card)
usr.put_in_hands(access_card)
access_card = null
show_inv(usr)
@@ -333,9 +281,22 @@
return
item_to_add.forceMove(src)
src.inventory_back = item_to_add
inventory_back = item_to_add
update_corgi_fluff()
regenerate_icons()
if("card")
if(access_card)
to_chat(usr, "<span class='warning'>[src] already has \an [access_card] pinned to [p_them()]!</span>")
return
var/obj/item/item_to_add = usr.get_active_held_item()
if(!usr.temporarilyRemoveItemFromInventory(item_to_add))
to_chat(usr, "<span class='warning'>\The [item_to_add] is stuck to your hand, you cannot pin it to [src]!</span>")
return
if(!istype(item_to_add, /obj/item/card/id))
to_chat(usr, "<span class='warning'>You can't pin [item_to_add] to [src]!</span>")
return
item_to_add.forceMove(src)
access_card = item_to_add
show_inv(usr)
else

View File

@@ -262,7 +262,8 @@
. = ..()
if(stat == DEAD)
. += "<span class='deadsay'>Upon closer examination, [p_they()] appear[p_s()] to be dead.</span>"
if(access_card)
. += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]."
/mob/living/simple_animal/update_stat()
if(status_flags & GODMODE)

View File

@@ -579,6 +579,7 @@
point_at(A)
SEND_SIGNAL(src, COMSIG_MOB_POINTED, A)
return TRUE
/**

View File

@@ -420,12 +420,15 @@
#include "code\datums\ai\_item_behaviors.dm"
#include "code\datums\ai\generic_actions.dm"
#include "code\datums\ai\telegraph_effects.dm"
#include "code\datums\ai\dog\dog_behaviors.dm"
#include "code\datums\ai\dog\dog_controller.dm"
#include "code\datums\ai\hauntium\haunted_controller.dm"
#include "code\datums\ai\monkey\monkey_behaviors.dm"
#include "code\datums\ai\monkey\monkey_controller.dm"
#include "code\datums\ai\movement\_ai_movement.dm"
#include "code\datums\ai\movement\ai_movement_basic_avoidance.dm"
#include "code\datums\ai\movement\ai_movement_dumb.dm"
#include "code\datums\ai\movement\ai_movement_jps.dm"
#include "code\datums\ai\objects\vending_machines\vending_machine_behaviors.dm"
#include "code\datums\ai\objects\vending_machines\vending_machine_controller.dm"
#include "code\datums\ai\robot_customer\robot_customer_behaviors.dm"
@@ -513,6 +516,7 @@
#include "code\datums\components\soundplayer.dm"
#include "code\datums\components\spawner.dm"
#include "code\datums\components\spill.dm"
#include "code\datums\components\spinny.dm"
#include "code\datums\components\spooky.dm"
#include "code\datums\components\squeak.dm"
#include "code\datums\components\stationloving.dm"