[MIRROR] Pet Command Component + Regal Rats can order their subjects around [MDB IGNORE] (#18131)

* 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.
/🆑

* Pet Command Component + Regal Rats can order their subjects around

Co-authored-by: Jacquerel <hnevard@gmail.com>
This commit is contained in:
SkyratBot
2022-12-20 02:19:47 +01:00
committed by GitHub
parent 2f081bf216
commit fcee54df91
18 changed files with 632 additions and 97 deletions

View File

@@ -0,0 +1,70 @@
/**
* # Obeys Commands Component
* Manages a list of pet command datums, allowing you to boss it around
* Creates a radial menu of pet commands when this creature is alt-clicked, if it has any
*/
/datum/component/obeys_commands
/// List of commands you can give to the owner of this component
var/list/available_commands = list()
/// The available_commands parameter should be passed as a list of typepaths
/datum/component/obeys_commands/Initialize(list/command_typepaths = list())
. = ..()
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
var/mob/living/living_parent = parent
if (!living_parent.ai_controller)
return COMPONENT_INCOMPATIBLE
if (!length(command_typepaths))
CRASH("Initialised obedience component with no commands.")
for (var/command_path in command_typepaths)
var/datum/pet_command/new_command = new command_path(parent)
available_commands[new_command.command_name] = new_command
/datum/component/obeys_commands/Destroy(force, silent)
. = ..()
QDEL_NULL(available_commands)
/datum/component/obeys_commands/RegisterWithParent()
RegisterSignal(parent, COMSIG_LIVING_BEFRIENDED, PROC_REF(add_friend))
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(display_menu))
/datum/component/obeys_commands/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_CLICK_ALT))
/datum/component/obeys_commands/proc/add_friend(datum/source, mob/living/new_friend)
SIGNAL_HANDLER
for (var/command_name as anything in available_commands)
var/datum/pet_command/command = available_commands[command_name]
INVOKE_ASYNC(command, TYPE_PROC_REF(/datum/pet_command, add_new_friend), new_friend)
/// Displays a radial menu of commands
/datum/component/obeys_commands/proc/display_menu(datum/source, mob/living/clicker)
SIGNAL_HANDLER
var/mob/living/living_parent = parent
if (IS_DEAD_OR_INCAP(living_parent))
return
if (!living_parent.ai_controller)
return
var/list/friends_list = living_parent.ai_controller.blackboard[BB_FRIENDS_LIST]
if (!friends_list || !friends_list[WEAKREF(clicker)])
return // Not our friend, can't boss us around
INVOKE_ASYNC(src, PROC_REF(display_radial_menu), clicker)
/// Actually display the radial menu and then do something with the result
/datum/component/obeys_commands/proc/display_radial_menu(mob/living/clicker)
var/list/radial_options = list()
for (var/command_name as anything in available_commands)
var/datum/pet_command/command = available_commands[command_name]
radial_options += command.provide_radial_data()
var/pick = show_radial_menu(clicker, clicker, radial_options, require_near = TRUE, tooltips = TRUE)
if (!pick)
return
var/datum/pet_command/picked_command = available_commands[pick]
picked_command.try_activate_command(clicker)

View File

