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
@@ -124,7 +124,7 @@
var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target)
step(living_pawn, get_dir(living_pawn, picked_turf))
/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target)
/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source, atom/target)
var/list/turf_list = get_line(source, target)
var/list_length = length(turf_list) - 1
for(var/i in 1 to list_length)
@@ -0,0 +1,19 @@
///behavior to befriend any targets
/datum/ai_behavior/befriend_target
/datum/ai_behavior/befriend_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, befriend_message)
var/mob/living/living_pawn = controller.pawn
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
living_pawn.befriend(living_target)
var/befriend_text = controller.blackboard[befriend_message]
if(befriend_text)
to_chat(living_target, "<span class='notice'>[living_pawn] [befriend_text]</span>")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/befriend_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
@@ -0,0 +1,53 @@
/**
* # Travel Towards
* Moves towards the atom in the passed blackboard key.
* Planning continues during this action so it can be interrupted by higher priority actions.
*/
/datum/ai_behavior/travel_towards
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// If true we will get rid of our target on completion
var/clear_target = FALSE
/// If desired, an alternate [/datum/ai_movement] to use for movement/pathfinding.
var/new_movement_type
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target, new_movement_type)
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
if(clear_target)
controller.clear_blackboard_key(target_key)
if(new_movement_type)
controller.change_ai_movement_type(initial(controller.ai_movement))
/datum/ai_behavior/travel_towards/stop_on_arrival
clear_target = TRUE
/datum/ai_behavior/travel_towards/adjacent
required_distance = 1
/**
* # Travel Towards Atom
* Travel towards an atom you pass directly from the controller rather than a blackboard key.
* You might need to do this to avoid repeating some checks in both a controller and an action.
*/
/datum/ai_behavior/travel_towards_atom
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/travel_towards_atom/setup(datum/ai_controller/controller, atom/target_atom)
. = ..()
if(isnull(target_atom))
return FALSE
set_movement_target(controller, target_atom)
/datum/ai_behavior/travel_towards_atom/perform(seconds_per_tick, datum/ai_controller/controller, atom/target_atom)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,31 @@
/datum/ai_behavior/find_mineral_wall
/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
var/mob/living_pawn = controller.pawn
for(var/turf/simulated/mineral/potential_wall in oview(9, living_pawn))
if(!check_if_mineable(controller, potential_wall)) // check if its surrounded by walls
continue
controller.set_blackboard_key(found_wall_key, potential_wall) // closest wall first!
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/// Check to see if we can get to and mine the turf the mineral is in
/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
var/mob/living/source = controller.pawn
var/direction_to_turf = get_dir(target_wall, source)
if(!IS_DIR_DIAGONAL(direction_to_turf))
return TRUE
var/list/directions_to_check = list()
for(var/direction_check in GLOB.cardinal)
if(direction_check & direction_to_turf)
directions_to_check += direction_check
for(var/direction in directions_to_check)
var/turf/test_turf = get_step(target_wall, direction)
if(isnull(test_turf))
continue
if(!test_turf.is_blocked_turf(source_atom = source))
return TRUE
return FALSE
@@ -11,3 +11,14 @@
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
if(end_planning)
return SUBTREE_RETURN_FINISH_PLANNING // we are going into battle...no distractions.
/datum/ai_planning_subtree/basic_ranged_attack_subtree
operational_datums = list(/datum/component/ranged_attacks)
var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack
/datum/ai_planning_subtree/basic_ranged_attack_subtree/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
@@ -0,0 +1,19 @@
/datum/ai_planning_subtree/simple_find_target
/// Variable to store target in
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/datum/ai_planning_subtree/simple_find_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
// Prevents finding a target if a human is nearby
/datum/ai_planning_subtree/simple_find_target/not_while_observed
/datum/ai_planning_subtree/simple_find_target/not_while_observed/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
for(var/mob/living/carbon/human/watcher in hearers(11, controller.pawn))
if(watcher.stat != DEAD)
return
return ..()
/datum/ai_planning_subtree/simple_find_target/to_flee
target_key = BB_BASIC_MOB_FLEE_TARGET
@@ -0,0 +1,33 @@
/**
* Simple behaviours which simply try to use an ability whenever it is available.
* For something which wants a target try `targeted_mob_ability`.
*/
/datum/ai_planning_subtree/use_mob_ability
/// Blackboard key for the ability
var/ability_key = BB_GENERIC_ACTION
/// Behaviour to perform using ability
var/use_ability_behavior = /datum/ai_behavior/use_mob_ability
/// If true we terminate planning after trying to use the ability.
var/finish_planning = FALSE
/datum/ai_planning_subtree/use_mob_ability/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!ability_key)
CRASH("You forgot to tell this mob where to find its ability")
var/datum/action/using_action = controller.blackboard[ability_key]
if(!using_action?.IsAvailable())
return
controller.queue_behavior(use_ability_behavior, ability_key)
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/use_mob_ability
/datum/ai_behavior/use_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key)
var/datum/action/using_action = controller.blackboard[ability_key]
if(QDELETED(using_action))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(using_action.Trigger())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
@@ -0,0 +1,52 @@
/**
* Tells the AI to find a certain target nearby to hunt.
* If a target has been found, we will start to move towards it, and eventually attack it.
*/
/datum/ai_planning_subtree/find_and_hunt_target
/// What key in the blacbkboard do we store our hunting target?
/// If you want to have multiple hunting behaviors on a controller be sure that this is unique
var/target_key = BB_CURRENT_HUNTING_TARGET
/// What behavior to execute if we have no target
var/finding_behavior = /datum/ai_behavior/find_hunt_target
/// What behavior to execute if we do have a target
var/hunting_behavior = /datum/ai_behavior/hunt_target
/// What targets we're hunting for
var/list/hunt_targets
/// In what radius will we hunt
var/hunt_range = 2
/// What are the chances we hunt something at any given moment
var/hunt_chance = 100
///do we finish planning subtree
var/finish_planning = TRUE
/datum/ai_planning_subtree/find_and_hunt_target/New()
. = ..()
hunt_targets = typecacheof(hunt_targets)
/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(hunt_chance, seconds_per_tick))
return
if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time)
return
if(!controller.blackboard_key_exists(target_key))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
return
// We ARE hunting something, execute the hunt.
// Note that if our AI controller has multiple hunting subtrees set,
// we may accidentally be executing another tree's hunt - not ideal,
// try to set a unique target key if you have multiple
controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type))
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else
/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores
target_key = BB_ORE_TARGET
hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/hunt_ores
finding_behavior = /datum/ai_behavior/find_hunt_target/hunt_ores
hunt_targets = list(/obj/item/stack/ore)
hunt_chance = 90
hunt_range = 9
@@ -0,0 +1,39 @@
/// Finds a specific atom type to hunt.
/datum/ai_behavior/find_hunt_target
/// is this only meant to search for turf types?
var/search_turf_types = FALSE
/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
var/mob/living/living_mob = controller.pawn
var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob)
for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt))
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(isliving(dinner))
var/mob/living/living_target = dinner
if(living_target.stat == DEAD)
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/find_hunt_target/hunt_ores
/datum/ai_behavior/find_hunt_target/hunt_ores/valid_dinner(mob/living/basic/source, obj/item/stack/ore/target, radius, datum/ai_controller/controller)
var/list/forbidden_ore = controller.blackboard[BB_ORE_IGNORE_TYPES]
if(is_type_in_list(target, forbidden_ore))
return FALSE
if(!isturf(target.loc))
return FALSE
var/obj/item/pet_target = controller.blackboard[BB_CURRENT_PET_TARGET]
if(target == pet_target) // we are currently fetching this ore for master, dont eat it!
return FALSE
return can_see(source, target, radius)
@@ -0,0 +1,63 @@
/// Hunts down a specific atom type.
/datum/ai_behavior/hunt_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/// How long do we have to wait after a successful hunt?
var/hunt_cooldown = 5 SECONDS
/// Do we reset the target after attacking something, so we can check for status changes.
var/always_reset_target = FALSE
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/atom/hunt_target = controller.blackboard[hunting_target_key]
if(isnull(hunt_target))
return FALSE
set_movement_target(controller, hunt_target)
/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
if(QDELETED(hunted))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
target_caught(hunter, hunted)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted)
if(isliving(hunted)) // Are we hunting a living mob?
var/mob/living/living_target = hunted
hunter.manual_emote("chomps [living_target]!")
living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS)
add_attack_logs(hunter, hunted, "AI controller killed", ATKLOG_ALL)
living_target.death()
else if(isfood(hunted))
hunted.attack_animal(hunter)
else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs.
hunter.manual_emote("chomps [hunted]!")
qdel(hunted)
/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
. = ..()
if(succeeded && hunting_cooldown_key)
controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown)
else if(hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
if(always_reset_target && hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
/datum/ai_behavior/hunt_target/interact_with_target
/// Which intent should we use to interact with
var/behavior_intent = INTENT_HARM
/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, atom/hunted)
var/datum/ai_controller/controller = hunter.ai_controller
controller.ai_interact(target = hunted, intent = behavior_intent)
/datum/ai_behavior/hunt_target/interact_with_target/hunt_ores
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot
always_reset_target = TRUE
hunt_cooldown = 2 SECONDS
behavior_intent = INTENT_HELP
@@ -1,125 +0,0 @@
/**
* Tells the AI to find a certain target nearby to hunt.
* If a target has been found, we will start to move towards it, and eventually attack it.
*/
/datum/ai_planning_subtree/find_and_hunt_target
/// What key in the blacbkboard do we store our hunting target?
/// If you want to have multiple hunting behaviors on a controller be sure that this is unique
var/target_key = BB_CURRENT_HUNTING_TARGET
/// What behavior to execute if we have no target
var/finding_behavior = /datum/ai_behavior/find_hunt_target
/// What behavior to execute if we do have a target
var/hunting_behavior = /datum/ai_behavior/hunt_target
/// What targets we're hunting for
var/list/hunt_targets
/// In what radius will we hunt
var/hunt_range = 2
/// What are the chances we hunt something at any given moment
var/hunt_chance = 100
///do we finish planning subtree
var/finish_planning = TRUE
/datum/ai_planning_subtree/find_and_hunt_target/New()
. = ..()
hunt_targets = typecacheof(hunt_targets)
/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(hunt_chance, seconds_per_tick))
return
if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time)
return
if(!controller.blackboard_key_exists(target_key))
controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range)
return
// We ARE hunting something, execute the hunt.
// Note that if our AI controller has multiple hunting subtrees set,
// we may accidentally be executing another tree's hunt - not ideal,
// try to set a unique target key if you have multiple
controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type))
if(finish_planning)
return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else
/// Finds a specific atom type to hunt.
/datum/ai_behavior/find_hunt_target
///is this only meant to search for turf types?
var/search_turf_types = FALSE
/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
var/mob/living/living_mob = controller.pawn
var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob)
for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt))
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(isliving(dinner))
var/mob/living/living_target = dinner
if(living_target.stat == DEAD)
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/find_hunt_target/search_turf_types
search_turf_types = TRUE
/// Hunts down a specific atom type.
/datum/ai_behavior/hunt_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/// How long do we have to wait after a successful hunt?
var/hunt_cooldown = 5 SECONDS
/// Do we reset the target after attacking something, so we can check for status changes.
var/always_reset_target = FALSE
/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
. = ..()
var/atom/hunt_target = controller.blackboard[hunting_target_key]
if(isnull(hunt_target))
return FALSE
set_movement_target(controller, hunt_target)
/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
var/mob/living/hunter = controller.pawn
var/atom/hunted = controller.blackboard[hunting_target_key]
if(QDELETED(hunted))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
target_caught(hunter, hunted)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted)
if(isliving(hunted)) // Are we hunting a living mob?
var/mob/living/living_target = hunted
hunter.manual_emote("chomps [living_target]!")
living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS)
living_target.death()
else if(isfood(hunted))
hunted.attack_animal(hunter)
else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs.
hunter.manual_emote("chomps [hunted]!")
qdel(hunted)
/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
. = ..()
if(succeeded && hunting_cooldown_key)
controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown)
else if(hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
if(always_reset_target && hunting_target_key)
controller.clear_blackboard_key(hunting_target_key)
/datum/ai_behavior/hunt_target/interact_with_target
/// Which intent should we use to interact with
var/behavior_intent = INTENT_HARM
/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, obj/structure/cable/hunted)
var/datum/ai_controller/controller = hunter.ai_controller
controller.ai_interact(target = hunted, intent = behavior_intent)
@@ -0,0 +1,44 @@
/**
* This movement datum represents smart-pathing
*/
/datum/ai_movement/jps
max_pathing_attempts = 20
var/maximum_length = AI_MAX_PATH_LENGTH
/// how we deal with diagonal movement, whether we try to avoid them or follow through with them
var/diagonal_flags = DIAGONAL_REMOVE_CLUNKY
/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
. = ..()
var/atom/movable/moving = controller.pawn
var/delay = controller.movement_delay
var/datum/move_loop/has_target/jps/loop = GLOB.move_manager.jps_move(moving,
current_movement_target,
delay,
repath_delay = 0.5 SECONDS,
// TODO: "simulated_only" refers to only considering atmos-simulated tiles in pathfinding.
// At some point we need to make it easy to tell, here, if a given atom controlled
// by an AI should consider itself "spaceproof" enough to traverse turfs
// without air, but should be definintely keyed to every mob's own survival
// requirements.
simulated_only = FALSE,
max_path_length = maximum_length,
minimum_distance = controller.get_minimum_distance(),
access = controller.get_access(),
subsystem = SSai_movement,
diagonal_handling = diagonal_flags,
extra_info = controller,
)
RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move))
RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move))
RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming))
return loop
/datum/ai_movement/jps/proc/repath_incoming(datum/move_loop/has_target/jps/source)
SIGNAL_HANDLER // COMSIG_MOVELOOP_JPS_REPATH
var/datum/ai_controller/controller = source.extra_info
source.access = controller.get_access()
source.minimum_distance = controller.get_minimum_distance()
@@ -80,3 +80,6 @@
if(controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || controller.blackboard[BB_TEMPORARILY_IGNORE_FACTION])
return FALSE
return living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly)
/datum/targeting_strategy/basic/always_check_factions
check_factions_exactly = TRUE
@@ -0,0 +1,46 @@
/// Don't target an atom in our friends list (or turfs), anything else is fair game
/datum/targeting_strategy/basic/not_friends
/// If we can try to wall/mineral turfs or not
var/attack_closed_turf = FALSE
/// If we want it to attack ALL OBJECTS
var/attack_obj = FALSE
/// Returns true or false depending on if the target can be attacked by the mob
/datum/targeting_strategy/basic/not_friends/can_attack(mob/living/living_mob, atom/target, vision_range)
if(attack_closed_turf && (iswallturf(target) || ismineralturf(target)))
return TRUE
if(attack_obj && isobj(target))
var/obj/target_obj = target
if(!(target_obj.resistance_flags & INDESTRUCTIBLE))
return TRUE
if(living_mob?.ai_controller?.blackboard[BB_FRIENDS_LIST] && (target in living_mob.ai_controller.blackboard[BB_FRIENDS_LIST]))
return FALSE
return ..()
/// friends dont care about factions
/datum/targeting_strategy/basic/not_friends/faction_check(mob/living/living_mob, mob/living/the_target)
return FALSE
/datum/targeting_strategy/basic/not_friends/attack_closed_turfs
attack_closed_turf = TRUE
// Allows mobs to target objs. In case you want your mob to target these without going after walls
/datum/targeting_strategy/basic/not_friends/attack_objs
attack_obj = TRUE
// Let us target everything that needs targeting, just like they used to.
/datum/targeting_strategy/basic/not_friends/attack_everything
attack_closed_turf = TRUE
attack_obj = TRUE
/// Subtype that allows us to target items while deftly avoiding attacking our allies. Be careful when it comes to targeting items as an AI could get trapped targeting something it can't destroy.
/datum/targeting_strategy/basic/not_friends/allow_items
/datum/targeting_strategy/basic/not_friends/allow_items/can_attack(mob/living/living_mob, atom/the_target, vision_range)
. = ..()
if(isitem(the_target))
// trust fall exercise
return TRUE
@@ -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)
+1
View File
@@ -158,6 +158,7 @@
if(GIBTONITE_UNSTRUCK)
playsound(src,'sound/effects/hit_on_shattered_glass.ogg', 50, TRUE)
explosive_reaction(source, user, triggered_by_explosion)
SEND_SIGNAL(source, COMSIG_MINE_EXPOSE_GIBTONITE, user)
return MINERAL_PREVENT_DIG
if(GIBTONITE_ACTIVE)
detonate(source)