mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-08-24 20:47:10 +01:00
port minebots to basic mobs and add some behavior (#29319)
* port minebots to basic mobs and add some behavior * remove unused define * update is_blocked_turf usage * fix radio, goldgrub, and armor upgrade * standardize blackboard names * Apply suggestions from code review Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> Signed-off-by: warriorstar-orion <orion@snowfrost.garden> * lewc review * whoops * Apply suggestions from code review Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: warriorstar-orion <orion@snowfrost.garden> --------- Signed-off-by: warriorstar-orion <orion@snowfrost.garden> Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com> Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
This commit is contained in:
co-authored by
Luc
PollardTheDragon
parent
586e2e6c4d
commit
e63415a483
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* # 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)
|
||||
QDEL_LIST_ASSOC_VAL(available_commands)
|
||||
return ..()
|
||||
|
||||
/datum/component/obeys_commands/RegisterWithParent()
|
||||
RegisterSignal(parent, COMSIG_LIVING_BEFRIENDED, PROC_REF(add_friend))
|
||||
RegisterSignal(parent, COMSIG_LIVING_UNFRIENDED, PROC_REF(remove_friend))
|
||||
RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
|
||||
|
||||
/datum/component/obeys_commands/UnregisterFromParent()
|
||||
UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_LIVING_UNFRIENDED, COMSIG_PARENT_EXAMINE))
|
||||
|
||||
/// Add someone to our friends list
|
||||
/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)
|
||||
|
||||
/// Remove someone from our friends list
|
||||
/datum/component/obeys_commands/proc/remove_friend(datum/source, mob/living/old_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, remove_friend), old_friend)
|
||||
|
||||
/// Add a note about whether they will follow the instructions of the inspecting mob
|
||||
/datum/component/obeys_commands/proc/on_examine(mob/living/source, mob/user, list/examine_list)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
if(source.stat == DEAD || source.stat == UNCONSCIOUS)
|
||||
return
|
||||
if(!(user in source.ai_controller?.blackboard[BB_FRIENDS_LIST]))
|
||||
return
|
||||
examine_list += "<span class='notice'>[source.p_they(TRUE)] seem[source.p_s()] happy to see you!</span>"
|
||||
@@ -0,0 +1,179 @@
|
||||
// TODO: port /datum/callout_option and all the associated shenanigans
|
||||
|
||||
/**
|
||||
* # 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
|
||||
/// UID of who follows this command
|
||||
var/parent_uid
|
||||
/// 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
|
||||
/// Speech strings to listen out for
|
||||
var/list/speech_commands = list()
|
||||
/// 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)
|
||||
. = ..()
|
||||
parent_uid = parent.UID()
|
||||
|
||||
/// 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))
|
||||
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_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 // COMSIG_MOB_AUTOMUTE_CHECK
|
||||
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 // COMSIG_MOB_SAY
|
||||
var/mob/living/parent = locateUID(parent_uid)
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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 as anything 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 = locateUID(parent_uid)
|
||||
if(isnull(parent) || isnull(parent.ai_controller))
|
||||
return FALSE
|
||||
if(parent.stat == DEAD || parent.stat == UNCONSCIOUS) // 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 = locateUID(parent_uid)
|
||||
set_command_active(parent, commander, radial_command)
|
||||
return TRUE
|
||||
|
||||
/datum/pet_command/proc/generate_emote_command(atom/target)
|
||||
var/mob/living/living_pet = locateUID(parent_uid)
|
||||
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 = locateUID(parent_uid)
|
||||
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.cancel_actions()
|
||||
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.cancel_actions() // Stop whatever you're doing and do this instead
|
||||
parent.ai_controller.set_blackboard_key(BB_ACTIVE_PET_COMMAND, src)
|
||||
if(command_feedback)
|
||||
parent.emote("me", EMOTE_VISIBLE, "[command_feedback]")
|
||||
if(!radial_command)
|
||||
return
|
||||
if(!requires_pointing)
|
||||
var/manual_emote_text = generate_emote_command()
|
||||
commander.emote("me", EMOTE_VISIBLE, manual_emote_text)
|
||||
return
|
||||
RegisterSignal(commander, COMSIG_MOB_CLICKON, PROC_REF(click_on_target))
|
||||
|
||||
/datum/pet_command/proc/click_on_target(mob/living/source, atom/target, list/modifiers)
|
||||
SIGNAL_HANDLER // COMSIG_MOB_CLICKON
|
||||
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(/mob, custom_emote), EMOTE_VISIBLE, manual_emote_text)
|
||||
UnregisterSignal(source, COMSIG_MOB_CLICKON)
|
||||
return COMSIG_MOB_CANCEL_CLICKON
|
||||
|
||||
/datum/pet_command/proc/point_on_target(mob/living/friend, atom/potential_target)
|
||||
SIGNAL_HANDLER // COMSIG_MOVABLE_POINTED
|
||||
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
|
||||
|
||||
// TODO: Port /datum/pet_command/proc/provide_radial_data
|
||||
|
||||
/**
|
||||
* 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 = locateUID(parent_uid)
|
||||
if(!parent)
|
||||
return FALSE
|
||||
|
||||
parent.ai_controller.cancel_actions()
|
||||
if(!look_for_target(friend, potential_target) || !set_command_target(parent, potential_target))
|
||||
return FALSE
|
||||
parent.visible_message("<span class='warning'>[parent] follows [friend]'s gesture towards [potential_target] [pointed_reaction]!</span>")
|
||||
return TRUE
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* # 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/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/datum/pet_command/command = controller.blackboard[BB_ACTIVE_PET_COMMAND]
|
||||
if(!command)
|
||||
return // Do something else
|
||||
return command.execute_action(controller)
|
||||
@@ -0,0 +1,210 @@
|
||||
// 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."
|
||||
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
|
||||
|
||||
/datum/pet_command/idle/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return "signals [living_pet] to stay idle!"
|
||||
|
||||
/**
|
||||
* # 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."
|
||||
speech_commands = list("free", "loose")
|
||||
command_feedback = "relaxes."
|
||||
|
||||
/datum/pet_command/free/execute_action(datum/ai_controller/controller)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
return // Just move on to the next planning subtree.
|
||||
|
||||
/datum/pet_command/free/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return "signals [living_pet] to go free!"
|
||||
|
||||
/**
|
||||
* # 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."
|
||||
speech_commands = list("heel", "follow")
|
||||
///the behavior we use to follow
|
||||
var/follow_behavior = /datum/ai_behavior/pet_follow_friend
|
||||
|
||||
/datum/pet_command/follow/set_command_active(mob/living/parent, mob/living/commander)
|
||||
. = ..()
|
||||
set_command_target(parent, commander)
|
||||
|
||||
/datum/pet_command/follow/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return "signals [living_pet] to follow!"
|
||||
|
||||
/datum/pet_command/follow/execute_action(datum/ai_controller/controller)
|
||||
controller.queue_behavior(follow_behavior, BB_CURRENT_PET_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/**
|
||||
* # Pet Command: Use ability
|
||||
* Use an an ability that does not require any targets
|
||||
*/
|
||||
/datum/pet_command/untargeted_ability
|
||||
///untargeted ability we will use
|
||||
var/ability_key
|
||||
|
||||
/datum/pet_command/untargeted_ability/execute_action(datum/ai_controller/controller)
|
||||
var/datum/action/ability = controller.blackboard[ability_key]
|
||||
if(!ability?.IsAvailable())
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/pet_command/untargeted_ability/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return "signals [living_pet] to use an ability!"
|
||||
|
||||
/**
|
||||
* # Pet Command: Attack
|
||||
* Tells a pet to chase and bite the next thing you point at
|
||||
*/
|
||||
/datum/pet_command/attack
|
||||
command_name = "Attack"
|
||||
command_desc = "Command your pet to attack things that you point out to it."
|
||||
requires_pointing = TRUE
|
||||
speech_commands = list("attack", "sic", "kill")
|
||||
command_feedback = "growls."
|
||||
pointed_reaction = "and growls"
|
||||
/// Balloon alert to display if providing an invalid target
|
||||
var/refuse_reaction = "shakes head"
|
||||
/// Attack behaviour to use
|
||||
var/attack_behaviour = /datum/ai_behavior/basic_melee_attack
|
||||
|
||||
// Refuse to target things we can't target, chiefly other friends
|
||||
/datum/pet_command/attack/set_command_target(mob/living/parent, atom/target)
|
||||
if(!target)
|
||||
return FALSE
|
||||
var/mob/living/living_parent = parent
|
||||
if(!living_parent.ai_controller)
|
||||
return FALSE
|
||||
var/datum/targeting_strategy/targeter = GET_TARGETING_STRATEGY(living_parent.ai_controller.blackboard[targeting_strategy_key])
|
||||
if(!targeter)
|
||||
return FALSE
|
||||
if(!targeter.can_attack(living_parent, target))
|
||||
refuse_target(parent, target)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/pet_command/attack/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return isnull(target) ? null : "signals [living_pet] to attack [target]!"
|
||||
|
||||
/// Display feedback about not targeting something
|
||||
/datum/pet_command/attack/proc/refuse_target(mob/living/parent, atom/target)
|
||||
var/mob/living/living_parent = parent
|
||||
living_parent.custom_emote(EMOTE_VISIBLE, refuse_reaction)
|
||||
living_parent.visible_message("<span class='notice'>[living_parent] refuses to attack [target].</span>")
|
||||
|
||||
/datum/pet_command/attack/execute_action(datum/ai_controller/controller)
|
||||
controller.queue_behavior(attack_behaviour, BB_CURRENT_PET_TARGET, targeting_strategy_key)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/pet_command/protect_owner
|
||||
command_name = "Protect owner"
|
||||
command_desc = "Your pet will run to your aid."
|
||||
hidden = TRUE
|
||||
/// The range our owner needs to be in for us to protect him
|
||||
var/protect_range = 9
|
||||
/// The behavior we will use when he is attacked
|
||||
var/protect_behavior = /datum/ai_behavior/basic_melee_attack
|
||||
/// Message cooldown to prevent too many people from telling you not to commit suicide
|
||||
COOLDOWN_DECLARE(self_harm_message_cooldown)
|
||||
/// Message cooldown to prevent spamming apologies
|
||||
COOLDOWN_DECLARE(friendly_fire_message_cooldown)
|
||||
|
||||
/datum/pet_command/protect_owner/add_new_friend(mob/living/tamer)
|
||||
RegisterSignal(tamer, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(set_attacking_target))
|
||||
if(!HAS_TRAIT(tamer, TRAIT_RELAYING_ATTACKER))
|
||||
tamer.AddElement(/datum/element/relay_attackers)
|
||||
|
||||
/datum/pet_command/protect_owner/remove_friend(mob/living/unfriended)
|
||||
UnregisterSignal(unfriended, COMSIG_ATOM_WAS_ATTACKED)
|
||||
|
||||
/datum/pet_command/protect_owner/execute_action(datum/ai_controller/controller)
|
||||
var/mob/living/victim = controller.blackboard[BB_CURRENT_PET_TARGET]
|
||||
if(QDELETED(victim))
|
||||
controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
|
||||
return
|
||||
// cancel the action if they're below our given crit stat, OR if we're trying to attack ourselves (this can happen on tamed mobs w/ protect subtree rarely)
|
||||
if(victim.stat > controller.blackboard[BB_TARGET_MINIMUM_STAT] || victim == controller.pawn)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
return
|
||||
controller.queue_behavior(protect_behavior, BB_CURRENT_PET_TARGET, BB_PET_TARGETING_STRATEGY)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/pet_command/protect_owner/set_command_active(mob/living/parent, mob/living/victim)
|
||||
. = ..()
|
||||
set_command_target(parent, victim)
|
||||
|
||||
/datum/pet_command/protect_owner/proc/set_attacking_target(atom/source, mob/living/attacker)
|
||||
SIGNAL_HANDLER // COMSIG_ATOM_WAS_ATTACKED
|
||||
|
||||
var/mob/living/basic/owner = locateUID(parent_uid)
|
||||
if(isnull(owner))
|
||||
return
|
||||
|
||||
// TODO: Be smarter about handling signals when our AI controller isn't active
|
||||
// This should definitely be handled somewhere higher up
|
||||
if(owner.ai_controller.ai_status != AI_STATUS_ON)
|
||||
return
|
||||
|
||||
if(source == attacker)
|
||||
var/list/interventions = owner.ai_controller?.blackboard[BB_OWNER_SELF_HARM_RESPONSES] || list()
|
||||
if(length(interventions) && COOLDOWN_FINISHED(src, self_harm_message_cooldown) && prob(30))
|
||||
COOLDOWN_START(src, self_harm_message_cooldown, 5 SECONDS)
|
||||
var/chosen_statement = pick(interventions)
|
||||
INVOKE_ASYNC(owner, TYPE_PROC_REF(/atom, atom_say), chosen_statement)
|
||||
return
|
||||
if(owner == attacker)
|
||||
var/list/apologies = owner.ai_controller?.blackboard[BB_OWNER_FRIENDLY_FIRE_APOLOGIES] || list()
|
||||
if(length(apologies) && COOLDOWN_FINISHED(src, friendly_fire_message_cooldown))
|
||||
COOLDOWN_START(src, friendly_fire_message_cooldown, 5 SECONDS)
|
||||
var/chosen_statement = pick(apologies)
|
||||
INVOKE_ASYNC(owner, TYPE_PROC_REF(/atom, atom_say), chosen_statement)
|
||||
return
|
||||
|
||||
var/mob/living/current_target = owner.ai_controller?.blackboard[BB_CURRENT_PET_TARGET]
|
||||
if(attacker == current_target) // we are already dealing with this target
|
||||
return
|
||||
if(isliving(attacker) && can_see(owner, attacker, protect_range))
|
||||
set_command_active(owner, attacker)
|
||||
|
||||
/datum/pet_command/move
|
||||
command_name = "Move"
|
||||
command_desc = "Command your pet to move to a location!"
|
||||
requires_pointing = TRUE
|
||||
speech_commands = list("move", "walk")
|
||||
/// the behavior we use to walk towards targets
|
||||
var/datum/ai_behavior/walk_behavior = /datum/ai_behavior/travel_towards
|
||||
|
||||
/datum/pet_command/move/set_command_target(mob/living/parent, atom/target)
|
||||
if(isnull(target) || !can_see(parent, target, 9))
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/pet_command/move/execute_action(datum/ai_controller/controller)
|
||||
if(controller.blackboard_key_exists(BB_CURRENT_PET_TARGET))
|
||||
controller.queue_behavior(walk_behavior, BB_CURRENT_PET_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/pet_command/move/retrieve_command_text(atom/living_pet, atom/target)
|
||||
return "signals [living_pet] to move!"
|
||||
@@ -0,0 +1,16 @@
|
||||
/// 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/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/pet_follow_friend/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
return AI_BEHAVIOR_DELAY
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Configurable ranged attack for basic mobs.
|
||||
*/
|
||||
/datum/component/ranged_attacks
|
||||
/// What kind of casing do we use to fire?
|
||||
var/casing_type
|
||||
/// What kind of projectile to we fire? Use only one of this or casing_type
|
||||
var/projectile_type
|
||||
/// Sound to play when we fire our projectile
|
||||
var/projectile_sound
|
||||
/// how many shots we will fire
|
||||
var/burst_shots
|
||||
/// intervals between shots
|
||||
var/burst_intervals
|
||||
/// Time to wait between shots
|
||||
var/cooldown_time
|
||||
/// Tracks time between shots
|
||||
COOLDOWN_DECLARE(fire_cooldown)
|
||||
|
||||
/datum/component/ranged_attacks/Initialize(
|
||||
casing_type,
|
||||
projectile_type,
|
||||
projectile_sound = 'sound/weapons/gunshots/gunshot_pistol.ogg',
|
||||
burst_shots,
|
||||
burst_intervals = 0.2 SECONDS,
|
||||
cooldown_time = 3 SECONDS,
|
||||
)
|
||||
. = ..()
|
||||
if(!isbasicmob(parent))
|
||||
return COMPONENT_INCOMPATIBLE
|
||||
|
||||
src.casing_type = casing_type
|
||||
src.projectile_sound = projectile_sound
|
||||
src.projectile_type = projectile_type
|
||||
src.cooldown_time = cooldown_time
|
||||
|
||||
if(casing_type && projectile_type)
|
||||
CRASH("Set both casing type and projectile type in [parent]'s ranged attacks component! uhoh! stinky!")
|
||||
if(!casing_type && !projectile_type)
|
||||
CRASH("Set neither casing type nor projectile type in [parent]'s ranged attacks component! What are they supposed to be attacking with, air?")
|
||||
if(burst_shots <= 1)
|
||||
return
|
||||
src.burst_shots = burst_shots
|
||||
src.burst_intervals = burst_intervals
|
||||
|
||||
/datum/component/ranged_attacks/RegisterWithParent()
|
||||
. = ..()
|
||||
RegisterSignal(parent, COMSIG_MOB_ATTACK_RANGED, PROC_REF(fire_ranged_attack))
|
||||
ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
|
||||
|
||||
/datum/component/ranged_attacks/UnregisterFromParent()
|
||||
. = ..()
|
||||
UnregisterSignal(parent, COMSIG_MOB_ATTACK_RANGED)
|
||||
REMOVE_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type)
|
||||
|
||||
/datum/component/ranged_attacks/proc/fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers)
|
||||
SIGNAL_HANDLER
|
||||
if(!COOLDOWN_FINISHED(src, fire_cooldown))
|
||||
return
|
||||
if(SEND_SIGNAL(firer, COMSIG_BASICMOB_PRE_ATTACK_RANGED, target, modifiers) & COMPONENT_CANCEL_RANGED_ATTACK)
|
||||
return
|
||||
COOLDOWN_START(src, fire_cooldown, cooldown_time)
|
||||
INVOKE_ASYNC(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers)
|
||||
if(isnull(burst_shots))
|
||||
return
|
||||
for(var/i in 1 to (burst_shots - 1))
|
||||
addtimer(CALLBACK(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers), i * burst_intervals)
|
||||
|
||||
/// Actually fire the damn thing
|
||||
/datum/component/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers)
|
||||
if(QDELETED(firer))
|
||||
return
|
||||
|
||||
firer.face_atom(target)
|
||||
if(projectile_type)
|
||||
firer.fire_projectile(projectile_type, target, projectile_sound)
|
||||
SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers)
|
||||
return
|
||||
playsound(firer, projectile_sound, 100, TRUE)
|
||||
var/turf/startloc = get_turf(firer)
|
||||
var/obj/item/ammo_casing/casing = new casing_type(startloc)
|
||||
var/target_zone
|
||||
if(ismob(target))
|
||||
var/mob/target_mob = target
|
||||
target_zone = target_mob.get_random_valid_zone()
|
||||
else
|
||||
target_zone = ran_zone()
|
||||
casing.fire(target, firer, null, null, null, target_zone, 0, firer)
|
||||
casing.update_appearance()
|
||||
SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers)
|
||||
Reference in New Issue
Block a user