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:
warriorstar-orion
2025-06-13 19:48:40 +00:00
committed by GitHub
co-authored by Luc PollardTheDragon
parent 586e2e6c4d
commit e63415a483
50 changed files with 1892 additions and 529 deletions
@@ -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
+90
View File
@@ -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)