@@ -0,0 +1,143 @@
/**
* # Pet Command
* Set some AI blackboard commands in response to receiving instructions
* This is abstract and should be extended for actual behaviour
*/
/datum/pet_command
/// Weak reference to who follows this command
var/datum/weakref/weak_parent
/// Key for command applied when you receive an order
var/command_key = PET_COMMAND_NONE
/// Unique name used for radial selection, should not be shared with other commands on one mob
var/command_name
/// Description to display in radial menu
var/command_desc
/// Icon to display in radial menu
var/icon/radial_icon
/// Icon state to display in radial menu
var/radial_icon_state
/// Speech strings to listen out for
var/list/speech_commands
/// Shown above the mob's head when it hears you
var/command_feedback
/// How close a mob needs to be to a target to respond to a command
var/sense_radius = 7
/datum/pet_command/New(mob/living/parent)
. = ..()
weak_parent = WEAKREF(parent)
parent.ai_controller.blackboard[command_key] = WEAKREF(src)
/// Register a new guy we want to listen to
/datum/pet_command/proc/add_new_friend(mob/living/tamer)
RegisterSignal(tamer, COMSIG_MOB_SAY, PROC_REF(respond_to_command))
/// Respond to something that one of our friends has asked us to do
/datum/pet_command/proc/respond_to_command(mob/living/speaker, speech_args)
SIGNAL_HANDLER
var/mob/living/parent = weak_parent.resolve()
if (!parent)
return
if (!can_see(parent, speaker, sense_radius)) // Basically the same rules as hearing
return
var/spoken_text = speech_args[SPEECH_MESSAGE]
if (!find_command_in_text(spoken_text))
return
try_activate_command(speaker)
/// Returns true if we find any of our spoken commands in the text
/datum/pet_command/proc/find_command_in_text(spoken_text)
for (var/command as anything in speech_commands)
if (!findtext(spoken_text, command))
continue
return TRUE
return FALSE
/// Apply a command state if conditions are right, return command if successful
/datum/pet_command/proc/try_activate_command(mob/living/commander)
var/mob/living/parent = weak_parent.resolve()
if (!parent)
return
if (!parent.ai_controller) // We stopped having a brain at some point
return
if (IS_DEAD_OR_INCAP(parent)) // Probably can't hear them if we're dead
return
if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] == command_key) // We're already doing it
return
set_command_active(parent, commander)
/// Activate the command, extend to add visible messages and the like
/datum/pet_command/proc/set_command_active(mob/living/parent, mob/living/commander)
set_command_target(parent, null)
parent.ai_controller.CancelActions() // Stop whatever you're doing and do this instead
parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] = command_key
if (command_feedback)
parent.balloon_alert_to_viewers("[command_feedback]") // If we get a nicer runechat way to do this, refactor this
/// Store the target for the AI blackboard
/datum/pet_command/proc/set_command_target(mob/living/parent, atom/target)
parent.ai_controller.blackboard[BB_CURRENT_PET_TARGET] = WEAKREF(target)
/// Provide information about how to display this command in a radial menu
/datum/pet_command/proc/provide_radial_data()
var/datum/radial_menu_choice/choice = new()
choice.name = command_name
choice.image = icon(icon = radial_icon, icon_state = radial_icon_state)
var/tooltip = command_desc
if (length(speech_commands))
tooltip += "<br>Speak this command with the words [speech_commands.Join(", ")]."
choice.info = tooltip
return list("[command_name]" = choice)
/**
* Execute an AI action on the provided controller, what we should actually do when this command is active.
* This should basically always be called from a planning subtree which passes its own controller.
* Return SUBTREE_RETURN_FINISH_PLANNING to pass that instruction on to the controller, or don't if you don't want that.
*/
/datum/pet_command/proc/execute_action(datum/ai_controller/controller)
SHOULD_CALL_PARENT(FALSE)
CRASH("Pet command execute action not implemented.")
/**
* # Point Targetting Pet Command
* As above but also listens for you pointing at something and marks it as a target
*/
/datum/pet_command/point_targetting
/// Text describing an action we perform upon receiving a new target
var/pointed_reaction
/// Blackboard key for targetting datum, this is likely going to need it
var/targetting_datum_key = BB_PET_TARGETTING_DATUM
/datum/pet_command/point_targetting/add_new_friend(mob/living/tamer)
. = ..()
RegisterSignal(tamer, COMSIG_MOB_POINTED, PROC_REF(look_for_target))
/// Target the pointed atom for actions
/datum/pet_command/point_targetting/proc/look_for_target(mob/living/friend, atom/pointed_atom)
SIGNAL_HANDLER
var/mob/living/parent = weak_parent.resolve()
if (!parent)
return
if (!parent.ai_controller)
return
if (IS_DEAD_OR_INCAP(parent))
return
if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] != command_key) // We're not listening right now
return
if (parent.ai_controller.blackboard[BB_CURRENT_PET_TARGET] == WEAKREF(pointed_atom)) // That's already our target
return
if (!can_see(parent, pointed_atom, sense_radius))
return
parent.ai_controller.CancelActions()
// Deciding if they can actually do anything with this target is the behaviour's job
set_command_target(parent, pointed_atom)
// These are usually hostile actions so should have a record in chat
parent.visible_message(span_warning("[parent] follows [friend]'s gesture towards [pointed_atom] and [pointed_reaction]!"))

View File

