mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 10:11:09 +00:00
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:
@@ -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 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)
|
#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
|
///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 MAX_PATHING_ATTEMPTS 16
|
#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()
|
///Flags for ai_behavior new()
|
||||||
#define AI_CONTROLLER_INCOMPATIBLE (1<<0)
|
#define AI_CONTROLLER_INCOMPATIBLE (1<<0)
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown"
|
#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///Haunted item controller defines
|
///Haunted item controller defines
|
||||||
|
|
||||||
///Chance for haunted item to haunt someone
|
///Chance for haunted item to haunt someone
|
||||||
@@ -69,3 +68,40 @@
|
|||||||
#define BB_CUSTOMER_EATING "BB_customer_eating"
|
#define BB_CUSTOMER_EATING "BB_customer_eating"
|
||||||
#define BB_CUSTOMER_ATTENDING_VENUE "BB_customer_attending_avenue"
|
#define BB_CUSTOMER_ATTENDING_VENUE "BB_customer_attending_avenue"
|
||||||
#define BB_CUSTOMER_LEAVING "BB_customer_leaving"
|
#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"
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
#define COMSIG_GLOB_PRE_RANDOM_EVENT "!pre_random_event"
|
#define COMSIG_GLOB_PRE_RANDOM_EVENT "!pre_random_event"
|
||||||
/// Do not allow this random event to continue.
|
/// Do not allow this random event to continue.
|
||||||
#define CANCEL_PRE_RANDOM_EVENT (1<<0)
|
#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
|
/// signals from globally accessible objects
|
||||||
|
|
||||||
@@ -305,6 +307,7 @@
|
|||||||
#define COMSIG_CLICK_CTRL "ctrl_click"
|
#define COMSIG_CLICK_CTRL "ctrl_click"
|
||||||
///from base of atom/AltClick(): (/mob)
|
///from base of atom/AltClick(): (/mob)
|
||||||
#define COMSIG_CLICK_ALT "alt_click"
|
#define COMSIG_CLICK_ALT "alt_click"
|
||||||
|
#define COMPONENT_CANCEL_CLICK_ALT (1<<0)
|
||||||
///from base of atom/CtrlShiftClick(/mob)
|
///from base of atom/CtrlShiftClick(/mob)
|
||||||
#define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click"
|
#define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click"
|
||||||
///from base of atom/MouseDrop(): (/atom/over, /mob/user)
|
///from base of atom/MouseDrop(): (/atom/over, /mob/user)
|
||||||
@@ -376,6 +379,8 @@
|
|||||||
#define COMPONENT_CANCEL_THROW (1<<0)
|
#define COMPONENT_CANCEL_THROW (1<<0)
|
||||||
///from base of atom/movable/throw_at(): (datum/thrownthing, spin)
|
///from base of atom/movable/throw_at(): (datum/thrownthing, spin)
|
||||||
#define COMSIG_MOVABLE_POST_THROW "movable_post_throw"
|
#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)
|
///from base of atom/movable/onTransitZ(): (old_z, new_z)
|
||||||
#define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit"
|
#define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit"
|
||||||
///called when the movable is placed in an unaccessible area, used for stationloving: ()
|
///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)
|
///from base of mob/swap_hand(): (obj/item)
|
||||||
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
|
#define COMSIG_MOB_SWAP_HANDS "mob_swap_hands"
|
||||||
#define COMPONENT_BLOCK_SWAP (1<<0)
|
#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)
|
///from /obj/structure/door/crush(): (mob/living/crushed, /obj/machinery/door/crushing_door)
|
||||||
#define COMSIG_LIVING_DOORCRUSHED "living_doorcrush"
|
#define COMSIG_LIVING_DOORCRUSHED "living_doorcrush"
|
||||||
|
|
||||||
///from base of mob/living/resist() (/mob/living)
|
///from base of mob/living/resist() (/mob/living)
|
||||||
#define COMSIG_LIVING_RESIST "living_resist"
|
#define COMSIG_LIVING_RESIST "living_resist"
|
||||||
///from base of mob/living/IgniteMob() (/mob/living)
|
///from base of mob/living/IgniteMob() (/mob/living)
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
qdel(pathfind_datum)
|
qdel(pathfind_datum)
|
||||||
|
|
||||||
SSpathfinder.mobs.found(l)
|
SSpathfinder.mobs.found(l)
|
||||||
if(!path)
|
|
||||||
path = list()
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +126,7 @@
|
|||||||
src.simulated_only = simulated_only
|
src.simulated_only = simulated_only
|
||||||
src.avoid = avoid
|
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()
|
/datum/pathfind/proc/search()
|
||||||
start = get_turf(caller)
|
start = get_turf(caller)
|
||||||
if(!start || !end)
|
if(!start || !end)
|
||||||
|
|||||||
@@ -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.
|
/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/current = get_turf(source)
|
||||||
var/turf/target_turf = get_turf(target)
|
var/turf/target_turf = get_turf(target)
|
||||||
|
if(get_dist(source, target) > length)
|
||||||
|
return FALSE
|
||||||
var/steps = 1
|
var/steps = 1
|
||||||
if(current != target_turf)
|
if(current != target_turf)
|
||||||
current = get_step_towards(current, target_turf)
|
current = get_step_towards(current, target_turf)
|
||||||
|
|||||||
@@ -346,7 +346,8 @@
|
|||||||
A.AltClick(src)
|
A.AltClick(src)
|
||||||
|
|
||||||
/atom/proc/AltClick(mob/user)
|
/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)
|
var/turf/T = get_turf(src)
|
||||||
if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T))
|
if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T))
|
||||||
user.listed_turf = T
|
user.listed_turf = T
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ SUBSYSTEM_DEF(throwing)
|
|||||||
if(T && thrownthing.has_gravity(T))
|
if(T && thrownthing.has_gravity(T))
|
||||||
T.zFall(thrownthing)
|
T.zFall(thrownthing)
|
||||||
|
|
||||||
|
if(thrownthing)
|
||||||
|
SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src)
|
||||||
|
|
||||||
qdel(src)
|
qdel(src)
|
||||||
|
|
||||||
/datum/thrownthing/proc/hit_atom(atom/A)
|
/datum/thrownthing/proc/hit_atom(atom/A)
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ have ways of interacting with a specific atom and control it. They posses a blac
|
|||||||
COOLDOWN_DECLARE(movement_cooldown)
|
COOLDOWN_DECLARE(movement_cooldown)
|
||||||
///Delay between movements. This is on the controller so we can keep the movement datum singleton
|
///Delay between movements. This is on the controller so we can keep the movement datum singleton
|
||||||
var/movement_delay = 0.1 SECONDS
|
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)
|
/datum/ai_controller/New(atom/new_pawn)
|
||||||
ai_movement = SSai_movement.movement_types[ai_movement]
|
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)
|
UnregisterSignal(pawn, COMSIG_MOB_LOGOUT)
|
||||||
set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected
|
set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected
|
||||||
RegisterSignal(pawn, COMSIG_MOB_LOGIN, .proc/on_sentience_gained)
|
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
|
||||||
|
|||||||
208
code/datums/ai/dog/dog_behaviors.dm
Normal file
208
code/datums/ai/dog/dog_behaviors.dm
Normal 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)")
|
||||||
314
code/datums/ai/dog/dog_controller.dm
Normal file
314
code/datums/ai/dog/dog_controller.dm
Normal 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)
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
var/list/moving_controllers = list()
|
var/list/moving_controllers = list()
|
||||||
///Does this type require processing?
|
///Does this type require processing?
|
||||||
var/requires_processing = TRUE
|
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)
|
/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target)
|
||||||
controller.pathing_attempts = 0
|
controller.pathing_attempts = 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
///The most braindead type of movement, bee-line to the target with no concern of whats infront of us.
|
///The most braindead type of movement, bee-line to the target with no concern of whats infront of us.
|
||||||
/datum/ai_movement/dumb
|
/datum/ai_movement/dumb
|
||||||
|
max_pathing_attempts = 16
|
||||||
|
|
||||||
///Put your movement behavior in here!
|
///Put your movement behavior in here!
|
||||||
/datum/ai_movement/dumb/process(delta_time)
|
/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?
|
if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move?
|
||||||
controller.pathing_attempts++
|
controller.pathing_attempts++
|
||||||
if(controller.pathing_attempts >= MAX_PATHING_ATTEMPTS)
|
if(controller.pathing_attempts >= max_pathing_attempts)
|
||||||
controller.CancelActions()
|
controller.CancelActions()
|
||||||
|
|||||||
60
code/datums/ai/movement/ai_movement_jps.dm
Normal file
60
code/datums/ai/movement/ai_movement_jps.dm
Normal 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 ..()
|
||||||
33
code/datums/components/spinny.dm
Normal file
33
code/datums/components/spinny.dm
Normal 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)))
|
||||||
@@ -138,6 +138,7 @@
|
|||||||
|
|
||||||
/mob/proc/throw_item(atom/target)
|
/mob/proc/throw_item(atom/target)
|
||||||
SEND_SIGNAL(src, COMSIG_MOB_THROW, target)
|
SEND_SIGNAL(src, COMSIG_MOB_THROW, target)
|
||||||
|
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CARBON_THROW_THING, src, target)
|
||||||
return
|
return
|
||||||
|
|
||||||
/mob/living/carbon/throw_item(atom/target)
|
/mob/living/carbon/throw_item(atom/target)
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
can_be_held = TRUE
|
can_be_held = TRUE
|
||||||
pet_bonus = TRUE
|
pet_bonus = TRUE
|
||||||
pet_bonus_emote = "woofs happily!"
|
pet_bonus_emote = "woofs happily!"
|
||||||
var/turns_since_scan = 0
|
ai_controller = /datum/ai_controller/dog
|
||||||
var/obj/movement_target
|
stop_automated_movement = TRUE
|
||||||
|
|
||||||
footstep_type = FOOTSTEP_MOB_CLAW
|
footstep_type = FOOTSTEP_MOB_CLAW
|
||||||
|
|
||||||
@@ -28,63 +28,6 @@
|
|||||||
. = ..()
|
. = ..()
|
||||||
add_cell_sample()
|
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
|
//Corgis and pugs are now under one dog subtype
|
||||||
|
|
||||||
/mob/living/simple_animal/pet/dog/corgi
|
/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>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>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>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>
|
dat += {"</table>
|
||||||
<A href='?src=[REF(user)];mach_close=mob[REF(src)]'>Close</A>
|
<A href='?src=[REF(user)];mach_close=mob[REF(src)]'>Close</A>
|
||||||
"}
|
"}
|
||||||
@@ -281,6 +225,10 @@
|
|||||||
pcollar = null
|
pcollar = null
|
||||||
update_corgi_fluff()
|
update_corgi_fluff()
|
||||||
regenerate_icons()
|
regenerate_icons()
|
||||||
|
if("card")
|
||||||
|
if(access_card)
|
||||||
|
usr.put_in_hands(access_card)
|
||||||
|
access_card = null
|
||||||
|
|
||||||
show_inv(usr)
|
show_inv(usr)
|
||||||
|
|
||||||
@@ -333,9 +281,22 @@
|
|||||||
return
|
return
|
||||||
|
|
||||||
item_to_add.forceMove(src)
|
item_to_add.forceMove(src)
|
||||||
src.inventory_back = item_to_add
|
inventory_back = item_to_add
|
||||||
update_corgi_fluff()
|
update_corgi_fluff()
|
||||||
regenerate_icons()
|
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)
|
show_inv(usr)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -262,7 +262,8 @@
|
|||||||
. = ..()
|
. = ..()
|
||||||
if(stat == DEAD)
|
if(stat == DEAD)
|
||||||
. += "<span class='deadsay'>Upon closer examination, [p_they()] appear[p_s()] to be dead.</span>"
|
. += "<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()
|
/mob/living/simple_animal/update_stat()
|
||||||
if(status_flags & GODMODE)
|
if(status_flags & GODMODE)
|
||||||
|
|||||||
@@ -579,6 +579,7 @@
|
|||||||
|
|
||||||
point_at(A)
|
point_at(A)
|
||||||
|
|
||||||
|
SEND_SIGNAL(src, COMSIG_MOB_POINTED, A)
|
||||||
return TRUE
|
return TRUE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -420,12 +420,15 @@
|
|||||||
#include "code\datums\ai\_item_behaviors.dm"
|
#include "code\datums\ai\_item_behaviors.dm"
|
||||||
#include "code\datums\ai\generic_actions.dm"
|
#include "code\datums\ai\generic_actions.dm"
|
||||||
#include "code\datums\ai\telegraph_effects.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\hauntium\haunted_controller.dm"
|
||||||
#include "code\datums\ai\monkey\monkey_behaviors.dm"
|
#include "code\datums\ai\monkey\monkey_behaviors.dm"
|
||||||
#include "code\datums\ai\monkey\monkey_controller.dm"
|
#include "code\datums\ai\monkey\monkey_controller.dm"
|
||||||
#include "code\datums\ai\movement\_ai_movement.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_basic_avoidance.dm"
|
||||||
#include "code\datums\ai\movement\ai_movement_dumb.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_behaviors.dm"
|
||||||
#include "code\datums\ai\objects\vending_machines\vending_machine_controller.dm"
|
#include "code\datums\ai\objects\vending_machines\vending_machine_controller.dm"
|
||||||
#include "code\datums\ai\robot_customer\robot_customer_behaviors.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\soundplayer.dm"
|
||||||
#include "code\datums\components\spawner.dm"
|
#include "code\datums\components\spawner.dm"
|
||||||
#include "code\datums\components\spill.dm"
|
#include "code\datums\components\spill.dm"
|
||||||
|
#include "code\datums\components\spinny.dm"
|
||||||
#include "code\datums\components\spooky.dm"
|
#include "code\datums\components\spooky.dm"
|
||||||
#include "code\datums\components\squeak.dm"
|
#include "code\datums\components\squeak.dm"
|
||||||
#include "code\datums\components\stationloving.dm"
|
#include "code\datums\components\stationloving.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user