Pet Command Component + Regal Rats can order their subjects around (#71590)

## About The Pull Request

Another atomisation of #71421 but I had a fun idea while I was testing
it.

This adds a component based on the existing system for giving
instructions to tamed carp or dogs, but hopefully more modular.
It also gives it to the rat minions of a regal rat.
The basic function allows the mob to listen and react to spoken
commands, which passes things to its AI blackboard. Additionally if you
alt-click a commandable mob it will show a radial menu which both allows
you to select a command, and also contains tooltips explaining what they
do and what audible words trigger it.

<details>
  <summary>Video</summary>
  

https://user-images.githubusercontent.com/7483112/204308693-0eccebec-75c9-411c-81c5-5aa0d682d1a5.mp4

</details>

Now if you riot some rats, you can alt click on them individually to
give them specific orders (more useful for other creatures than rats),
or you can speak out loud to command your legion.
Rats aren't very smart so you can't give them many instructions, but
this is expandable for other creatures.

Additional change: Mice don't squeak if stepped on by other mice because
this made an absolutely unholy noise and I am not sure there's a way to
get non-dense mobs to spread out.

## Why It's Good For The Game

Allows for giving more mobs the ability to be tamed and instructable by
their owner, without copy/pasting code which lives inside a specific
mob.
Yelling at your rats to give them commands is funny. It also adds the
possibility of telling your rats to stop biting someone if they have
agreed to your demands, allowing for more courtly roleplay.
When Regal Rat is converted to a basic mob its AI can also give other
AIs instructions by yelling at them which I think is a good feature.

## Changelog

🆑
add: The followers of Regal Rats will now respond to simple
instructions, if given by their rightful lord. Except frogs. They're too
busy licking themselves and watching the colours.
/🆑
This commit is contained in:
Jacquerel
2022-12-13 20:34:23 +00:00
committed by GitHub
parent 64e2d027c3
commit 38bd952b19
18 changed files with 632 additions and 97 deletions
@@ -9,6 +9,9 @@
var/atom/target = weak_target?.resolve()
if(!target)
return FALSE
var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key]
if (!targetting_datum)
return
controller.set_movement_target(target)
/datum/ai_behavior/basic_melee_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key)
@@ -0,0 +1,18 @@
/**
* # Pet Planning
* Perform behaviour based on what pet commands you have received. This is delegated to the pet command datum.
* When a command is set, we blackboard a key to our currently active command.
* The blackboard also has a weak reference to every command datum available to us.
* We use the key to figure out which datum to run, then ask it to figure out how to execute its action.
*/
/datum/ai_planning_subtree/pet_planning
/datum/ai_planning_subtree/pet_planning/SelectBehaviors(datum/ai_controller/controller, delta_time)
var/active_command_key = controller.blackboard[BB_ACTIVE_PET_COMMAND]
if (!active_command_key)
return // Do something else
var/datum/weakref/weak_command = controller.blackboard[active_command_key]
var/datum/pet_command/command = weak_command?.resolve()
if (!command)
return // We forgot this command at some point
return command.execute_action(controller)
@@ -0,0 +1,19 @@
/// Just keep following the target until the command is interrupted
/datum/ai_behavior/pet_follow_friend
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
/datum/ai_behavior/pet_follow_friend/setup(datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/weak_target = controller.blackboard[target_key]
var/atom/target = weak_target?.resolve()
if (!target)
return FALSE
controller.current_movement_target = target
/datum/ai_behavior/pet_follow_friend/perform(delta_time, datum/ai_controller/controller, target_key)
. = ..()
var/datum/weakref/weak_target = controller.blackboard[target_key]
var/atom/target = weak_target?.resolve()
if (!target)
finish_action(controller, FALSE, target_key)
return
@@ -0,0 +1,27 @@
/// Pet owners can't see their pet's ability cooldowns so we keep attempting to use an ability until we succeed
/datum/ai_behavior/pet_use_ability
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
/datum/ai_behavior/pet_use_ability/setup(datum/ai_controller/controller, ability_key, target_key)
. = ..()
var/datum/weakref/weak_target = controller.blackboard[target_key]
var/mob/living/target = weak_target?.resolve()
if (QDELETED(target))
return FALSE
controller.current_movement_target = target
/datum/ai_behavior/pet_use_ability/perform(delta_time, datum/ai_controller/controller, ability_key, target_key)
var/datum/action/cooldown/mob_cooldown/ability = controller.blackboard[ability_key]
var/datum/weakref/weak_target = controller.blackboard[target_key]
var/mob/living/target = weak_target?.resolve()
if (!ability || QDELETED(target))
finish_action(controller, FALSE, ability_key, target_key)
return
var/mob/pawn = controller.pawn
if (ability.InterceptClickOn(pawn, null, target))
finish_action(controller, TRUE, ability_key, target_key)
/datum/ai_behavior/pet_use_ability/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
. = ..()
controller.blackboard[BB_ACTIVE_PET_COMMAND] = PET_COMMAND_IDLE // Wait for further instruction
controller.blackboard[target_key] = null
@@ -0,0 +1,30 @@
/// Don't target an atom in our friends list (or turfs), anything else is fair game
/datum/targetting_datum/not_friends
///Returns true or false depending on if the target can be attacked by the mob
/datum/targetting_datum/not_friends/can_attack(mob/living/living_mob, atom/target)
if (!target)
return FALSE
if (isturf(target))
return FALSE
if (ismob(target))
var/mob/mob_target = target
if (mob_target.status_flags & GODMODE)
return FALSE
if (living_mob.see_invisible < target.invisibility)
return FALSE
if (isturf(target.loc) && living_mob.z != target.z) // z check will always fail if target is in a mech
return FALSE
if (!living_mob.ai_controller) // How did you get here?
return FALSE
var/list/friends_list = living_mob.ai_controller.blackboard[BB_FRIENDS_LIST]
if (!friends_list)
return TRUE // We don't have any friends, anything's fair game
if (!friends_list[WEAKREF(target)])
return TRUE // This is not our friend, fire at will
return FALSE