mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 18:22:14 +00:00
223 lines
8.5 KiB
Plaintext
223 lines
8.5 KiB
Plaintext
/**
|
|
* # 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
|
|
/// 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
|
|
/// If true, command will not appear in radial menu and can only be accessed through speech
|
|
var/hidden = FALSE
|
|
/// Icon to display in radial menu
|
|
var/icon/radial_icon = 'icons/hud/radial_pets.dmi'
|
|
/// Icon state to display in radial menu
|
|
var/radial_icon_state
|
|
/// Speech strings to listen out for
|
|
var/list/speech_commands = list()
|
|
/// Callout that triggers this command
|
|
var/callout_type
|
|
/// 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
|
|
/// does this pet command need a point to activate?
|
|
var/requires_pointing = FALSE
|
|
/// Blackboard key for targeting strategy, this is likely going to need it
|
|
var/targeting_strategy_key = BB_PET_TARGETING_STRATEGY
|
|
///our pointed reaction we play
|
|
var/pointed_reaction
|
|
|
|
/datum/pet_command/New(mob/living/parent)
|
|
. = ..()
|
|
weak_parent = WEAKREF(parent)
|
|
|
|
/// 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))
|
|
RegisterSignal(tamer, COMSIG_MOB_AUTOMUTE_CHECK, PROC_REF(waive_automute))
|
|
RegisterSignal(tamer, COMSIG_MOB_CREATED_CALLOUT, PROC_REF(respond_to_callout))
|
|
if(requires_pointing)
|
|
RegisterSignal(tamer, COMSIG_MOVABLE_POINTED, PROC_REF(point_on_target))
|
|
|
|
/// Stop listening to a guy
|
|
/datum/pet_command/proc/remove_friend(mob/living/unfriended)
|
|
UnregisterSignal(unfriended, list(
|
|
COMSIG_MOB_SAY,
|
|
COMSIG_MOB_AUTOMUTE_CHECK,
|
|
COMSIG_MOB_CREATED_CALLOUT,
|
|
COMSIG_MOVABLE_POINTED,
|
|
))
|
|
|
|
/// Stop the automute from triggering for commands (unless the spoken text is suspiciously longer than the command)
|
|
/datum/pet_command/proc/waive_automute(mob/living/speaker, client/client, last_message, mute_type)
|
|
SIGNAL_HANDLER
|
|
if(mute_type == MUTE_IC && find_command_in_text(last_message, check_verbosity = TRUE))
|
|
return WAIVE_AUTOMUTE_CHECK
|
|
return NONE
|
|
|
|
/// 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(commander = speaker, radial_command = FALSE)
|
|
|
|
/// Respond to a callout
|
|
/datum/pet_command/proc/respond_to_callout(mob/living/speaker, datum/callout_option/callout, atom/target)
|
|
SIGNAL_HANDLER
|
|
|
|
if (isnull(callout_type) || !ispath(callout, callout_type))
|
|
return
|
|
|
|
var/mob/living/parent = weak_parent.resolve()
|
|
if (!parent)
|
|
return
|
|
|
|
if (!valid_callout_target(speaker, callout, target))
|
|
var/found_new_target = FALSE
|
|
for (var/atom/new_target in range(2, target))
|
|
if (valid_callout_target(speaker, callout, new_target))
|
|
target = new_target
|
|
found_new_target = TRUE
|
|
|
|
if (!found_new_target)
|
|
return
|
|
|
|
if (try_activate_command(commander = speaker, radial_command = FALSE))
|
|
look_for_target(parent, target)
|
|
|
|
/// Does this callout with this target trigger this command?
|
|
/datum/pet_command/proc/valid_callout_target(mob/living/speaker, datum/callout_option/callout, atom/target)
|
|
return TRUE
|
|
|
|
/**
|
|
* Returns true if we find any of our spoken commands in the text.
|
|
* if check_verbosity is true, skip the match if there spoken_text is way longer than the match
|
|
*/
|
|
/datum/pet_command/proc/find_command_in_text(spoken_text, check_verbosity = FALSE)
|
|
for (var/command in speech_commands)
|
|
if (!findtext(spoken_text, command))
|
|
continue
|
|
if(check_verbosity && length(spoken_text) > length(command) + MAX_NAME_LEN)
|
|
continue
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/datum/pet_command/proc/pet_able_to_respond()
|
|
var/mob/living/parent = weak_parent.resolve()
|
|
if(isnull(parent) || isnull(parent.ai_controller))
|
|
return FALSE
|
|
if(IS_DEAD_OR_INCAP(parent)) // Probably can't hear them if we're dead
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Apply a command state if conditions are right, return command if successful
|
|
/datum/pet_command/proc/try_activate_command(mob/living/commander, radial_command)
|
|
if(!pet_able_to_respond())
|
|
return FALSE
|
|
var/mob/living/parent = weak_parent.resolve()
|
|
set_command_active(parent, commander, radial_command)
|
|
return TRUE
|
|
|
|
/datum/pet_command/proc/generate_emote_command(atom/target)
|
|
var/mob/living/living_pet = weak_parent?.resolve()
|
|
return isnull(living_pet) ? null : retrieve_command_text(living_pet, target)
|
|
|
|
/datum/pet_command/proc/retrieve_command_text(atom/living_pet, atom/target)
|
|
return "signals [living_pet] to spring into action!"
|
|
|
|
/// Target the pointed atom for actions
|
|
/datum/pet_command/proc/look_for_target(mob/living/friend, atom/potential_target)
|
|
var/mob/living/parent = weak_parent.resolve()
|
|
if(!pet_able_to_respond())
|
|
return FALSE
|
|
if (parent.ai_controller.blackboard[BB_CURRENT_PET_TARGET] == potential_target) // That's already our target
|
|
return FALSE
|
|
if (!can_see(parent, potential_target, sense_radius))
|
|
return FALSE
|
|
|
|
parent.ai_controller.CancelActions()
|
|
set_command_target(parent, potential_target)
|
|
return TRUE
|
|
|
|
/// Activate the command, extend to add visible messages and the like
|
|
/datum/pet_command/proc/set_command_active(mob/living/parent, mob/living/commander, radial_command = FALSE)
|
|
parent.ai_controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
|
|
|
|
parent.ai_controller.CancelActions() // Stop whatever you're doing and do this instead
|
|
parent.ai_controller.set_blackboard_key(BB_ACTIVE_PET_COMMAND, src)
|
|
if (command_feedback)
|
|
parent.balloon_alert_to_viewers("[command_feedback]") // If we get a nicer runechat way to do this, refactor this
|
|
if(!radial_command)
|
|
return
|
|
if(!requires_pointing)
|
|
var/manual_emote_text = generate_emote_command()
|
|
commander.manual_emote(manual_emote_text)
|
|
return
|
|
RegisterSignal(commander, COMSIG_MOB_CLICKON, PROC_REF(click_on_target))
|
|
commander.client?.mouse_override_icon = 'icons/effects/mouse_pointers/pet_paw.dmi'
|
|
commander.update_mouse_pointer()
|
|
|
|
/datum/pet_command/proc/click_on_target(mob/living/source, atom/target, list/modifiers)
|
|
SIGNAL_HANDLER
|
|
if(!can_see(source, target, 9))
|
|
return COMSIG_MOB_CANCEL_CLICKON
|
|
var/manual_emote_text = generate_emote_command(target)
|
|
if(on_target_set(source, target) && !isnull(manual_emote_text))
|
|
INVOKE_ASYNC(source, TYPE_PROC_REF(/atom, manual_emote), manual_emote_text)
|
|
UnregisterSignal(source, COMSIG_MOB_CLICKON)
|
|
source.client?.mouse_override_icon = source.client::mouse_override_icon
|
|
source.update_mouse_pointer()
|
|
return COMSIG_MOB_CANCEL_CLICKON
|
|
|
|
/datum/pet_command/proc/point_on_target(mob/living/friend, atom/potential_target)
|
|
SIGNAL_HANDLER
|
|
on_target_set(friend, potential_target)
|
|
|
|
/// Store the target for the AI blackboard
|
|
/datum/pet_command/proc/set_command_target(mob/living/parent, atom/target)
|
|
parent.ai_controller.set_blackboard_key(BB_CURRENT_PET_TARGET, target)
|
|
return TRUE
|
|
|
|
/// Provide information about how to display this command in a radial menu
|
|
/datum/pet_command/proc/provide_radial_data()
|
|
if (hidden)
|
|
return
|
|
var/datum/radial_menu_choice/choice = new()
|
|
choice.name = command_name
|
|
choice.image = icon(icon = radial_icon, icon_state = radial_icon_state)
|
|
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.")
|
|
|
|
/// Target the pointed atom for actions
|
|
/datum/pet_command/proc/on_target_set(mob/living/friend, atom/potential_target)
|
|
var/mob/living/parent = weak_parent.resolve()
|
|
if (!parent)
|
|
return FALSE
|
|
|
|
parent.ai_controller.CancelActions()
|
|
if(!look_for_target(friend, potential_target) || !set_command_target(parent, potential_target))
|
|
return FALSE
|
|
parent.visible_message(span_warning("[parent] follows [friend]'s gesture towards [potential_target] [pointed_reaction]!"))
|
|
return TRUE
|