mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-08-24 12:37:26 +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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user