mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 01:34:01 +00:00
* Improves dog AI resilience (#65384) About The Pull Request AI dogs currently have a nasty habit of getting stuck when trying to fetch items sometimes, rendering their AI behavior basically dead as they're stuck in a state where they're unable to accept any commands/inputs from their environment. This PR fixes that by adding some more robust checks to make sure a failed fetch attempt doesn't softlock the pups. This PR also adds some growling sounds for dogs in harass mode who are guarding against someone not within biting distance. Why It's Good For The Game Fixes some edge cases where dog AI would stop working entirely. Makes dog AI's in attack mode more conspicuous, and less reliant on text spam to show that. Changelog cl Ryll/Shaps fix: Dog AI's should no longer lock up and become unresponsive after failed fetch attempts soundadd: Dogs in harass mode that are guarding against someone will now make growling sounds /cl * Fixes dog AI lockups when fetching things, adds growling noises for dog attack mode Co-authored-by: Ryll Ryll <3589655+Ryll-Ryll@users.noreply.github.com>
228 lines
9.4 KiB
Plaintext
228 lines
9.4 KiB
Plaintext
/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]
|
|
|
|
//either we can't pick it up, or we'd rather eat it, so stop trying.
|
|
if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing) || !living_pawn.CanReach(fetch_thing))
|
|
finish_action(controller, FALSE)
|
|
return
|
|
|
|
finish_action(controller, TRUE)
|
|
|
|
/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][WEAKREF(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) || !isitem(fetch_target)) // someone picked it up, something happened to it, or it wasn't an item anyway
|
|
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
|
|
if(!success)
|
|
controller.blackboard[BB_FETCH_DELIVER_TO] = 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_notice("[pawn] picks up [target] in [pawn.p_their()] mouth."))
|
|
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_notice("[pawn] drops [carried_item]."))
|
|
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_TARGET] = null
|
|
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_notice("[controller.pawn] delivers [carried_item] at [return_target]'s feet."))
|
|
else // not sure how to best phrase this
|
|
controller.pawn.visible_message(span_notice("[controller.pawn] delivers [carried_item] to [return_target]."))
|
|
|
|
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.set_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_notice("[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!"))
|
|
simple_pawn.icon_state = simple_pawn.icon_living
|
|
if(simple_pawn.flip_on_death)
|
|
simple_pawn.transform = simple_pawn.transform.Turn(180)
|
|
simple_pawn.set_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) || !(isturf(living_pawn.loc) || HAS_TRAIT(living_pawn, TRAIT_AI_BAGATTACK)))
|
|
return
|
|
|
|
var/datum/weakref/harass_ref = controller.blackboard[BB_DOG_HARASS_TARGET]
|
|
var/atom/movable/harass_target = harass_ref.resolve()
|
|
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_ref])
|
|
living_pawn.visible_message(span_danger("[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression."))
|
|
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
|
|
|
|
if(!controller.blackboard[BB_DOG_HARASS_FRUSTRATION])
|
|
controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = world.time
|
|
else if(controller.blackboard[BB_DOG_HARASS_FRUSTRATION] + AI_DOG_HARASS_FRUSTRATE_TIME < world.time) // if we haven't actually bit them in a while, give up
|
|
living_pawn.visible_message(span_danger("[living_pawn] yawns and seems to lose interest in harassing [harass_target]."))
|
|
finish_action(controller, FALSE)
|
|
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(15, delta_time))
|
|
living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!")
|
|
if(DT_PROB(40, delta_time))
|
|
playsound(living_pawn, pick('sound/creatures/dog/growl1.ogg', 'sound/creatures/dog/growl2.ogg'), 50, TRUE, -1)
|
|
|
|
/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded)
|
|
. = ..()
|
|
controller.blackboard[BB_DOG_HARASS_TARGET] = null
|
|
controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = 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
|
|
|
|
controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = world.time
|
|
|
|
// make sure the pawn gets some temporary strength boost to actually attack the target instead of pathetically nuzzling them.
|
|
var/old_melee_lower = living_pawn.melee_damage_lower
|
|
var/old_melee_upper = living_pawn.melee_damage_upper
|
|
living_pawn.melee_damage_lower = max(5, old_melee_lower)
|
|
living_pawn.melee_damage_upper = max(10, old_melee_upper)
|
|
|
|
living_pawn.UnarmedAttack(living_target, FALSE)
|
|
|
|
living_pawn.melee_damage_lower = old_melee_lower
|
|
living_pawn.melee_damage_upper = old_melee_upper
|