mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 12:41:09 +01:00
[MIRROR] Monkey subtree breakup refactor! [MDB IGNORE] (#8748)
* Monkey subtree breakup refactor! (#61741) Splitting up monkey ai into subtrees allows me to make a punpun ai after this pr is merged, makes stopping planning matter for the AI subtrees, and more generic subtrees that can be used by most ais It also gets rid of bad practices like setting blackboards in the ai controller. * Monkey subtree breakup refactor! Co-authored-by: tralezab <40974010+tralezab@users.noreply.github.com>
This commit is contained in:
+16
-10
@@ -4,11 +4,6 @@
|
||||
#define AI_STATUS_ON 1
|
||||
#define AI_STATUS_OFF 2
|
||||
|
||||
|
||||
///Monkey checks
|
||||
#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE))
|
||||
#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat)
|
||||
|
||||
///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on
|
||||
#define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles
|
||||
|
||||
@@ -27,22 +22,34 @@
|
||||
///AI flags
|
||||
#define STOP_MOVING_WHEN_PULLED (1<<0)
|
||||
|
||||
///Subtree defines
|
||||
//Base Subtree defines
|
||||
|
||||
///This subtree should cancel any further planning, (Including from other subtrees)
|
||||
#define SUBTREE_RETURN_FINISH_PLANNING 1
|
||||
|
||||
//Generic subtree defines
|
||||
|
||||
/// probability that the pawn should try resisting out of restraints
|
||||
#define RESIST_SUBTREE_PROB 50
|
||||
///macro for whether it's appropriate to resist right now, used by resist subtree
|
||||
#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE))
|
||||
///macro for whether the pawn can act, used generally to prevent some horrifying ai disasters
|
||||
#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat)
|
||||
|
||||
//Generic BB keys
|
||||
#define BB_CURRENT_MIN_MOVE_DISTANCE "min_move_distance"
|
||||
///time until we should next eat, set by the generic hunger subtree
|
||||
#define BB_NEXT_HUNGRY "BB_NEXT_HUNGRY"
|
||||
///what we're going to eat next
|
||||
#define BB_FOOD_TARGET "bb_food_target"
|
||||
|
||||
//for songs
|
||||
|
||||
///song datum blackboard, set by instrument subtrees
|
||||
#define BB_SONG_DATUM "BB_SONG_DATUM"
|
||||
///song instrument blackboard, set by instrument subtrees
|
||||
#define BB_SONG_INSTRUMENT "BB_SONG_INSTRUMENT"
|
||||
///song lines blackboard, set by default on controllers
|
||||
#define BB_SONG_LINES "song_lines"
|
||||
|
||||
|
||||
// Monkey AI controller blackboard keys
|
||||
|
||||
#define BB_MONKEY_AGGRESSIVE "BB_monkey_aggressive"
|
||||
@@ -59,7 +66,6 @@
|
||||
#define BB_MONKEY_TARGET_DISPOSAL "BB_monkey_target_disposal"
|
||||
#define BB_MONKEY_DISPOSING "BB_monkey_disposing"
|
||||
#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown"
|
||||
#define BB_MONKEY_NEXT_HUNGRY "BB_monkey_next_hungry"
|
||||
|
||||
///Haunted item controller defines
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
#define MONKEY_SYRINGE_RETALIATION_PROB 20
|
||||
|
||||
// Probability per Life tick that the monkey will:
|
||||
/// probability that monkey resist out of restraints
|
||||
#define MONKEY_RESIST_PROB 50
|
||||
/// probability that monkey aggro against the mob pulling it
|
||||
#define MONKEY_PULL_AGGRO_PROB 5
|
||||
/// probability that monkey will get into mischief, i.e. finding/stealing items
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//this file has procs that help ais think good. used by behaviors mostly
|
||||
|
||||
|
||||
/// Returns either the best weapon from the given choices or null if held weapons are better
|
||||
/proc/GetBestWeapon(datum/ai_controller/controller, list/choices, list/held_weapons)
|
||||
var/gun_neurons_activated = controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED]
|
||||
var/top_force = 0
|
||||
var/obj/item/top_force_item
|
||||
for(var/obj/item/item as anything in held_weapons)
|
||||
if(!item)
|
||||
continue
|
||||
if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item])
|
||||
continue
|
||||
if(gun_neurons_activated && istype(item, /obj/item/gun))
|
||||
// We have a gun, why bother looking for something inferior
|
||||
// Also yes it is intentional that pawns dont know how to pick the best gun
|
||||
return item
|
||||
if(item.force > top_force)
|
||||
top_force = item.force
|
||||
top_force_item = item
|
||||
|
||||
for(var/obj/item/item as anything in choices)
|
||||
if(!item)
|
||||
continue
|
||||
if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item])
|
||||
continue
|
||||
if(gun_neurons_activated && istype(item, /obj/item/gun))
|
||||
return item
|
||||
if(item.force <= top_force)
|
||||
continue
|
||||
top_force_item = item
|
||||
top_force = item.force
|
||||
|
||||
return top_force_item
|
||||
|
||||
///returns if something can be consumed, drink or food
|
||||
/proc/IsEdible(obj/item/thing)
|
||||
if(!istype(thing))
|
||||
return FALSE
|
||||
if(IS_EDIBLE(thing))
|
||||
return TRUE
|
||||
if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass))
|
||||
var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing
|
||||
if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid!
|
||||
return TRUE
|
||||
return FALSE
|
||||
@@ -1,9 +1,4 @@
|
||||
|
||||
/datum/ai_behavior/find_and_set/cursed
|
||||
//optional, don't use if you're changing search_tactic()
|
||||
locate_path = /mob/living/carbon
|
||||
bb_key_to_set = BB_CURSE_TARGET
|
||||
|
||||
/datum/ai_behavior/item_move_close_and_attack/cursed
|
||||
attack_sound = 'sound/items/haunted/ghostitemattack.ogg'
|
||||
max_attempts = 4
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//make sure we have a target
|
||||
var/mob/living/carbon/curse_target = controller.blackboard[BB_CURSE_TARGET]
|
||||
if(!curse_target)
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/cursed)
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_CURSE_TARGET, /mob/living/carbon, CURSED_VIEW_RANGE)
|
||||
return
|
||||
//make sure attack is valid
|
||||
if(get_dist(curse_target, item_pawn) > CURSED_VIEW_RANGE)
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
. = ..()
|
||||
controller.current_movement_target = controller.blackboard[target_key]
|
||||
|
||||
|
||||
/datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/mob/living/pawn = controller.pawn
|
||||
@@ -150,25 +149,31 @@
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
|
||||
action_cooldown = 2 SECONDS
|
||||
|
||||
/datum/ai_behavior/consume/setup(datum/ai_controller/controller, obj/item/target)
|
||||
/datum/ai_behavior/consume/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
controller.current_movement_target = target
|
||||
controller.current_movement_target = controller.blackboard[target_key]
|
||||
|
||||
/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, obj/item/target)
|
||||
/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, target_key, hunger_timer_key)
|
||||
. = ..()
|
||||
var/mob/living/pawn = controller.pawn
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
|
||||
if(!(target in pawn.held_items))
|
||||
if(!pawn.put_in_hand_check(target))
|
||||
finish_action(controller, FALSE)
|
||||
if(!(target in living_pawn.held_items))
|
||||
if(!living_pawn.put_in_hand_check(target))
|
||||
finish_action(controller, FALSE, target, hunger_timer_key)
|
||||
return
|
||||
|
||||
pawn.put_in_hands(target)
|
||||
living_pawn.put_in_hands(target)
|
||||
|
||||
target.melee_attack_chain(pawn, pawn)
|
||||
target.melee_attack_chain(living_pawn, living_pawn)
|
||||
|
||||
if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done
|
||||
finish_action(controller, TRUE)
|
||||
finish_action(controller, TRUE, null, hunger_timer_key)
|
||||
|
||||
/datum/ai_behavior/consume/finish_action(datum/ai_controller/controller, succeeded, target_key, hunger_timer_key)
|
||||
. = ..()
|
||||
if(succeeded)
|
||||
controller.blackboard[hunger_timer_key] = world.time + rand(12 SECONDS, 60 SECONDS)
|
||||
|
||||
/**find and set
|
||||
* Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something.
|
||||
@@ -177,24 +182,73 @@
|
||||
*/
|
||||
/datum/ai_behavior/find_and_set
|
||||
action_cooldown = 5 SECONDS
|
||||
///search range in how many tiles around the pawn to look for the path
|
||||
var/search_range = 7
|
||||
//optional, don't use if you're changing search_tactic()
|
||||
var/locate_path
|
||||
var/bb_key_to_set
|
||||
|
||||
/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller)
|
||||
/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller, set_key, locate_path, search_range)
|
||||
. = ..()
|
||||
var/find_this_thing = search_tactic(controller)
|
||||
var/find_this_thing = search_tactic(controller, locate_path, search_range)
|
||||
if(find_this_thing)
|
||||
controller.blackboard[bb_key_to_set] = find_this_thing
|
||||
controller.blackboard[set_key] = find_this_thing
|
||||
finish_action(controller, TRUE)
|
||||
else
|
||||
finish_action(controller, FALSE)
|
||||
|
||||
/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller)
|
||||
/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range)
|
||||
return locate(locate_path) in oview(search_range, controller.pawn)
|
||||
|
||||
/**
|
||||
* Variant of find and set that fails if the living pawn doesn't hold something
|
||||
*/
|
||||
/datum/ai_behavior/find_and_set/pawn_must_hold_item
|
||||
|
||||
/datum/ai_behavior/find_and_set/pawn_must_hold_item/search_tactic(datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if(!living_pawn.get_num_held_items())
|
||||
return //we want to fail the search if we don't have something held
|
||||
. = ..()
|
||||
|
||||
/**
|
||||
* Variant of find and set that also requires the item to be edible. checks hands too
|
||||
*/
|
||||
/datum/ai_behavior/find_and_set/edible
|
||||
|
||||
/datum/ai_behavior/find_and_set/edible/search_tactic(datum/ai_controller/controller, locate_path, search_range)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/list/food_candidates = list()
|
||||
for(var/held_candidate as anything in living_pawn.held_items)
|
||||
if(!held_candidate || !IsEdible(held_candidate))
|
||||
continue
|
||||
food_candidates += held_candidate
|
||||
|
||||
var/list/local_results = locate(locate_path) in oview(search_range, controller.pawn)
|
||||
for(var/local_candidate in local_results)
|
||||
if(!IsEdible(local_candidate))
|
||||
continue
|
||||
food_candidates += local_candidate
|
||||
if(food_candidates.len)
|
||||
return pick(food_candidates)
|
||||
|
||||
/**
|
||||
* Variant of find and set that only checks in hands, search range should be excluded for this
|
||||
*/
|
||||
/datum/ai_behavior/find_and_set/in_hands
|
||||
|
||||
/datum/ai_behavior/find_and_set/in_hands/search_tactic(datum/ai_controller/controller, locate_path)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
return locate(locate_path) in living_pawn.held_items
|
||||
|
||||
/**
|
||||
* Drops items in hands, very important for future behaviors that require the pawn to grab stuff
|
||||
*/
|
||||
/datum/ai_behavior/drop_item
|
||||
|
||||
/datum/ai_behavior/drop_item/perform(delta_time, datum/ai_controller/controller)
|
||||
. = ..()
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/obj/item/best_held = GetBestWeapon(controller, null, living_pawn.held_items)
|
||||
for(var/obj/item/held as anything in living_pawn.held_items)
|
||||
if(!held || held == best_held)
|
||||
continue
|
||||
living_pawn.dropItemToGround(held)
|
||||
|
||||
/// This behavior involves attacking a target.
|
||||
/datum/ai_behavior/attack
|
||||
@@ -284,10 +338,11 @@
|
||||
|
||||
/datum/ai_behavior/setup_instrument
|
||||
|
||||
/datum/ai_behavior/setup_instrument/perform(delta_time, datum/ai_controller/controller, song_datum_key, song_lines_key)
|
||||
/datum/ai_behavior/setup_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key, song_lines_key)
|
||||
. = ..()
|
||||
|
||||
var/datum/song/song = controller.blackboard[song_datum_key]
|
||||
var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
|
||||
var/datum/song/song = song_instrument.song
|
||||
var/song_lines = controller.blackboard[song_lines_key]
|
||||
|
||||
//just in case- it won't do anything if the instrument isn't playing
|
||||
@@ -299,10 +354,28 @@
|
||||
|
||||
/datum/ai_behavior/play_instrument
|
||||
|
||||
/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_datum_key)
|
||||
/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key)
|
||||
. = ..()
|
||||
|
||||
var/datum/song/song = controller.blackboard[song_datum_key]
|
||||
var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
|
||||
var/datum/song/song = song_instrument.song
|
||||
|
||||
song.start_playing(controller.pawn)
|
||||
finish_action(controller, TRUE)
|
||||
|
||||
/datum/ai_behavior/find_nearby
|
||||
|
||||
/datum/ai_behavior/find_nearby/perform(delta_time, datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
|
||||
var/list/possible_targets = list()
|
||||
for(var/atom/thing in view(2, controller.pawn))
|
||||
if(!thing.mouse_opacity)
|
||||
continue
|
||||
if(thing.IsObscured())
|
||||
continue
|
||||
possible_targets += thing
|
||||
if(!possible_targets.len)
|
||||
finish_action(controller, FALSE)
|
||||
controller.blackboard[target_key] = pick(possible_targets)
|
||||
finish_action(controller, TRUE)
|
||||
|
||||
@@ -1,16 +1,62 @@
|
||||
/datum/ai_planning_subtree/play_instrument/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/obj/item/instrument/song_player = locate(/obj/item/instrument) in living_pawn.held_items
|
||||
if(!song_player)
|
||||
controller.blackboard[BB_SONG_DATUM] = null
|
||||
/**
|
||||
* Generic Instrument Subtree, For your pawn playing instruments
|
||||
*
|
||||
* Requires at least a living mob that can hold items.
|
||||
*
|
||||
* relevant blackboards:
|
||||
* * BB_SONG_INSTRUMENT - set by this subtree, is the song datum the pawn plays music from.
|
||||
* * BB_SONG_LINES - not set by this subtree, is the song loaded into the song datum.
|
||||
*/
|
||||
/datum/ai_planning_subtree/generic_play_instrument/SelectBehaviors(datum/ai_controller/controller, delta_time)
|
||||
if(!controller.blackboard[BB_SONG_INSTRUMENT])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/in_hands, BB_SONG_INSTRUMENT, /obj/item/instrument)
|
||||
return //we can't play a song since we do not have an instrument
|
||||
|
||||
controller.blackboard[BB_SONG_DATUM] = song_player.song
|
||||
var/obj/item/instrument/song_player = controller.blackboard[BB_SONG_INSTRUMENT]
|
||||
|
||||
var/list/donkey_kong_lines = splittext(MONKEY_SONG, "\n")
|
||||
popleft(donkey_kong_lines) //remove BPM as it is parsed out
|
||||
if(!compare_list(song_player.song.lines, donkey_kong_lines) || !song_player.song.repeat)
|
||||
controller.queue_behavior(/datum/ai_behavior/setup_instrument, BB_SONG_DATUM, BB_SONG_LINES)
|
||||
var/list/parsed_song_lines = splittext(controller.blackboard[BB_SONG_LINES], "\n")
|
||||
popleft(parsed_song_lines) //remove BPM as it is parsed out
|
||||
if(!compare_list(song_player.song.lines, parsed_song_lines) || !song_player.song.repeat)
|
||||
controller.queue_behavior(/datum/ai_behavior/setup_instrument, BB_SONG_INSTRUMENT, BB_SONG_LINES)
|
||||
|
||||
if(!song_player.song.playing) //we may stop playing if we weren't playing before, were setting up dk theme, or ran out of repeats (also causing setup behavior)
|
||||
controller.queue_behavior(/datum/ai_behavior/play_instrument, BB_SONG_DATUM)
|
||||
controller.queue_behavior(/datum/ai_behavior/play_instrument, BB_SONG_INSTRUMENT)
|
||||
|
||||
/**
|
||||
* Generic Resist Subtree, resist if it makes sense to!
|
||||
*
|
||||
* Requires nothing beyond a living pawn, makes sense on a good amount of mobs since anything can get buckled.
|
||||
*
|
||||
* relevant blackboards:
|
||||
* * None!
|
||||
*/
|
||||
/datum/ai_planning_subtree/generic_resist/SelectBehaviors(datum/ai_controller/controller, delta_time)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
|
||||
if(SHOULD_RESIST(living_pawn) && DT_PROB(RESIST_SUBTREE_PROB, delta_time))
|
||||
controller.queue_behavior(/datum/ai_behavior/resist) //BRO IM ON FUCKING FIRE BRO
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY.
|
||||
|
||||
/**
|
||||
* Generic Hunger Subtree,
|
||||
*
|
||||
* Requires at least a living mob that can hold items.
|
||||
*
|
||||
* relevant blackboards:
|
||||
* * BB_NEXT_HUNGRY - set by this subtree, is when the controller is next hungry
|
||||
*/
|
||||
/datum/ai_planning_subtree/generic_hunger/SelectBehaviors(datum/ai_controller/controller, delta_time)
|
||||
//inits the blackboard timer
|
||||
if(!controller.blackboard[BB_NEXT_HUNGRY])
|
||||
controller.blackboard[BB_NEXT_HUNGRY] = world.time + rand(0, 30 SECONDS)
|
||||
|
||||
if(world.time < controller.blackboard[BB_NEXT_HUNGRY])
|
||||
return
|
||||
|
||||
if(!controller.blackboard[BB_FOOD_TARGET])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/edible, BB_FOOD_TARGET, /obj/item, 2)
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/drop_item)
|
||||
controller.queue_behavior(/datum/ai_behavior/consume, BB_FOOD_TARGET, BB_NEXT_HUNGRY)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
@@ -139,7 +139,11 @@
|
||||
/datum/ai_behavior/monkey_attack_mob
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration
|
||||
|
||||
/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller)
|
||||
/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
controller.current_movement_target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
|
||||
|
||||
/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
|
||||
var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
|
||||
@@ -288,3 +292,17 @@
|
||||
enemies[your_enemy] = MONKEY_RECRUIT_HATED_AMOUNT
|
||||
monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN
|
||||
finish_action(controller, TRUE)
|
||||
|
||||
/datum/ai_behavior/monkey_set_combat_target/perform(delta_time, datum/ai_controller/controller, set_key, enemies_key)
|
||||
var/list/enemies = controller.blackboard[enemies_key]
|
||||
var/list/valids = list()
|
||||
for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, controller.pawn))
|
||||
if(possible_enemy == controller.pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?)
|
||||
continue
|
||||
// Weighted list, so the closer they are the more likely they are to be chosen as the enemy
|
||||
valids[possible_enemy] = CEILING(100 / (get_dist(controller.pawn, possible_enemy) || 1), 1)
|
||||
|
||||
if(!valids.len)
|
||||
finish_action(controller, FALSE)
|
||||
controller.blackboard[set_key] = pickweight(valids)
|
||||
finish_action(controller, TRUE)
|
||||
|
||||
@@ -7,9 +7,11 @@ have ways of interacting with a specific mob and control it.
|
||||
/datum/ai_controller/monkey
|
||||
movement_delay = 0.4 SECONDS
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/monkey_tree,
|
||||
//the monkey tree is mostly combat related, if it isn't busy with that then it may do instrument related things
|
||||
/datum/ai_planning_subtree/play_instrument,
|
||||
/datum/ai_planning_subtree/generic_resist,
|
||||
/datum/ai_planning_subtree/monkey_combat,
|
||||
/datum/ai_planning_subtree/generic_hunger,
|
||||
/datum/ai_planning_subtree/generic_play_instrument,
|
||||
/datum/ai_planning_subtree/monkey_shenanigans,
|
||||
)
|
||||
blackboard = list(
|
||||
BB_MONKEY_AGGRESSIVE = FALSE,
|
||||
@@ -23,7 +25,6 @@ have ways of interacting with a specific mob and control it.
|
||||
BB_MONKEY_CURRENT_ATTACK_TARGET = null,
|
||||
BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE,
|
||||
BB_MONKEY_GUN_WORKED = TRUE,
|
||||
BB_MONKEY_NEXT_HUNGRY = 0,
|
||||
BB_SONG_LINES = MONKEY_SONG,
|
||||
)
|
||||
var/static/list/loc_connections = list(
|
||||
@@ -43,8 +44,6 @@ have ways of interacting with a specific mob and control it.
|
||||
if(!isliving(new_pawn))
|
||||
return AI_CONTROLLER_INCOMPATIBLE
|
||||
|
||||
blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(0, 300)
|
||||
|
||||
var/mob/living/living_pawn = new_pawn
|
||||
RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
|
||||
RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)
|
||||
@@ -58,7 +57,6 @@ have ways of interacting with a specific mob and control it.
|
||||
RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, .proc/on_attack_hulk)
|
||||
RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, .proc/on_attempt_cuff)
|
||||
RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, .proc/update_movespeed)
|
||||
RegisterSignal(new_pawn, COMSIG_FOOD_EATEN, .proc/on_eat)
|
||||
|
||||
AddComponent(/datum/component/connect_loc_behalf, new_pawn, loc_connections)
|
||||
movement_delay = living_pawn.cached_multiplicative_slowdown
|
||||
@@ -103,11 +101,11 @@ have ways of interacting with a specific mob and control it.
|
||||
for(var/obj/item/item in oview(2, living_pawn))
|
||||
nearby_items += item
|
||||
|
||||
weapon = GetBestWeapon(nearby_items, living_pawn.held_items)
|
||||
weapon = GetBestWeapon(src, nearby_items, living_pawn.held_items)
|
||||
|
||||
var/pickpocket = FALSE
|
||||
for(var/mob/living/carbon/human/human in oview(5, living_pawn))
|
||||
var/obj/item/held_weapon = GetBestWeapon(human.held_items + weapon, living_pawn.held_items)
|
||||
var/obj/item/held_weapon = GetBestWeapon(src, human.held_items + weapon, living_pawn.held_items)
|
||||
if(held_weapon == weapon) // It's just the same one, not a held one
|
||||
continue
|
||||
pickpocket = TRUE
|
||||
@@ -124,47 +122,6 @@ have ways of interacting with a specific mob and control it.
|
||||
queue_behavior(/datum/ai_behavior/monkey_equip/ground)
|
||||
return TRUE
|
||||
|
||||
/// Returns either the best weapon from the given choices or null if held weapons are better
|
||||
/datum/ai_controller/monkey/proc/GetBestWeapon(list/choices, list/held_weapons)
|
||||
var/gun_neurons_activated = blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED]
|
||||
var/top_force = 0
|
||||
var/obj/item/top_force_item
|
||||
for(var/obj/item/item as anything in held_weapons)
|
||||
if(!item)
|
||||
continue
|
||||
if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || blackboard[BB_MONKEY_BLACKLISTITEMS][item])
|
||||
continue
|
||||
if(gun_neurons_activated && istype(item, /obj/item/gun))
|
||||
// We have a gun, why bother looking for something inferior
|
||||
// Also yes it is intentional that monkeys dont know how to pick the best gun
|
||||
return item
|
||||
if(item.force > top_force)
|
||||
top_force = item.force
|
||||
top_force_item = item
|
||||
|
||||
for(var/obj/item/item as anything in choices)
|
||||
if(!item)
|
||||
continue
|
||||
if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || blackboard[BB_MONKEY_BLACKLISTITEMS][item])
|
||||
continue
|
||||
if(gun_neurons_activated && istype(item, /obj/item/gun))
|
||||
return item
|
||||
if(item.force <= top_force)
|
||||
continue
|
||||
top_force_item = item
|
||||
top_force = item.force
|
||||
|
||||
return top_force_item
|
||||
|
||||
/datum/ai_controller/monkey/proc/IsEdible(obj/item/thing)
|
||||
if(IS_EDIBLE(thing))
|
||||
return TRUE
|
||||
if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass))
|
||||
var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing
|
||||
if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid!
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
///Reactive events to being hit
|
||||
/datum/ai_controller/monkey/proc/retaliate(mob/living/L)
|
||||
var/list/enemies = blackboard[BB_MONKEY_ENEMIES]
|
||||
@@ -251,7 +208,3 @@ have ways of interacting with a specific mob and control it.
|
||||
/datum/ai_controller/monkey/proc/target_del(target)
|
||||
SIGNAL_HANDLER
|
||||
blackboard[BB_MONKEY_BLACKLISTITEMS] -= target
|
||||
|
||||
/datum/ai_controller/monkey/proc/on_eat(mob/living/pawn)
|
||||
SIGNAL_HANDLER
|
||||
blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(120, 600) SECONDS
|
||||
|
||||
@@ -1,102 +1,62 @@
|
||||
/datum/ai_planning_subtree/monkey_tree/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
|
||||
if(SHOULD_RESIST(living_pawn) && DT_PROB(MONKEY_RESIST_PROB, delta_time))
|
||||
controller.queue_behavior(/datum/ai_behavior/resist) //BRO IM ON FUCKING FIRE BRO
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY.
|
||||
|
||||
var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]
|
||||
|
||||
if(HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) //Not a pacifist? lets try some combat behavior.
|
||||
return
|
||||
|
||||
var/mob/living/selected_enemy
|
||||
if(length(enemies) || controller.blackboard[BB_MONKEY_AGGRESSIVE]) //We have enemies or are pissed
|
||||
var/list/valids = list()
|
||||
for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, living_pawn))
|
||||
if(possible_enemy == living_pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?)
|
||||
continue
|
||||
// Weighted list, so the closer they are the more likely they are to be chosen as the enemy
|
||||
valids[possible_enemy] = CEILING(100 / (get_dist(living_pawn, possible_enemy) || 1), 1)
|
||||
|
||||
selected_enemy = pickweight(valids)
|
||||
|
||||
if(selected_enemy)
|
||||
if(!selected_enemy.stat) //He's up, get him!
|
||||
if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle
|
||||
controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
|
||||
controller.queue_behavior(/datum/ai_behavior/monkey_flee)
|
||||
return //I'm running fuck you guys
|
||||
|
||||
if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing.
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
|
||||
controller.current_movement_target = selected_enemy
|
||||
if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time)
|
||||
controller.queue_behavior(/datum/ai_behavior/recruit_monkeys)
|
||||
controller.queue_behavior(/datum/ai_behavior/battle_screech/monkey)
|
||||
controller.queue_behavior(/datum/ai_behavior/monkey_attack_mob)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //Focus on this
|
||||
|
||||
else //He's down, can we disposal him?
|
||||
var/obj/machinery/disposal/bodyDisposal = locate(/obj/machinery/disposal/) in view(MONKEY_ENEMY_VISION, living_pawn)
|
||||
if(bodyDisposal)
|
||||
controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
|
||||
controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = bodyDisposal
|
||||
controller.queue_behavior(/datum/ai_behavior/disposal_mob, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_TARGET_DISPOSAL)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
/datum/ai_planning_subtree/monkey_shenanigans/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
|
||||
|
||||
if(prob(5))
|
||||
controller.queue_behavior(/datum/ai_behavior/use_in_hand)
|
||||
|
||||
if(selected_enemy || !DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time))
|
||||
if(!DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time))
|
||||
return
|
||||
|
||||
if(world.time >= controller.blackboard[BB_MONKEY_NEXT_HUNGRY])
|
||||
var/list/food_candidates = list()
|
||||
for(var/obj/item as anything in living_pawn.held_items)
|
||||
if(!item || !controller.IsEdible(item))
|
||||
continue
|
||||
food_candidates += item
|
||||
|
||||
for(var/obj/item/candidate in oview(2, living_pawn))
|
||||
if(!controller.IsEdible(candidate))
|
||||
continue
|
||||
food_candidates += candidate
|
||||
|
||||
if(length(food_candidates))
|
||||
var/obj/item/best_held = controller.GetBestWeapon(null, living_pawn.held_items)
|
||||
for(var/obj/item/held as anything in living_pawn.held_items)
|
||||
if(!held || held == best_held)
|
||||
continue
|
||||
living_pawn.dropItemToGround(held)
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/consume, pick(food_candidates))
|
||||
return
|
||||
if(!controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_nearby, BB_MONKEY_CURRENT_PRESS_TARGET)
|
||||
return
|
||||
|
||||
if(prob(50))
|
||||
var/list/possible_targets = list()
|
||||
for(var/atom/thing in view(2, living_pawn))
|
||||
if(!thing.mouse_opacity)
|
||||
continue
|
||||
if(thing.IsObscured())
|
||||
continue
|
||||
possible_targets += thing
|
||||
var/atom/target = pick(possible_targets)
|
||||
if(target)
|
||||
controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET] = target
|
||||
controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET)
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if(prob(5) && (locate(/obj/item) in living_pawn.held_items))
|
||||
var/list/possible_receivers = list()
|
||||
for(var/mob/living/candidate in oview(2, controller.pawn))
|
||||
possible_receivers += candidate
|
||||
if(!controller.blackboard[BB_MONKEY_CURRENT_GIVE_TARGET])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/pawn_must_hold_item, BB_MONKEY_CURRENT_GIVE_TARGET, /mob/living, 2)
|
||||
return
|
||||
|
||||
if(length(possible_receivers))
|
||||
controller.blackboard[BB_MONKEY_CURRENT_GIVE_TARGET] = pick(possible_receivers)
|
||||
controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET)
|
||||
return
|
||||
if(prob(5))
|
||||
controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
controller.TryFindWeapon()
|
||||
|
||||
///monkey combat subtree.
|
||||
/datum/ai_planning_subtree/monkey_combat/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]
|
||||
|
||||
if((HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) || (!length(enemies) && !controller.blackboard[BB_MONKEY_AGGRESSIVE])) //Pacifist, or we have no enemies and we're not pissed
|
||||
return
|
||||
|
||||
if(!controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET])
|
||||
controller.queue_behavior(/datum/ai_behavior/monkey_set_combat_target, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_ENEMIES)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
var/mob/living/selected_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
|
||||
|
||||
if(!selected_enemy.stat) //He's up, get him!
|
||||
if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle
|
||||
controller.queue_behavior(/datum/ai_behavior/monkey_flee)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //I'm running fuck you guys
|
||||
|
||||
if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing.
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time)
|
||||
controller.queue_behavior(/datum/ai_behavior/recruit_monkeys, BB_MONKEY_CURRENT_ATTACK_TARGET)
|
||||
controller.queue_behavior(/datum/ai_behavior/battle_screech/monkey)
|
||||
controller.queue_behavior(/datum/ai_behavior/monkey_attack_mob, BB_MONKEY_CURRENT_ATTACK_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
//by this point we have a target but they're down, let's try dumpstering this loser
|
||||
|
||||
if(!controller.blackboard[BB_MONKEY_TARGET_DISPOSAL])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_MONKEY_TARGET_DISPOSAL, /obj/machinery/disposal, MONKEY_ENEMY_VISION)
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/disposal_mob, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_TARGET_DISPOSAL)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/datum/ai_planning_subtree/cursed/SelectBehaviors(datum/ai_controller/controller, delta_time)
|
||||
var/obj/item/item_pawn = controller.pawn
|
||||
|
||||
//make sure we have a target
|
||||
var/mob/living/carbon/curse_target = controller.blackboard[BB_ITEM_TARGET]
|
||||
|
||||
if(curse_target && get_dist(curse_target, item_pawn) > ITEM_AGGRO_VIEW_RANGE)
|
||||
controller.blackboard[BB_ITEM_TARGET] = null
|
||||
return
|
||||
|
||||
if(!controller.blackboard[BB_ITEM_TARGET])
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/item_target, BB_ITEM_TARGET, /mob/living/carbon, ITEM_AGGRO_VIEW_RANGE)
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly, BB_ITEM_TARGET, BB_ITEM_THROW_ATTEMPT_COUNT)
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
///This behavior is for obj/items, it is used to free themselves out of the hands of whoever is holding them
|
||||
/datum/ai_behavior/item_escape_grasp
|
||||
|
||||
/datum/ai_behavior/item_escape_grasp/perform(delta_time, datum/ai_controller/controller)
|
||||
. = ..()
|
||||
var/obj/item/item_pawn = controller.pawn
|
||||
var/mob/item_holder = item_pawn.loc
|
||||
if(!istype(item_holder))
|
||||
finish_action(controller, FALSE) //We're no longer beind held. abort abort!!
|
||||
item_pawn.visible_message(span_warning("[item_pawn] slips out of the hands of [item_holder]!"))
|
||||
item_holder.dropItemToGround(item_pawn, TRUE)
|
||||
finish_action(controller, TRUE)
|
||||
|
||||
///This behavior is for obj/items, it is used to move closer to a target and throw themselves towards them.
|
||||
/datum/ai_behavior/item_move_close_and_attack
|
||||
required_distance = 3
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
|
||||
action_cooldown = 20
|
||||
///Sound to use
|
||||
var/attack_sound
|
||||
///Max attempts to make
|
||||
var/max_attempts = 3
|
||||
|
||||
|
||||
/datum/ai_behavior/item_move_close_and_attack/setup(datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key)
|
||||
. = ..()
|
||||
controller.current_movement_target = controller.blackboard[target_key]
|
||||
|
||||
|
||||
/datum/ai_behavior/item_move_close_and_attack/perform(delta_time, datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key)
|
||||
. = ..()
|
||||
var/obj/item/item_pawn = controller.pawn
|
||||
var/atom/throw_target = controller.blackboard[target_key]
|
||||
|
||||
item_pawn.visible_message(span_warning("[item_pawn] hurls towards [throw_target]!"))
|
||||
item_pawn.throw_at(throw_target, rand(4,5), 9)
|
||||
playsound(item_pawn.loc, attack_sound, 100, TRUE)
|
||||
controller.blackboard[throw_count_key]++
|
||||
if(controller.blackboard[throw_count_key] >= max_attempts)
|
||||
finish_action(controller, TRUE, target_key, throw_count_key)
|
||||
|
||||
/datum/ai_behavior/item_move_close_and_attack/finish_action(datum/ai_controller/controller, succeeded, aggro_list_key, target_key, throw_count_key)
|
||||
. = ..()
|
||||
var/atom/throw_target = controller.blackboard[target_key]
|
||||
|
||||
controller.blackboard -= target_key
|
||||
controller.blackboard[throw_count_key] = 0
|
||||
if(aggro_list_key)
|
||||
var/list/aggro_list = controller.blackboard[aggro_list_key]
|
||||
aggro_list[throw_target]--
|
||||
|
||||
/datum/ai_behavior/item_move_close_and_attack/ghostly
|
||||
attack_sound = 'sound/items/haunted/ghostitemattack.ogg'
|
||||
max_attempts = 4
|
||||
@@ -214,6 +214,7 @@
|
||||
#include "code\__HELPERS\_logging.dm"
|
||||
#include "code\__HELPERS\_string_lists.dm"
|
||||
#include "code\__HELPERS\admin.dm"
|
||||
#include "code\__HELPERS\ai.dm"
|
||||
#include "code\__HELPERS\areas.dm"
|
||||
#include "code\__HELPERS\atmospherics.dm"
|
||||
#include "code\__HELPERS\atoms.dm"
|
||||
|
||||
Reference in New Issue
Block a user