@@ -0,0 +1,124 @@
// None of these are really complex enough to merit their own file
/**
* # Pet Command: Idle
* Tells a pet to resume its idle behaviour, usually staying put where you leave it
*/
/datum/pet_command/idle
command_name = "Stay"
command_desc = "Command your pet to stay idle in this location."
radial_icon = 'icons/testing/turf_analysis.dmi'
radial_icon_state = "red_arrow"
command_key = PET_COMMAND_IDLE
speech_commands = list("sit", "stay", "stop")
command_feedback = "sits"
/datum/pet_command/idle/execute_action(datum/ai_controller/controller)
return SUBTREE_RETURN_FINISH_PLANNING // This cancels further AI planning
/**
* # Pet Command: Stop
* Tells a pet to exit command mode and resume its normal behaviour, which includes regular target-seeking and what have you
*/
/datum/pet_command/free
command_name = "Loose"
command_desc = "Allow your pet to resume its natural behaviours."
radial_icon = 'icons/mob/actions/actions_spells.dmi'
radial_icon_state = "repulse"
command_key = PET_COMMAND_NONE
speech_commands = list("free", "loose")
command_feedback = "relaxes"
/datum/pet_command/free/execute_action(datum/ai_controller/controller)
return // Just move on to the next planning subtree.
/**
* # Pet Command: Follow
* Tells a pet to follow you until you tell it to do something else
*/
/datum/pet_command/follow
command_name = "Follow"
command_desc = "Command your pet to accompany you."
radial_icon = 'icons/mob/actions/actions_spells.dmi'
radial_icon_state = "summons"
command_key = PET_COMMAND_FOLLOW
speech_commands = list("heel", "follow")
/datum/pet_command/follow/set_command_active(mob/living/parent, mob/living/commander)
. = ..()
set_command_target(parent, commander)
/datum/pet_command/follow/execute_action(datum/ai_controller/controller)
controller.queue_behavior(/datum/ai_behavior/pet_follow_friend, BB_CURRENT_PET_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/**
* # Pet Command: Attack
* Tells a pet to chase and bite the next thing you point at
*/
/datum/pet_command/point_targetting/attack
command_name = "Attack"
command_desc = "Command your pet to attack things that you point out to it."
radial_icon = 'icons/effects/effects.dmi'
radial_icon_state = "bite"
command_key = PET_COMMAND_ATTACK
speech_commands = list("attack", "sic", "kill")
command_feedback = "growl"
pointed_reaction = "growls"
/// Balloon alert to display if providing an invalid target
var/refuse_reaction = "shakes head"
/// Attack behaviour to use, generally you will want to override this to add some kind of cooldown
var/attack_behaviour = /datum/ai_behavior/basic_melee_attack
// Refuse to target things we can't target, chiefly other friends
/datum/pet_command/point_targetting/attack/set_command_target(mob/living/parent, atom/target)
if (!target)
return
var/mob/living/living_parent = parent
if (!living_parent.ai_controller)
return
var/datum/targetting_datum/targeter = living_parent.ai_controller.blackboard[targetting_datum_key]
if (!targeter)
return
if (!targeter.can_attack(living_parent, target))
refuse_target(parent, target)
return
return ..()
/// Display feedback about not targetting something
/datum/pet_command/point_targetting/attack/proc/refuse_target(mob/living/parent, atom/target)
var/mob/living/living_parent = parent
living_parent.balloon_alert_to_viewers("[refuse_reaction]")
living_parent.visible_message(span_notice("[living_parent] refuses to attack [target]."))
/datum/pet_command/point_targetting/attack/execute_action(datum/ai_controller/controller)
controller.queue_behavior(attack_behaviour, BB_CURRENT_PET_TARGET, targetting_datum_key)
return SUBTREE_RETURN_FINISH_PLANNING
/**
* # Pet Command: Targetted Ability
* Tells a pet to use some kind of ability on the next thing you point at
*/
/datum/pet_command/point_targetting/use_ability
command_name = "Use ability"
command_desc = "Command your pet to use one of its special skills on something that you point out to it."
radial_icon = 'icons/mob/actions/actions_spells.dmi'
radial_icon_state = "projectile"
command_key = PET_COMMAND_USE_ABILITY
speech_commands = list("shoot", "blast", "cast")
command_feedback = "growl"
pointed_reaction = "growls"
/// Blackboard key where a reference to some kind of mob ability is stored
var/pet_ability_key
/datum/pet_command/point_targetting/use_ability/execute_action(datum/ai_controller/controller)
if (!pet_ability_key)
return
var/datum/action/cooldown/using_action = controller.blackboard[pet_ability_key]
if (QDELETED(using_action))
return
// We don't check if the target exists because we want to 'sit attentively' if we've been instructed to attack but not given one yet
// We also don't check if the cooldown is over because there's no way a pet owner can know that, the behaviour will handle it
controller.queue_behavior(/datum/ai_behavior/pet_use_ability, pet_ability_key, targetting_datum_key)
return SUBTREE_RETURN_FINISH_PLANNING