From 61bb059ae26f2796436500a6396367f3966c4fee Mon Sep 17 00:00:00 2001 From: Neerti Date: Thu, 22 Mar 2018 19:55:37 -0400 Subject: [PATCH] further work --- code/__defines/mobs.dm | 20 +- code/_onclick/item_attack.dm | 2 +- code/modules/ai/__readme.dm | 57 ++++ code/modules/ai/_defines.dm | 28 +- code/modules/ai/ai_holder.dm | 169 ++++++++-- code/modules/ai/ai_holder_combat.dm | 318 +++++++++--------- code/modules/ai/ai_holder_combat_unseen.dm | 43 +++ code/modules/ai/ai_holder_communication.dm | 65 +++- code/modules/ai/ai_holder_cooperation.dm | 110 ++++++ code/modules/ai/ai_holder_debug.dm | 100 +++++- code/modules/ai/ai_holder_fleeing.dm | 38 +++ code/modules/ai/ai_holder_follow.dm | 60 ++++ code/modules/ai/ai_holder_movement.dm | 207 ++++++------ code/modules/ai/ai_holder_pathfinding.dm | 58 ++++ code/modules/ai/ai_holder_targeting.dm | 159 +++++++++ code/modules/ai/interfaces.dm | 22 +- code/modules/ai/say_list.dm | 34 ++ code/modules/client/client defines.dm | 2 +- code/modules/mob/_modifiers/modifiers_misc.dm | 2 +- code/modules/mob/emote.dm | 7 + code/modules/mob/living/living_defines.dm | 1 + .../mob/living/simple_animal/simple_animal.dm | 22 +- .../living/simple_animal/simple_animal2.dm | 74 ++++ code/modules/mob/mob.dm | 5 +- code/modules/mob/mob_defines.dm | 3 +- code/modules/mob/mob_movement.dm | 23 +- code/stylesheet.dm | 7 + polaris.dme | 10 +- 28 files changed, 1289 insertions(+), 357 deletions(-) create mode 100644 code/modules/ai/__readme.dm create mode 100644 code/modules/ai/ai_holder_combat_unseen.dm create mode 100644 code/modules/ai/ai_holder_cooperation.dm create mode 100644 code/modules/ai/ai_holder_fleeing.dm create mode 100644 code/modules/ai/ai_holder_follow.dm create mode 100644 code/modules/ai/ai_holder_pathfinding.dm create mode 100644 code/modules/ai/ai_holder_targeting.dm create mode 100644 code/modules/ai/say_list.dm create mode 100644 code/modules/mob/living/simple_animal/simple_animal2.dm diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm index 25c75dea73..44d0389d5a 100644 --- a/code/__defines/mobs.dm +++ b/code/__defines/mobs.dm @@ -25,6 +25,9 @@ #define BORGTHERM 0x2 #define BORGXRAY 0x4 #define BORGMATERIAL 8 + +#define STANCE_ATTACK 11 // Backwards compatability +#define STANCE_ATTACKING 12 // Ditto /* #define STANCE_IDLE 1 // Looking for targets if hostile. Does idle wandering. #define STANCE_ALERT 2 // Bears @@ -37,13 +40,16 @@ #define STANCE_SLEEP 0 // Doing (almost) nothing, to save on CPU because nobody is around to notice or the mob died. #define STANCE_IDLE 1 // The more or less default state. Wanders around, looks for baddies, and spouts one-liners. #define STANCE_ALERT 2 // A baddie is visible but not too close, and essentially we tell them to go away or die. -#define STANCE_ATTACK 3 // Attempting to get into range to attack them. -#define STANCE_ATTACKING 4 // Actually fighting, with melee or ranged. -#define STANCE_REPOSITION 5 // Relocating to a better position while in combat. Only used for ranged mobs since melee only has one better position, which STANCE_ATTACK already handles. -#define STANCE_MOVE 6 // Similar to above but for out of combat. If a baddie is seen, they'll cancel and fight them. -#define STANCE_FOLLOW 7 // Following somone, without trying to murder them. -#define STANCE_FLEE 8 // Run away from the target because they're too spooky/we're dying/some other reason. -#define STANCE_STUNNED 9 // Do nothing, because the mob is unable to act in some form. Can be applied by other disabling effects besides stuns. +#define STANCE_APPROACH 3 // Attempting to get into range to attack them. +#define STANCE_FIGHT 4 // Actually fighting, with melee or ranged. +#define STANCE_BLINDFIGHT 5 // Fighting something that cannot be seen by the mob, from invisibility or out of sight. +#define STANCE_REPOSITION 6 // Relocating to a better position while in combat. Also used when moving away from a danger like grenades. +#define STANCE_MOVE 7 // Similar to above but for out of combat. If a baddie is seen, they'll cancel and fight them. +#define STANCE_FOLLOW 8 // Following somone, without trying to murder them. +#define STANCE_FLEE 9 // Run away from the target because they're too spooky/we're dying/some other reason. +#define STANCE_STUNNED 10 // Do nothing, because the mob is unable to act in some form. Can be applied by other disabling effects besides stuns. + +#define STANCES_COMBAT list(STANCE_ALERT, STANCE_APPROACH, STANCE_FIGHT, STANCE_BLINDFIGHT, STANCE_REPOSITION) #define LEFT 0x1 #define RIGHT 0x2 diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 5808fc4498..a066981e01 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -59,7 +59,7 @@ avoid code duplication. This includes items that may sometimes act as a standard // Same as above but actually does useful things. // W is the item being used in the attack, if any. modifier is if the attack should be longer or shorter than usual, for whatever reason. /mob/living/get_attack_speed(var/obj/item/W) - var/speed = DEFAULT_ATTACK_COOLDOWN + var/speed = base_attack_cooldown if(W && istype(W)) speed = W.attackspeed for(var/datum/modifier/M in modifiers) diff --git a/code/modules/ai/__readme.dm b/code/modules/ai/__readme.dm new file mode 100644 index 0000000000..583fc1e0ca --- /dev/null +++ b/code/modules/ai/__readme.dm @@ -0,0 +1,57 @@ +/* + + This module contains an AI implementation designed to be (mostly) mobtype-agnostic, by being held inside a datum instead of being on the mob directly. + + +Seperation: + +The ai_holder datum is designed to be fairly distant from its mob holder, in terms of coupling. +This presents some advantages. + * Not being tied to the mob's Life() cycle allows for a different tick rate. + * Being seperate from the mob simplifies mob code greatly. + * It allows for better encapsulation and seperation of duties from the mob. + * It is more logical to think that the mob is the 'body', where as its ai_holder is the 'mind'. + * It can be made mobtype-independant with the use of Interfaces. + + + The datum is held by the mob that it should control, called the holder, and is not processed by the mob itself (as Life() did so in previous implementations). +Instead, each instance of /datum/ai_holder is processed by the 'AI' master controller subsystem. The datum itself has two seperate process tracks instead of one, +as most other objects ( process() ) or mobs ( Life() ) do. + + + + +Flow of Execution: + + / - Every 0.5s - > /datum/ai_holder/handle_tactics() - > /datum/ai_holder/handle_stance_tactical() - > switch(stance)... + AI Subsystem - * + \ - Every 2.0s - > /datum/ai_holder/handle_strategicals() - > /datum/ai_holder/handle_stance_strategical() - > switch(stance)... + + The datum is not driven by its mob, as previous implementations did, meaning Life() is not involved. Instead, it is processed by a specific Master Controller Subsystem +titled 'AI', which by default ticks every half a second. Each instance of the ai_holder datum that is not 'asleep' is part of a list, containing ai_holders that are awake. +When the subsystem runs, each ai_holder instance inside the list is iterated on, and calls one or two procs on it. +Every tick, each instance has handle_tactics() called on it, and every four ticks, handle_strategicals(), meaning every half a second and very two seconds, respectively. +This means that the ai_holder datum has two seperate types of processing, for the purposes of efficency. + + handle_tactics() is used for 'cheap' processing that needs to happen fast, such as walking along a path, initiating an attack, or firing a gun. +The rate that it is called allows for the ai_holder to interact with the world through its mob very often, giving a more convincing appearance of intelligence, +allowing for faster reaction times to certain events, and allowing for variable attack speeds that would not be possible when bound to a two second Life() cycle. + + handle_strategicals() on the other hand, is for 'expensive' processing that might be too demanding on the CPU to do every half a second, such as re/calculating an A* path, or +running a complicated tension assessment to determine how brave the mob is feeling. This is the same delay used for certain tasks in the old implementation. + + Having two seperate process procs allows for 'fast' and 'slow' actions to be more easily encapsulated, and ensures that all ai_holders are syncronized with each other, +as opposed to having individual tick counters inside all of the ai_holder instances. It should be noted that handle_tactics() is called first, +before handle_strategicals() every two seconds. + + Both process procs work in a similar fashion, using a large amount of 'stances' to act in a specific way, effectively creating a state pattern. +There are 10 stances implemented, and more can easily be added by defining them and switching to them with the set_stance() proc. +This is similar to the old implementation, except with a vastly larger amount of options for the AI to make use of. +See code/__defines/mob.dm for the stance defines and descriptions about their purpose. + +Each stance is evaluated once per tick, meaning that switching the stance takes effect on the next tick. + + + + +*/ \ No newline at end of file diff --git a/code/modules/ai/_defines.dm b/code/modules/ai/_defines.dm index 3da2641b75..354f428e88 100644 --- a/code/modules/ai/_defines.dm +++ b/code/modules/ai/_defines.dm @@ -1,3 +1,25 @@ -#define AI_DUMB 1 -#define AI_NORMAL 2 -#define AI_SMART 3 \ No newline at end of file +// Defines for the ai_intelligence var. +// Controls if the mob will do 'advanced tactics' like running from grenades. +#define AI_DUMB 1 // Be dumber than usual. +#define AI_NORMAL 2 // Default level. +#define AI_SMART 3 // Will do more processing to perceive threats such as active grenades. Use sparingly. + +#define ai_log(M,V) if(debug_ai) ai_log_output(M,V) + +// Logging level defines. +#define AI_LOG_OFF 0 // Don't show anything. +#define AI_LOG_ERROR 1 // Show logs of things likely causing the mob to not be functioning correctly. +#define AI_LOG_WARNING 2 // Show less serious but still helpful to know issues that might be causing things to work incorrectly. +#define AI_LOG_INFO 3 // Important regular events, like selecting a target or switching stances. +#define AI_LOG_DEBUG 4 // More detailed information about the flow of execution. +#define AI_LOG_TRACE 5 // Even more detailed than the last. + +// Reasons for targets to not be valid. Based on why, the AI responds differently. +#define AI_TARGET_VALID 0 // We can fight them. +#define AI_TARGET_INVIS 1 // They were in field of view but became invisible. Switch to STANCE_BLINDFIGHT if no other viable targets exist. +#define AI_TARGET_NOSIGHT 2 // No longer in field of view. Go STANCE_REPOSITION to their last known location if no other targets are seen. +#define AI_TARGET_ALLY 3 // They are an ally. Find a new target. +#define AI_TARGET_DEAD 4 // They're dead. Find a new target. +#define AI_TARGET_INVINCIBLE 5 // Target is currently unable to receive damage for whatever reason. Find a new target or wait. + +//TRACE DEBUG INFO WARN ERROR OFF \ No newline at end of file diff --git a/code/modules/ai/ai_holder.dm b/code/modules/ai/ai_holder.dm index 4b45aa8228..41c5210833 100644 --- a/code/modules/ai/ai_holder.dm +++ b/code/modules/ai/ai_holder.dm @@ -18,7 +18,7 @@ /datum/ai_holder var/mob/living/holder = null // The mob this datum is going to control. var/stance = STANCE_IDLE // Determines if the mob should be doing a specific thing, e.g. attacking, following, standing around, etc. - var/intelligence_level = AI_NORMAL + var/intelligence_level = AI_NORMAL // Adjust to make the AI be intentionally dumber, or make it more robust (e.g. dodging grenades). @@ -29,10 +29,6 @@ hostile = TRUE retaliate = TRUE -/datum/ai_holder/test - hostile = TRUE - use_astar = TRUE - /datum/ai_holder/New(var/new_holder) ASSERT(new_holder) holder = new_holder @@ -49,18 +45,31 @@ // Now for the actual AI stuff. +// Makes this ai holder not get processed. +// Called automatically when the host mob is killed. +// Potential future optimization would be to sleep AIs which mobs that are far away from in-round players. /datum/ai_holder/proc/go_sleep() if(stance == STANCE_SLEEP) return - stance = STANCE_SLEEP + forget_everything() // If we ever wake up, its really unlikely that our current memory will be of use. + set_stance(STANCE_SLEEP) SSai.processing -= src +// Reverses the above proc. +// Revived mobs will wake their AI if they have one. /datum/ai_holder/proc/go_wake() if(stance != STANCE_SLEEP) return - stance = STANCE_IDLE + set_stance(STANCE_IDLE) SSai.processing += src +// Resets a lot of 'memory' vars. +/datum/ai_holder/proc/forget_everything() + // Some of these might be redundant, but hopefully this prevents future bugs if that changes. + lose_follow() + lose_target() + lose_target_position() + give_up_movement() // 'Tactical' processes such as moving a step, meleeing an enemy, firing a projectile, and other fairly cheap actions that need to happen quickly. /datum/ai_holder/proc/handle_tactics() @@ -68,7 +77,7 @@ // 'Strategical' processes that are more expensive on the CPU and so don't get run as often as the above proc, such as A* pathfinding or robust targeting. /datum/ai_holder/proc/handle_strategicals() - world << "[holder.name] Strategicals!" +// world << "[holder.name] Strategicals!" handle_stance_strategical() /* @@ -100,42 +109,141 @@ // For setting the stance WITHOUT processing it /datum/ai_holder/proc/set_stance(var/new_stance) + ai_log("set_stance() : Setting stance from [stance] to [new_stance].", AI_LOG_INFO) stance = new_stance + if(stance_coloring) // For debugging or really weird mobs. + stance_color() -/datum/ai_holder/proc/handle_stance_tactical(var/new_stance) - if(new_stance) - set_stance(new_stance) +// This is called every half a second. +/datum/ai_holder/proc/handle_stance_tactical() + ai_log("========= Fast Process Beginning ==========", AI_LOG_TRACE) // This is to make it easier visually to disinguish between 'blocks' of what a tick did. + ai_log("handle_stance_tactical() : Called.", AI_LOG_TRACE) - if(attack_cooldown_left > 0) - attack_cooldown_left-- + if(stance == STANCE_SLEEP) + ai_log("handle_stance_tactical() : Going to sleep.", AI_LOG_TRACE) + go_sleep() + return + + if(target && can_see_target(target)) + track_target_position() + + if(!can_act()) + ai_log("handle_stance_tactical() : Stunned.", AI_LOG_TRACE) + set_stance(STANCE_STUNNED) + return + + if(stance in STANCES_COMBAT) + // Should resist? We check this before fleeing so that we can actually flee and not be trapped in a chair. + if(holder.incapacitated(INCAPACITATION_BUCKLED_PARTIALLY)) + ai_log("handle_stance_tactical() : Going to handle_resist().", AI_LOG_TRACE) + handle_resist() + + else if(istype(holder.loc, /obj/structure/closet)) + var/obj/structure/closet/C = holder.loc + ai_log("handle_stance_tactical() : Inside a closet. Going to attempt escape.", AI_LOG_TRACE) + if(C.welded) + holder.resist() + else + C.open() + + // Should we flee? + if(should_flee()) + ai_log("handle_stance_tactical() : Going to flee.", AI_LOG_TRACE) + set_stance(STANCE_FLEE) + return switch(stance) - if(STANCE_SLEEP) - go_sleep() - return if(STANCE_IDLE) - holder.a_intent = I_HELP + if(should_go_home()) + ai_log("handle_stance_tactical() : STANCE_IDLE, going to go home.", AI_LOG_TRACE) + go_home() + + else if(should_follow_leader()) + ai_log("handle_stance_tactical() : STANCE_IDLE, going to follow leader.", AI_LOG_TRACE) + set_stance(STANCE_FOLLOW) + + else if(should_wander()) + ai_log("handle_stance_tactical() : STANCE_IDLE, going to wander randomly.", AI_LOG_TRACE) + handle_wander_movement() - // if(hostile) - // find_target() if(STANCE_ALERT) + ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE) threaten_target() - if(STANCE_ATTACK) - if(target) - walk_to_target() + if(STANCE_APPROACH) + ai_log("handle_stance_tactical() : STANCE_APPROACH, going to walk_to_target().", AI_LOG_TRACE) + walk_to_target() - if(STANCE_ATTACKING) + if(STANCE_FIGHT) + ai_log("handle_stance_tactical() : STANCE_FIGHT, going to engage_target().", AI_LOG_TRACE) engage_target() + if(STANCE_MOVE) + ai_log("handle_stance_tactical() : STANCE_MOVE, going to walk_to_destination().", AI_LOG_TRACE) + walk_to_destination() + + if(STANCE_REPOSITION) // This is the same as above but doesn't stop if an enemy is visible since its an 'in-combat' move order. + ai_log("handle_stance_tactical() : STANCE_REPOSITION, going to walk_to_destination().", AI_LOG_TRACE) + walk_to_destination() + + if(STANCE_FOLLOW) + ai_log("handle_stance_tactical() : STANCE_FOLLOW, going to walk_to_leader().", AI_LOG_TRACE) + walk_to_leader() + + if(STANCE_FLEE) + ai_log("handle_stance_tactical() : STANCE_FLEE, going to flee_from_target().", AI_LOG_TRACE) + flee_from_target() + + if(STANCE_STUNNED) + ai_log("handle_stance_tactical() : STANCE_STUNNED.", AI_LOG_TRACE) + if(can_act()) + ai_log("handle_stance_tactical() : No longer stunned.", AI_LOG_TRACE) + set_stance(STANCE_IDLE) + + ai_log("handle_stance_tactical() : Exiting.", AI_LOG_TRACE) + ai_log("========= Fast Process Ending ==========", AI_LOG_TRACE) + +// This is called every two seconds. /datum/ai_holder/proc/handle_stance_strategical() + ai_log("++++++++++ Slow Process Beginning ++++++++++", AI_LOG_TRACE) + ai_log("handle_stance_strategical() : Called.", AI_LOG_TRACE) + switch(stance) if(STANCE_IDLE) + + if(speak_chance) // In the long loop since otherwise it wont shut up. + handle_idle_speaking() + if(hostile) + ai_log("handle_stance_strategical() : STANCE_IDLE, going to find_target().", AI_LOG_TRACE) find_target() - if(STANCE_ATTACK) + if(STANCE_APPROACH) if(target) + ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE) calculate_path(target) + if(STANCE_MOVE) + if(hostile && find_target()) // This will switch its stance. + ai_log("handle_stance_strategical() : STANCE_MOVE, found target and was inturrupted.", AI_LOG_TRACE) + if(STANCE_FOLLOW) + if(hostile && find_target()) // This will switch its stance. + ai_log("handle_stance_strategical() : STANCE_FOLLOW, found target and was inturrupted.", AI_LOG_TRACE) + else if(leader) + ai_log("handle_stance_strategical() : STANCE_FOLLOW, going to calculate_path([leader]).", AI_LOG_TRACE) + calculate_path(leader) + + ai_log("handle_stance_strategical() : Exiting.", AI_LOG_TRACE) + ai_log("++++++++++ Slow Process Ending ++++++++++", AI_LOG_TRACE) + +/* + //Yes I'm breaking this into two if()'s for ease of reading + //If we ARE ALLOWED TO + if(returns_home && home_turf && !astarpathing && (world.time - stance_changed) > 10 SECONDS) + if(get_dist(src,home_turf) > wander_distance) + move_to_delay = initial(move_to_delay)*2 //Walk back. + GoHome() + else + stop_automated_movement = 0 +*/ /* // For proccessing the current stance, or setting and processing a new one @@ -173,14 +281,23 @@ return if(hostile) FindTarget() - if(STANCE_ATTACK) + if(STANCE_APPROACH) annoyed = 50 a_intent = I_HURT RequestHelp() MoveToTarget() - if(STANCE_ATTACKING) + if(STANCE_FIGHT) annoyed = 50 AttackTarget() */ +// If our holder is able to do anything. +/datum/ai_holder/proc/can_act() + if(holder.stat) // Dead or unconscious. + ai_log("can_act() : Stat was non-zero ([holder.stat]).", AI_LOG_TRACE) + return FALSE + if(holder.incapacitated(INCAPACITATION_DISABLED)) // Stunned in some form. + ai_log("can_act() : Incapacited.", AI_LOG_TRACE) + return FALSE + return TRUE diff --git a/code/modules/ai/ai_holder_combat.dm b/code/modules/ai/ai_holder_combat.dm index 484c14b180..d749761d1e 100644 --- a/code/modules/ai/ai_holder_combat.dm +++ b/code/modules/ai/ai_holder_combat.dm @@ -1,158 +1,89 @@ +// This file is for actual fighting. Targeting is in a seperate file. + /datum/ai_holder - var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing. - var/retaliate = FALSE // Requires hostile to be true to work. If both this and hostile are true, the mob won't attack unless attacked first. - var/cooperative = FALSE // If true, asks allies to help when fighting something. - var/firing_lanes = FALSE // If ture, tries to refrain from shooting allies or the wall. - var/conserve_ammo = FALSE // If true, the mob will avoid shooting anything that does not have a chance to hit a mob. Requires firing_lanes to be true. + var/firing_lanes = FALSE // If ture, tries to refrain from shooting allies or the wall. + var/conserve_ammo = FALSE // If true, the mob will avoid shooting anything that does not have a chance to hit a mob. Requires firing_lanes to be true. - var/atom/movable/target // The thing (mob or object) we're trying to kill. - var/attack_cooldown = 2 // If set, the mob will wait for the specified amount of ticks before attempting another attack. - var/attack_cooldown_left = 0 // Actual var for tracking if attacks are off cooldown or not. Note that melee and ranged attacks share this. + var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly. + var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy. + var/pointblank = FALSE // If ranged is true, and this is true, people adjacent to the mob will suffer the ranged instead of using a melee attack. - var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly. - var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy. - var/pointblank = FALSE // If ranged is true, and this is true, people adjacent to the mob will suffer the ranged instead of using a melee attack. + var/special_attack_prob = 0 // The chance to ATTEMPT a special_attack(). If it fails, it will do a regular attack instead. + var/special_attack_min_range = 2 // The minimum distance required for an attempt to be made. + var/special_attack_max_range = 7 // The maximum for an attempt. - var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting. + var/can_breakthrough = TRUE // If false, the AI will not try to break things like windows or other structures in the way. -/************** -* Strategical * -**************/ - -// Step 1, find out what we can see. -/datum/ai_holder/proc/list_targets() - . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions. - - var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) - - for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines)) - if(can_see(holder, HM, vision_range)) - . += HM - -// Step 2, filter down possible targets to things we actually care about. -/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE) - . = list() - if(!has_targets_list) - possible_targets = list_targets() - for(var/possible_target in possible_targets) - var/atom/A = possible_target -// if(Found(A)) // In case people want to override this. -// . = list(A) -// break - if(can_attack(A)) // Can we attack it? - . += A - continue - - var/new_target = pick_target(.) - give_target(new_target) - return new_target - -// Step 3, pick among the possible, attackable targets. -/datum/ai_holder/proc/pick_target(list/targets) - if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets. - for(var/possible_target in targets) - var/atom/A = possible_target - var/target_dist = get_dist(holder, target) - var/possible_target_distance = get_dist(holder, A) - if(target_dist < possible_target_distance) - targets -= A - if(!targets.len) // We found nothing. - return - var/chosen_target = pick(targets) - return chosen_target - -// Step 4, give us our selected target. -/datum/ai_holder/proc/give_target(new_target) - target = new_target - //LosePatience() - if(target != null) - //GainPatience() - //Aggro() - if(should_threaten()) - set_stance(STANCE_ALERT) - else - set_stance(STANCE_ATTACK) - return TRUE - -/datum/ai_holder/proc/can_attack(atom/movable/the_target) - if(!the_target) // Nothing to attack. - return FALSE - - if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well. - return FALSE - - if(isliving(the_target)) - var/mob/living/L = the_target - if(L.stat) - return FALSE - if(holder.IIsAlly(L)) - return FALSE - return TRUE - - if(istype(the_target, /obj/mecha)) - var/obj/mecha/M = the_target - if(M.occupant) - return can_attack(M.occupant) - - if(istype(the_target, /obj/machinery/porta_turret)) - var/obj/machinery/porta_turret/P = the_target - if(P.stat & BROKEN) - return FALSE // Already dead. - if(P.faction == holder.faction) - return FALSE // Don't shoot allied turrets. - if(!P.raised && !P.raising) - return FALSE // Turrets won't get hurt if they're still in their cover. - return TRUE - - return FALSE - -/*********** -* Tactical * -***********/ // This does the actual attacking. /datum/ai_holder/proc/engage_target() + ai_log("engage_target() : Entering.", AI_LOG_DEBUG) + + // Can we still see them? if(!target || !can_attack(target) || (!(target in list_targets())) ) - lose_target() - return + ai_log("engage_target() : Lost sight of target.", AI_LOG_TRACE) + lose_target() // We lost them. + + if(!find_target()) // If we can't get a new one, then wait for a bit and then time out. + set_stance(STANCE_IDLE) + lost_target() + ai_log("engage_target() : No more targets. Exiting.", AI_LOG_DEBUG) + return + // if(lose_target_time + lose_target_timeout < world.time) + // ai_log("engage_target() : Unseen enemy timed out.", AI_LOG_TRACE) + // set_stance(STANCE_IDLE) // It must've been the wind. + // lost_target() + // ai_log("engage_target() : Exiting.", AI_LOG_DEBUG) + // return + + // // But maybe we do one last ditch effort. + // if(!target_last_seen_turf || intelligence_level < AI_SMART) + // ai_log("engage_target() : No last known position or is too dumb to fight unseen enemies.", AI_LOG_TRACE) + // set_stance(STANCE_IDLE) + // else + // ai_log("engage_target() : Fighting unseen enemy.", AI_LOG_TRACE) + // engage_unseen_enemy() + else + ai_log("engage_target() : Got new target ([target]).", AI_LOG_TRACE) var/distance = get_dist(holder, target) + ai_log("engage_target() : Distance to target ([target]) is [distance].", AI_LOG_TRACE) holder.face_atom(target) last_conflict_time = world.time - // Stab them. - if(distance <= 1 && !pointblank) + request_help() // Call our allies. + + // Do a 'special' attack, if one is allowed. + if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range)) + ai_log("engage_target() : Attempting a special attack.", AI_LOG_TRACE) on_engagement(target) - if(attack_cooldown_left <= 0) + if(special_attack(target)) // If this fails, then we try a regular melee/ranged attack. + ai_log("engage_target() : Successful special attack. Exiting.", AI_LOG_DEBUG) + return - pre_melee_attack(target) - - if(melee_attack(target)) - post_melee_attack(target) - if(attack_cooldown) - attack_cooldown_left = attack_cooldown + // Stab them. + else if(distance <= 1 && !pointblank) + ai_log("engage_target() : Attempting a melee attack.", AI_LOG_TRACE) + on_engagement(target) + melee_attack(target) // Shoot them. else if(ranged && (distance <= shoot_range) ) on_engagement(target) - if(attack_cooldown_left <= 0) + if(firing_lanes && !test_projectile_safety(target)) + // Nudge them a bit, maybe they can shoot next time. + step_rand(holder) + holder.face_atom(target) + ai_log("engage_target() : Could not safely fire at target. Exiting.", AI_LOG_DEBUG) + return - if(firing_lanes && !test_projectile_safety(target)) - // Nudge them a bit, maybe they can shoot next time. - step_rand(holder) - holder.face_atom(target) - return - - pre_ranged_attack(target) - - if(ranged_attack(target)) - post_ranged_attack(target) - if(attack_cooldown) - attack_cooldown_left = attack_cooldown + ai_log("engage_target() : Attempting a ranged attack.", AI_LOG_TRACE) + ranged_attack(target) // Run after them. else - set_stance(STANCE_ATTACK) + ai_log("engage_target() : Target ([target]) too far away. Exiting.", AI_LOG_DEBUG) + set_stance(STANCE_APPROACH) // We're not entirely sure how holder will do melee attacks since any /mob/living could be holder, but we don't have to care because Interfaces. /datum/ai_holder/proc/melee_attack(atom/movable/AM) @@ -162,19 +93,15 @@ /datum/ai_holder/proc/ranged_attack(atom/movable/AM) return holder.IRangedAttack(AM) -// Called when within striking distance, however cooldown is not considered. +// Most mobs probably won't have this defined but we don't care. +/datum/ai_holder/proc/special_attack(atom/movable/AM) + return holder.ISpecialAttack(AM) + +// Called when within striking/shooting distance, however cooldown is not considered. +// Override to do things like move in a random step for evasiveness. +// Note that this is called BEFORE the attack. /datum/ai_holder/proc/on_engagement(atom/movable/AM) -// These two are called before an attack attempt. -/datum/ai_holder/proc/pre_melee_attack(atom/movable/AM) - -/datum/ai_holder/proc/pre_ranged_attack(atom/movable/AM) - -// These two are called after a successful(ish) attack. -/datum/ai_holder/proc/post_melee_attack(atom/movable/AM) - -/datum/ai_holder/proc/post_ranged_attack(atom/movable/AM) - // Used to make sure projectiles will probably hit the target and not the wall or a friend. /datum/ai_holder/proc/test_projectile_safety(atom/movable/AM) var/mob/living/L = check_trajectory(AM, holder) // This isn't always reliable but its better than the previous method. @@ -195,17 +122,104 @@ return !conserve_ammo // If we have infinite ammo than shooting the wall isn't so bad, but otherwise lets not. -//We can't see the target -/datum/ai_holder/proc/lose_target() - target = null - set_stance(STANCE_IDLE) - give_up_movement() - -//Target is no longer valid (?) -/datum/ai_holder/proc/lost_target() - set_stance(STANCE_IDLE) - give_up_movement() +// Test if we are within range to attempt an attack, melee or ranged. +/datum/ai_holder/proc/within_range(atom/movable/AM) + var/distance = get_dist(holder, AM) + if(distance <= 1) + return TRUE // Can melee. + else if(ranged && distance <= shoot_range) + return TRUE // Can shoot. + return FALSE // Can be used to conditionally do a ranged or melee attack. /datum/ai_holder/proc/closest_distance() return ranged ? shoot_range - 1 : 1 // Shoot range -1 just because we don't want to constantly get kited + +// Goes to the target, to attack them. +// Called when in STANCE_APPROACH. +/datum/ai_holder/proc/walk_to_target() + ai_log("walk_to_target() : Entering.", AI_LOG_DEBUG) + // Make sure we can still chase/attack them. + if(!target || !can_attack(target)) + ai_log("walk_to_target() : Lost target.", AI_LOG_INFO) + if(!find_target()) + lost_target() + ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG) + return + else + ai_log("walk_to_target() : Found new target ([target]).", AI_LOG_INFO) + + // Find out where we're going. + var/get_to = closest_distance() + var/distance = get_dist(holder, target) + ai_log("walk_to_target() : get_to is [get_to].", AI_LOG_TRACE) + + // We're here! + if(distance <= get_to) + ai_log("walk_to_target() : Within range.", AI_LOG_INFO) + forget_path() + set_stance(STANCE_FIGHT) + ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG) + return + + // Otherwise keep walking. + walk_path(target, get_to) + + ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG) + +// Resists out of things. +// Sometimes there are times you want your mob to be buckled to something, so override this for when that is needed. +/datum/ai_holder/proc/handle_resist() + holder.resist() + +// Used to break through windows and barriers to a target on the other side. +/datum/ai_holder/proc/breakthrough(atom/target_atom) + if(!can_breakthrough) + return FALSE + var/dir_to_target = get_dir(holder, target_atom) + holder.face_atom(target_atom) + + // First, try to break things directly in front of us. + var/result = destroy_surroundings(dir_to_target) + + // If that doesn't work, we might be trying to attack something diagonally. + // If so, we can try again with some adjustments to avoid invalid diagonal directions. + if(!result) + result = destroy_surroundings(turn(dir_to_target, 45)) + + // One last time, going the other way. + if(!result) + result = destroy_surroundings(turn(dir_to_target, -45)) + + // Welp. + return result + +/datum/ai_holder/proc/destroy_surroundings(direction) + if(!direction) + direction = pick(cardinal) // FLAIL WILDLY + + var/turf/problem_turf = get_step(holder, direction) + + // First, kill windows in the way. + for(var/obj/structure/window/W in problem_turf) + if(W.dir == reverse_dir[holder.dir]) // So that windows get smashed in the right order + ai_log("destroy_surroundings() : Attacking diagonal window.", AI_LOG_INFO) + return holder.IAttack(W) + + else if(W.is_fulltile()) + ai_log("destroy_surroundings() : Attacking full tile window.", AI_LOG_INFO) + return holder.IAttack(W) + + var/obj/structure/obstacle = locate(/obj/structure, problem_turf) + if(istype(obstacle, /obj/structure/window) || istype(obstacle, /obj/structure/closet) || istype(obstacle, /obj/structure/table) || istype(obstacle, /obj/structure/grille)) + ai_log("destroy_surroundings() : Attacking generic structure.", AI_LOG_INFO) + return holder.IAttack(obstacle) + + for(var/obj/machinery/door/D in problem_turf) // Required since firelocks take up the same turf. + if(D.density) + ai_log("destroy_surroundings() : Attacking closed door.", AI_LOG_INFO) + return holder.IAttack(D) + + return FALSE // Nothing to attack. + + diff --git a/code/modules/ai/ai_holder_combat_unseen.dm b/code/modules/ai/ai_holder_combat_unseen.dm new file mode 100644 index 0000000000..545281af9e --- /dev/null +++ b/code/modules/ai/ai_holder_combat_unseen.dm @@ -0,0 +1,43 @@ +// Used for fighting invisible things. + +// Used when a target is out of sight or invisible. +/datum/ai_holder/proc/engage_unseen_enemy() + // Lets do some last things before giving up. + if(!ranged) + if(get_dist(holder, target_last_seen_turf > 1)) // We last saw them over there. + // Go to where you last saw the enemy. + give_destination(target_last_seen_turf, 1, TRUE) // This will set it to STANCE_REPOSITION. + else // We last saw them next to us, so do a blind attack on that tile. + melee_on_tile(target_last_seen_turf) + + else if(!conserve_ammo) + shoot_near_turf(target_last_seen_turf) + +// This shoots semi-randomly near a specific turf. +/datum/ai_holder/proc/shoot_near_turf(turf/targeted_turf) + if(!ranged) + return // Can't shoot. + if(get_dist(holder, targeted_turf) > shoot_range) + return // Too far to shoot. + + var/turf/T = pick(RANGE_TURFS(2, targeted_turf)) // The turf we're actually gonna shoot at. + on_engagement(T) + if(firing_lanes && !test_projectile_safety(T)) + step_rand(holder) + holder.face_atom(T) + return + + ranged_attack(T) + +// Attempts to attack something on a specific tile. +// TODO: Put on mob/living? +/datum/ai_holder/proc/melee_on_tile(turf/T) + var/mob/living/L = locate() in T + if(!L) + T.visible_message("\The [holder] attacks nothing around \the [T].") + return + + if(holder.IIsAlly(L)) // Don't hurt our ally. + return + + melee_attack(L) \ No newline at end of file diff --git a/code/modules/ai/ai_holder_communication.dm b/code/modules/ai/ai_holder_communication.dm index dd311e3b5d..ba49fa29e6 100644 --- a/code/modules/ai/ai_holder_communication.dm +++ b/code/modules/ai/ai_holder_communication.dm @@ -1,6 +1,8 @@ +// Contains code for speaking and emoting. + /datum/ai_holder var/threaten = FALSE // If hostile and sees a valid target, gives a 'warning' to the target before beginning the attack. - var/threatened = FALSE // If the mob actually gave the warning, checked so it doesn't constantly yell every tick. + var/threatening = FALSE // If the mob actually gave the warning, checked so it doesn't constantly yell every tick. var/threaten_delay = 3 SECONDS // How long a 'threat' lasts, until actual fighting starts. If null, the mob never starts the fight but still does the threat. var/threaten_timeout = 1 MINUTE // If the mob threatens someone, they leave, and then come back before this timeout period, the mob escalates to fighting immediately. var/last_conflict_time = null // Last occurance of threatening or fighting being used, in world.time. @@ -8,13 +10,21 @@ var/threaten_sound = null // Sound file played when the mob calls threaten_target() for the first time. var/stand_down_sound = null // Sound file played when the mob loses sight of the threatened target. + var/speak_chance = 0 // Probability that the mob talks (this is 'X in 200' chance since even 1/100 is pretty noisy) + var/reacts = 0 // Reacts to some things being said. + var/datum/say_list/say_list = null // Datum containing all of our lines. + var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent. + +/datum/ai_holder/New() + ..() + say_list = new say_list_type() /datum/ai_holder/proc/should_threaten() if(!threaten) return FALSE // We don't negotiate. if(!will_threaten(target)) return FALSE // Pointless to threaten an animal, a mindless drone, or an object. - if(!(stance in list(STANCE_IDLE, STANCE_MOVE, STANCE_FOLLOW))) + if(stance in STANCES_COMBAT) return FALSE // We're probably already fighting or recently fought if not in these stances. if(last_conflict_time && threaten_delay && last_conflict_time + threaten_timeout > world.time) return FALSE // We threatened someone recently, so lets show them we mean business. @@ -23,27 +33,27 @@ /datum/ai_holder/proc/threaten_target() holder.face_atom(target) // Constantly face the target. - if(!threatened) // First tick. - threatened = TRUE + if(!threatening) // First tick. + threatening = TRUE last_conflict_time = world.time - //TODO: Actual speech. - holder.say("Oi, fuck off mate.") + + holder.say(safepick(say_list.say_threaten)) playsound(holder.loc, threaten_sound, 75, 1) // We do this twice to make the sound -very- noticable to the target. playsound(target.loc, threaten_sound, 75, 1) // Actual aim-mode also does that so at least it's consistant. else // Otherwise we are waiting for them to go away or to wait long enough for escalate. if(target in list_targets()) // Are they still visible? if(threaten_delay && last_conflict_time + threaten_delay < world.time) // Waited too long. - threatened = FALSE - set_stance(STANCE_ATTACK) - holder.say("Fine, now you die!") //WIP + threatening = FALSE + set_stance(STANCE_APPROACH) + holder.say(safepick(say_list.say_escalate)) else return // Wait a bit. else // They left, or so we think. - threatened = FALSE + threatening = FALSE set_stance(STANCE_IDLE) - holder.say("Good riddence.") //WIP + holder.say(safepick(say_list.say_stand_down)) playsound(holder.loc, stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target. playsound(target.loc, stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant. @@ -56,4 +66,35 @@ var/mob/living/simple_animal/them = target if(them.intelligence_level < us.intelligence_level) // Todo: Bitflag these. return FALSE // Humanoids don't care about drones/animals/etc. Drones don't care about animals, and so on. - return TRUE \ No newline at end of file + return TRUE + +// Temp defines to make the below code a bit more readable. +#define COMM_SAY "say" +#define COMM_AUDIBLE_EMOTE "audible emote" +#define COMM_VISUAL_EMOTE "visual emote" + +/datum/ai_holder/proc/handle_idle_speaking() + if(rand(0,200) < speak_chance) + var/list/comm_types = list() // What kinds of things can we do? + + if(say_list.speak.len) + comm_types += COMM_SAY + if(say_list.emote_hear.len) + comm_types += COMM_AUDIBLE_EMOTE + if(say_list.emote_see.len) + comm_types += COMM_VISUAL_EMOTE + + if(!comm_types.len) + return // All the relevant lists are empty, so do nothing. + + switch(pick(comm_types)) + if(COMM_SAY) + holder.say(safepick(say_list.speak)) + if(COMM_AUDIBLE_EMOTE) + holder.audible_emote(safepick(say_list.emote_hear)) + if(COMM_VISUAL_EMOTE) + holder.visible_emote(safepick(say_list.emote_see)) + +#undef COMM_SAY +#undef COMM_AUDIBLE_EMOTE +#undef COMM_VISUAL_EMOTE diff --git a/code/modules/ai/ai_holder_cooperation.dm b/code/modules/ai/ai_holder_cooperation.dm new file mode 100644 index 0000000000..638e6112d7 --- /dev/null +++ b/code/modules/ai/ai_holder_cooperation.dm @@ -0,0 +1,110 @@ +// Involves cooperating with other ai_holders. +/datum/ai_holder + var/cooperative = FALSE // If true, asks allies to help when fighting something. + var/call_distance = 14 // How far away calls for help will go for. + var/last_helpask_time = 0 // world.time when a mob asked for help. + var/list/faction_friends = list() // List of all mobs inside the faction with ai_holders that have cooperate on, to call for help without using range(). + // Note that this is only used for sending calls out. Receiving calls doesn't care about this list, only if the mob is in the faction. + // This means the AI could respond to a player's call for help, if a way to do so was implemented. + + // These vars don't do anything currently. They did before but an optimization made them nonfunctional. + // It was probably worth it. + var/call_players = FALSE // (Currently nonfunctional) If true, players get notified of an allied mob calling for help. + var/called_player_message = "needs help!" // (Currently nonfunctional) Part of a message used when above var is true. Full message is "\The [holder] [called_player_message]" + +/datum/ai_holder/New(new_holder) + ..() + if(cooperative) + build_faction_friends() + +/datum/ai_holder/Destroy() + if(faction_friends.len) //This list is shared amongst the faction + faction_friends -= src + return ..() + +// Handles everything about that list. +// Call on initialization or if something weird happened like the mob switched factions. +/datum/ai_holder/proc/build_faction_friends() + if(faction_friends.len) // Already have a list. + // Assume we're moving to a new faction. + faction_friends -= src // Get us out of the current list shared by everyone else. + faction_friends = list() // Then make our list empty and unshared in case we become a loner. + + // Find another AI-controlled mob in the same faction if possible. + var/mob/living/first_friend + for(var/mob/living/L in living_mob_list) + if(L.faction == holder.faction && L.ai_holder) + first_friend = L + break + + if(first_friend) // Joining an already established faction. + faction_friends = first_friend.ai_holder.faction_friends + faction_friends |= holder + else // We're the 'founder' (first and/or only member) of this faction. + faction_friends |= holder + +// Requests help in combat from other mobs possessing ai_holders. +/datum/ai_holder/proc/request_help() + ai_log("request_help() : Entering.", AI_LOG_DEBUG) + if(!cooperative || ((world.time - last_helpask_time) < 10 SECONDS)) + return + + ai_log("request_help() : Asking for help.", AI_LOG_INFO) + last_helpask_time = world.time + +// for(var/mob/living/L in range(call_distance, holder)) + for(var/mob/living/L in faction_friends) + if(L == holder) // Lets not call ourselves. + continue + if(get_dist(L, holder) > call_distance) // Too far to 'hear' the call for help. + continue + + if(holder.IIsAlly(L)) + // This will currently never run sadly, until faction_friends is made to accept players too. + // That might be for the best since I can imagine it getting spammy in a big fight. + if(L.client && call_players) // Dealing with a player. + ai_log("request_help() : Asking [L] (Player) for help.", AI_LOG_INFO) + to_chat(L, "\The [holder] [called_player_message]") + + else if(L.ai_holder) // Dealing with an AI. + ai_log("request_help() : Asking [L] (AI) for help.", AI_LOG_INFO) + L.ai_holder.help_requested(holder) + + ai_log("request_help() : Exiting.", AI_LOG_DEBUG) + +// What allies receive when someone else is calling for help. +/datum/ai_holder/proc/help_requested(mob/living/friend) + ai_log("help_requested() : Entering.", AI_LOG_DEBUG) + if(stance == STANCE_SLEEP) + ai_log("help_requested() : Help requested by [friend] but we are asleep.", AI_LOG_INFO) + return + if(!cooperative) + ai_log("help_requested() : Help requested by [friend] but we're not cooperative.", AI_LOG_INFO) + return + if(stance in STANCES_COMBAT) + ai_log("help_requested() : Help requested by [friend] but we are busy fighting something else.", AI_LOG_INFO) + return + if(!can_act()) + ai_log("help_requested() : Help requested by [friend] but cannot act (stunned or dead).", AI_LOG_INFO) + return + if(!holder.IIsAlly(friend)) // Extra sanity. + ai_log("help_requested() : Help requested by [friend] but we hate them.", AI_LOG_INFO) + return + if(get_dist(holder, friend) <= follow_distance) + ai_log("help_requested() : Help requested by [friend] but we're already here.", AI_LOG_INFO) + return + if(get_dist(holder, friend) <= vision_range) // Within our sight. + ai_log("help_requested() : Help requested by [friend], and within target sharing range.", AI_LOG_INFO) + if(friend.ai_holder) // AI calling for help. + if(friend.ai_holder.target && can_attack(friend.ai_holder.target)) // Friend wants us to attack their target. + last_conflict_time = world.time // So we attack immediately and not threaten. + give_target(friend.ai_holder.target) // This will set us to the appropiate stance. + ai_log("help_requested() : Given target [target] by [friend]. Exiting", AI_LOG_DEBUG) + return + + // Otherwise they're outside our sight, lack a target, or aren't AI controlled, but within call range. + // So assuming we're AI controlled, we'll go to them and see whats wrong. + ai_log("help_requested() : Help requested by [friend], going to go to friend.", AI_LOG_INFO) + set_follow(friend, 10 SECONDS) + ai_log("help_requested() : Exiting.", AI_LOG_DEBUG) + diff --git a/code/modules/ai/ai_holder_debug.dm b/code/modules/ai/ai_holder_debug.dm index ab62886d42..538d74e895 100644 --- a/code/modules/ai/ai_holder_debug.dm +++ b/code/modules/ai/ai_holder_debug.dm @@ -1,38 +1,126 @@ -/datum/ai_holder - var/path_display = FALSE // Displays a visual path when A* is being used. - var/path_icon = 'icons/misc/debug_group.dmi' // What icon to use for the overlay - var/path_icon_state = "red" // What state to use for the overlay - var/image/path_overlay // A reference to the overlay +// Contains settings to make it easier to debug things. +/datum/ai_holder + var/path_display = FALSE // Displays a visual path when A* is being used. + var/path_icon = 'icons/misc/debug_group.dmi' // What icon to use for the overlay + var/path_icon_state = "red" // What state to use for the overlay + var/image/path_overlay // A reference to the overlay + + var/last_turf_display = FALSE // Similar to above, but shows the target's last known turf visually. + var/last_turf_icon_state = "green" // A seperate icon_state from the previous. + var/image/last_turf_overlay // Another reference for an overlay. + + var/stance_coloring = FALSE // Colors the mob depending on its stance. + + var/debug_ai = AI_LOG_OFF // The level of debugging information to display to people who can see log_debug(). + +/* +#define AI_LOG_OFF 0 // Don't show anything. +#define AI_LOG_ERROR 1 // Show logs of things likely causing the mob to not be functioning correctly. +#define AI_LOG_WARNING 2 // Show less serious but still helpful to know issues that might be causing things to work incorrectly. +#define AI_LOG_INFO 3 // Important regular events, like selecting a target or switching stances. +#define AI_LOG_DEBUG 4 // More detailed information about the flow of execution. +#define AI_LOG_TRACE 5 // Even more detailed than the last. +*/ /datum/ai_holder/New() ..() path_overlay = new(path_icon,path_icon_state) + last_turf_overlay = new(path_icon, last_turf_icon_state) /datum/ai_holder/Destroy() path_overlay = null + last_turf_overlay = null return ..() +//For debug purposes! +/datum/ai_holder/proc/ai_log_output(var/msg = "missing message", var/ver = AI_LOG_INFO) + var/span_type + switch(ver) + if(AI_LOG_OFF) + return + if(AI_LOG_ERROR) + span_type = "debug_error" + if(AI_LOG_WARNING) + span_type = "debug_warning" + if(AI_LOG_INFO) + span_type = "debug_info" + if(AI_LOG_DEBUG) + span_type = "debug_debug" // RAS syndrome at work. + if(AI_LOG_TRACE) + span_type = "debug_trace" + if(ver <= debug_ai) + log_debug("AI: ([holder]:\ref[holder] | [holder.x],[holder.y],[holder.z])(@[world.time]): [msg] ") +// Colors the mob based on stance, to visually tell what stance it is for debugging. +// Probably not something you want for regular use. +/datum/ai_holder/proc/stance_color() + var/new_color = null + switch(stance) + if(STANCE_SLEEP) + new_color = "#FFFFFF" // White + if(STANCE_IDLE) + new_color = "#00FF00" // Green + if(STANCE_ALERT) + new_color = "#FFFF00" // Yellow + if(STANCE_APPROACH) + new_color = "#FF9933" // Orange + if(STANCE_FIGHT) + new_color = "#FF0000" // Red + if(STANCE_MOVE) + new_color = "#0000FF" // Blue + if(STANCE_REPOSITION) + new_color = "#FF00FF" // Purple + if(STANCE_FOLLOW) + new_color = "#00FFFF" // Cyan + if(STANCE_FLEE) + new_color = "#666666" // Grey + if(STANCE_STUNNED) + new_color = "#000000" // Black + holder.color = new_color + +// Turns on all the debugging stuff. +/datum/ai_holder/proc/debug() + stance_coloring = TRUE + path_display = TRUE + last_turf_display = TRUE + debug_ai = AI_LOG_INFO // Remove this when finished. /mob/living/simple_animal/corgi ai_holder_type = /datum/ai_holder/test +/datum/ai_holder/test + hostile = TRUE + use_astar = TRUE + /datum/ai_holder/hostile/ranged ranged = TRUE cooperative = TRUE firing_lanes = TRUE conserve_ammo = TRUE threaten = TRUE + wander = TRUE threaten_sound = 'sound/weapons/TargetOn.ogg' stand_down_sound = 'sound/weapons/TargetOff.ogg' +/datum/ai_holder/hostile/ranged/debug + wander = FALSE + conserve_ammo = FALSE + intelligence_level = AI_SMART + + stance_coloring = TRUE + path_display = TRUE + last_turf_display = TRUE + debug_ai = AI_LOG_INFO + + say_list_type = /datum/say_list/pirate + /mob/living/simple_animal/hostile/pirate hostile = FALSE ai_inactive = TRUE - ai_holder_type = /datum/ai_holder/hostile/ranged + ai_holder_type = /datum/ai_holder/hostile/ranged/debug /datum/ai_holder/hostile/ranged/robust/on_engagement(atom/movable/AM) step_rand(holder) diff --git a/code/modules/ai/ai_holder_fleeing.dm b/code/modules/ai/ai_holder_fleeing.dm new file mode 100644 index 0000000000..bde630ef57 --- /dev/null +++ b/code/modules/ai/ai_holder_fleeing.dm @@ -0,0 +1,38 @@ +// This code handles what to do inside STANCE_FLEE. + +/datum/ai_holder + var/can_flee = TRUE // If they're even allowed to flee. + var/flee_when_dying = TRUE // If they should flee when low on health. + var/dying_threshold = 0.3 // How low on health the holder needs to be before fleeing. Defaults to 30% or lower health. + var/flee_when_outmatched = FALSE // If they should flee upon reaching a specific tension threshold. + var/outmatched_threshold = 200 // The tension threshold needed for a mob to decide it should run away. + + + +/datum/ai_holder/proc/should_flee(force = FALSE) + if(holder.has_modifier_of_type(/datum/modifier/berserk)) // Berserked mobs will never flee, even if 'forced' to. + return FALSE + if(force) + return TRUE + + if(can_flee) + if(!hostile) + return TRUE // We're not hostile and someone attacked us first. + if(flee_when_dying && (holder.health / holder.getMaxHealth()) <= dying_threshold) + return TRUE // We're gonna die! + else if(flee_when_outmatched && holder.get_tension() >= outmatched_threshold) + return TRUE // We're fighting something way way stronger then us. + return FALSE + +/datum/ai_holder/proc/flee_from_target() + ai_log("flee_from_target() : Entering.", AI_LOG_DEBUG) + + if(!target || !can_attack(target)) // can_attack() is used since it checks the same things we would need to anyways. + ai_log("flee_from_target() : Lost target to flee from.", AI_LOG_INFO) + lose_target() + ai_log("flee_from_target() : Exiting.", AI_LOG_DEBUG) + return + + ai_log("flee_from_target() : Stepping away.", AI_LOG_TRACE) + step_away(holder, target, vision_range) + ai_log("flee_from_target() : Exiting.", AI_LOG_DEBUG) \ No newline at end of file diff --git a/code/modules/ai/ai_holder_follow.dm b/code/modules/ai/ai_holder_follow.dm new file mode 100644 index 0000000000..a9f1433c6b --- /dev/null +++ b/code/modules/ai/ai_holder_follow.dm @@ -0,0 +1,60 @@ +// This handles following a specific atom/movable, without violently murdering it. + +/datum/ai_holder + // Following. + var/atom/movable/leader = null // The movable atom that the mob wants to follow. + var/follow_distance = 2 // How far leader must be to start moving towards them. + var/follow_until_time = 0 // world.time when the mob will stop following leader. 0 means it won't time out. + +/datum/ai_holder/proc/walk_to_leader() + ai_log("walk_to_leader() : Entering.",AI_LOG_DEBUG) + if(!leader) + ai_log("walk_to_leader() : No leader.", AI_LOG_WARNING) + forget_path() + set_stance(STANCE_IDLE) + ai_log("walk_to_leader() : Exiting.", AI_LOG_DEBUG) + return + + // Did we time out? + if(follow_until_time && world.time > follow_until_time) + ai_log("walk_to_leader() : Follow timed out, losing leader.", AI_LOG_INFO) + lose_follow() + set_stance(STANCE_IDLE) + ai_log("walk_to_leader() : Exiting.", AI_LOG_DEBUG) + return + + var/get_to = follow_distance + var/distance = get_dist(holder, leader) + ai_log("walk_to_leader() : get_to is [get_to].", AI_LOG_TRACE) + + // We're here! + if(distance <= get_to) + give_up_movement() + set_stance(STANCE_IDLE) + ai_log("walk_to_leader() : Within range, exiting.", AI_LOG_INFO) + return + + ai_log("walk_to_leader() : Walking.", AI_LOG_TRACE) + walk_path(leader, get_to) + ai_log("walk_to_leader() : Exiting.",AI_LOG_DEBUG) + +/datum/ai_holder/proc/set_follow(mob/living/L, follow_for = 0) + ai_log("set_follow() : Entered.", AI_LOG_DEBUG) + if(!L) + ai_log("set_follow() : Was told to follow a nonexistant mob.", AI_LOG_ERROR) + return FALSE + + leader = L + follow_until_time = !follow_for ? 0 : world.time + follow_for + ai_log("set_follow() : Exited.", AI_LOG_DEBUG) + return TRUE + +/datum/ai_holder/proc/lose_follow() + ai_log("lose_follow() : Entered.", AI_LOG_DEBUG) + ai_log("lose_follow() : Going to lose leader [leader].", AI_LOG_INFO) + leader = null + give_up_movement() + ai_log("lose_follow() : Exited.", AI_LOG_DEBUG) + +/datum/ai_holder/proc/should_follow_leader() + return leader && get_dist(holder, leader) > follow_distance \ No newline at end of file diff --git a/code/modules/ai/ai_holder_movement.dm b/code/modules/ai/ai_holder_movement.dm index 322f5b0f95..3a15fa5dc1 100644 --- a/code/modules/ai/ai_holder_movement.dm +++ b/code/modules/ai/ai_holder_movement.dm @@ -1,147 +1,108 @@ /datum/ai_holder - var/use_astar = FALSE // Do we use the more expensive A* implementation or stick with BYOND's default step_to()? - var/using_astar = FALSE // Are we currently using an A* path? - var/list/path = list() // A list of tiles that A* gave us as a solution to reach the target. - var/list/obstacles = list() // Things A* will try to avoid. - var/astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccess // Proc to use when A* pathfinding. Default makes them bound to cardinals. - var/failed_steps = 0 // If move_once() fails to move the mob onto the correct tile, this increases. When it reaches 3, the path is recalc'd since they're probably stuck. + // General. + var/turf/destination = null // The targeted tile the mob wants to walk to. + var/min_distance_to_destination = 1 // Holds how close the mob should go to destination until they're done. + // Home. var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so. - var/return_home = FALSE // If true, makes the mob go to its 'home' if it strays too far. + var/returns_home = FALSE // If true, makes the mob go to its 'home' if it strays too far. + var/max_home_distance = 3 // How far the mob can go away from its home before being told to go_home(). -/************** -* Strategical * -**************/ + // Wandering. + var/wander = FALSE // If true, the mob will randomly move in the four cardinal directions when idle. + var/wander_delay = 0 // How many ticks until the mob can move a tile in handle_wander_movement(). + var/base_wander_delay = 2 // What the above var gets set to when it wanders. Note that a tick happens every half a second. + var/wander_when_pulled = FALSE // If the mob will refrain from wandering if someone is pulling it. -//Giving up on moving -/datum/ai_holder/proc/give_up_movement() -// ai_log("GiveUpMoving()",1) - forget_path() -// stop_automated_movement = 0 -//Forget the path entirely -/datum/ai_holder/proc/forget_path() -// ai_log("ForgetPath()",2) - if(path_display) - for(var/turf/T in path) - T.overlays -= path_overlay - using_astar = FALSE -// walk_list.Cut() - path.Cut() -/datum/ai_holder/proc/calculate_path(atom/A, get_to = 1) - if(!A) +/datum/ai_holder/proc/walk_to_destination() + ai_log("walk_to_destination() : Entering.",AI_LOG_DEBUG) + if(!destination) + ai_log("walk_to_destination() : No destination.", AI_LOG_WARNING) + forget_path() + set_stance(stance == STANCE_REPOSITION ? STANCE_APPROACH : STANCE_IDLE) + ai_log("walk_to_destination() : Exiting.", AI_LOG_DEBUG) return - if(!use_astar) // If we don't use A* then this is pointless. - return - - get_path(get_turf(A), get_to) - -//A* now, try to a path to a target -/datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6) -// ai_log("GetPath([target],[get_to],[max_distance])",2) - forget_path() - var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles) - - if(new_path && new_path.len) - path = new_path - if(path_display) - for(var/turf/T in path) - T.overlays |= path_overlay - else - return 0 - - return path.len - -/* -/datum/ai_holder/proc/walk_to_target() - //If we were chasing someone and we can't anymore, give up. - if(!target_mob) - // ai_log("MoveToTarget() Losing target at top",2) - lose_target() - return - - //We recompute our path every time we're called if we can still see them - if(target in list_targets(vision_range)) - - if(using_astar) - forget_path() - - // Find out where we're going. - var/get_to = 1 // TODO - var/distance = get_dist(holder, target) - - //We're here! - if(distance <= get_to) - // ai_log("MoveToTarget() [src] attack range",2) - set_stance(STANCE_ATTACKING) - return - - //We're just setting out, making a new path, or we can't path with A* - if(!path.len) - // ai_log("SA: MoveToTarget() pathing to [target_mob]",2) - - //GetPath failed for whatever reason, just smash into things towards them - if(run_at_them || !GetPath(get_turf(target_mob),get_to)) - - //We try the built-in way to stay close - walk_to(src, target_mob, get_to, move_to_delay) - // ai_log("MoveToTarget() walk_to([src],[target_mob],[get_to],[move_to_delay])",3) - - //Break shit in their direction! LEME SMAHSH - var/dir_to_mob = get_dir(src,target_mob) - face_atom(target_mob) - // DestroySurroundings(dir_to_mob) - // ai_log("MoveToTarget() DestroySurroundings([get_dir(src,target_mob)])",3) -*/ - -/*********** -* Tactical * -***********/ - -// Goes to the target, to attack them. -// Called when in STANCE_ATTACK. -/datum/ai_holder/proc/walk_to_target() - // Make sure we can still chase/attack them. - if(!target || !can_attack(target)) - lose_target() - return - - // Find out where we're going. - var/get_to = closest_distance() - var/distance = get_dist(holder, target) + var/get_to = min_distance_to_destination + var/distance = get_dist(holder, destination) + ai_log("walk_to_destination() : get_to is [get_to].", AI_LOG_TRACE) // We're here! if(distance <= get_to) - // ai_log("MoveToTarget() [src] attack range",2) - forget_path() - set_stance(STANCE_ATTACKING) + give_up_movement() + set_stance(stance == STANCE_REPOSITION ? STANCE_APPROACH : STANCE_IDLE) + ai_log("walk_to_destination() : Destination reached. Exiting.", AI_LOG_INFO) return - // Otherwise keep walking. - walk_path(target, get_to) + ai_log("walk_to_destination() : Walking.", AI_LOG_TRACE) + walk_path(destination, get_to) + ai_log("walk_to_destination() : Exiting.",AI_LOG_DEBUG) + +/datum/ai_holder/proc/should_go_home() + return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance) + +/datum/ai_holder/proc/go_home() + if(home_turf) + ai_log("go_home() : Telling holder to go home.", AI_LOG_INFO) + lose_follow() // So they don't try to path back and forth. + give_destination(home_turf, max_home_distance) + else + ai_log("go_home() : Told to go home without home_turf.", AI_LOG_ERROR) + +/datum/ai_holder/proc/give_destination(turf/new_destination, min_distance = 1, combat = FALSE) + ai_log("give_destination() : Entering.", AI_LOG_DEBUG) + + destination = new_destination + min_distance_to_destination = min_distance + + if(new_destination != null) + ai_log("give_destination() : Going to new destination.", AI_LOG_TRACE) + set_stance(combat ? STANCE_REPOSITION : STANCE_MOVE) + return TRUE + else + ai_log("give_destination() : Given null destination.", AI_LOG_ERROR) + + ai_log("give_destination() : Exiting.", AI_LOG_DEBUG) + // Walk towards whatever. /datum/ai_holder/proc/walk_path(atom/A, get_to = 1) + ai_log("walk_path() : Entered.", AI_LOG_DEBUG) + var/turf/pre_step_turf = get_turf(holder) + if(use_astar) if(!path.len) // If we're missing a path, make a new one. + ai_log("walk_path() : No path. Attempting to calculate path.", AI_LOG_TRACE) calculate_path(A, get_to) - if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us. + if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us. Get as close as we can. + ai_log("walk_path() : Failed to obtain path to target. Using step_to() instead.", AI_LOG_INFO) + step_to(holder, A) + if(get_turf(holder) == pre_step_turf) + breakthrough(A) // We failed to move, time to smash things. return if(!move_once()) // Start walking the path. + ai_log("walk_path() : Failed to step.", AI_LOG_TRACE) ++failed_steps if(failed_steps > 3) // We're probably stuck. + ai_log("walk_path() : Too many failed_steps.", AI_LOG_INFO) forget_path() // So lets try again with a new path. failed_steps = 0 else step_to(holder, A) + if(get_turf(holder) == pre_step_turf) + breakthrough(A) // We failed to move, time to smash things. + + ai_log("walk_path() : Exited.", AI_LOG_DEBUG) + //Take one step along a path /datum/ai_holder/proc/move_once() + ai_log("move_once() : Entered.", AI_LOG_DEBUG) if(!path.len) return @@ -151,9 +112,29 @@ step_towards(holder, src.path[1]) if(holder.loc != src.path[1]) -// ai_log("MoveOnce() step_towards returning 0",3) + ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE) return FALSE else path -= src.path[1] -// ai_log("MoveOnce() step_towards returning 1",3) - return TRUE \ No newline at end of file + ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE) + return TRUE + +/datum/ai_holder/proc/should_wander() + return wander && !leader + +// Wanders randomly in cardinal directions. +/datum/ai_holder/proc/handle_wander_movement() + ai_log("handle_wander_movement() : Entered.", AI_LOG_DEBUG) + if(isturf(holder.loc) && can_act()) + wander_delay-- + if(wander_delay <= 0) + if(!wander_when_pulled && holder.pulledby) + ai_log("handle_wander_movement() : Being pulled and cannot wander. Exiting.", AI_LOG_DEBUG) + return + + var/moving_to = 0 // Apparently this is required or it always picks 4, according to the previous developer for simplemob AI. + moving_to = pick(cardinal) + holder.set_dir(moving_to) + holder.Move(get_step(holder,moving_to)) + wander_delay = base_wander_delay + ai_log("handle_wander_movement() : Exited.", AI_LOG_DEBUG) diff --git a/code/modules/ai/ai_holder_pathfinding.dm b/code/modules/ai/ai_holder_pathfinding.dm new file mode 100644 index 0000000000..1a20c6f682 --- /dev/null +++ b/code/modules/ai/ai_holder_pathfinding.dm @@ -0,0 +1,58 @@ +// This handles obtaining a (usually A*) path towards something, such as a target, destination, or leader. +// This interacts heavily with code inside ai_holder_movement.dm + +/datum/ai_holder + // Pathfinding. + var/use_astar = FALSE // Do we use the more expensive A* implementation or stick with BYOND's default step_to()? + var/list/path = list() // A list of tiles that A* gave us as a solution to reach the target. + var/list/obstacles = list() // Things A* will try to avoid. + var/astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccess // Proc to use when A* pathfinding. Default makes them bound to cardinals. + var/failed_steps = 0 // If move_once() fails to move the mob onto the correct tile, this increases. When it reaches 3, the path is recalc'd since they're probably stuck. + +// This clears the stored A* path. +/datum/ai_holder/proc/forget_path() + ai_log("forget_path() : Entering.", AI_LOG_DEBUG) + if(path_display) + for(var/turf/T in path) + T.overlays -= path_overlay + path.Cut() + ai_log("forget_path() : Exiting.", AI_LOG_DEBUG) + +/datum/ai_holder/proc/give_up_movement() + ai_log("give_up_movement() : Entering.", AI_LOG_DEBUG) + forget_path() + destination = null + ai_log("give_up_movement() : Exiting.", AI_LOG_DEBUG) + +/datum/ai_holder/proc/calculate_path(atom/A, get_to = 1) + ai_log("calculate_path([A],[get_to]) : Entering.", AI_LOG_DEBUG) + if(!A) + ai_log("calculate_path() : Called without an atom. Exiting.",AI_LOG_WARNING) + return + + if(!use_astar) // If we don't use A* then this is pointless. + ai_log("calculate_path() : Not using A*, Exiting.", AI_LOG_DEBUG) + return + + get_path(get_turf(A), get_to) + + ai_log("calculate_path() : Exiting.", AI_LOG_DEBUG) + +//A* now, try to a path to a target +/datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6) + ai_log("get_path() : Entering.",AI_LOG_DEBUG) + forget_path() + var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles) + + if(new_path && new_path.len) + path = new_path + ai_log("get_path() : Made new path.",AI_LOG_DEBUG) + if(path_display) + for(var/turf/T in path) + T.overlays |= path_overlay + else + ai_log("get_path() : Failed to make new path. Exiting.",AI_LOG_DEBUG) + return 0 + + ai_log("get_path() : Exiting.", AI_LOG_DEBUG) + return path.len \ No newline at end of file diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm new file mode 100644 index 0000000000..5de7b2890c --- /dev/null +++ b/code/modules/ai/ai_holder_targeting.dm @@ -0,0 +1,159 @@ +// Used for assigning a target for attacking. + +/datum/ai_holder + var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing. + var/retaliate = FALSE // Attacks whatever struck it first. + + var/atom/movable/target = null // The thing (mob or object) we're trying to kill. + var/turf/target_last_seen_turf = null // Where the mob last observed the target being, used if the target disappears but the mob wants to keep fighting. + + var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting. + + var/lose_target_time = 0 // world.time when a target was lost. + var/lose_target_timeout = 5 SECONDS // How long until a mob 'times out' and stops trying to find the mob that disappeared. + +// A lot of this is based off of /TG/'s AI code. + +// Step 1, find out what we can see. +/datum/ai_holder/proc/list_targets() + . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions. + + var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) + + for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines)) + if(can_see(holder, HM, vision_range)) + . += HM + +// Step 2, filter down possible targets to things we actually care about. +/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE) + . = list() + if(!has_targets_list) + possible_targets = list_targets() + for(var/possible_target in possible_targets) + var/atom/A = possible_target + if(found(A)) // In case people want to override this. + . = list(A) + break + if(can_attack(A)) // Can we attack it? + . += A + continue + + var/new_target = pick_target(.) + give_target(new_target) + return new_target + +// Step 3, pick among the possible, attackable targets. +/datum/ai_holder/proc/pick_target(list/targets) + if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets. + for(var/possible_target in targets) + var/atom/A = possible_target + var/target_dist = get_dist(holder, target) + var/possible_target_distance = get_dist(holder, A) + if(target_dist < possible_target_distance) + targets -= A + if(!targets.len) // We found nothing. + return + var/chosen_target = pick(targets) + return chosen_target + +// Step 4, give us our selected target. +/datum/ai_holder/proc/give_target(new_target) + target = new_target + if(target != null) + if(should_threaten()) + set_stance(STANCE_ALERT) + else + set_stance(STANCE_APPROACH) + return TRUE + +/datum/ai_holder/proc/can_attack(atom/movable/the_target) + if(!can_see_target(the_target)) + return FALSE + + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat) + return FALSE + if(holder.IIsAlly(L)) + return FALSE + return TRUE + + if(istype(the_target, /obj/mecha)) + var/obj/mecha/M = the_target + if(M.occupant) + return can_attack(M.occupant) + + if(istype(the_target, /obj/machinery/porta_turret)) + var/obj/machinery/porta_turret/P = the_target + if(P.stat & BROKEN) + return FALSE // Already dead. + if(P.faction == holder.faction) + return FALSE // Don't shoot allied turrets. + if(!P.raised && !P.raising) + return FALSE // Turrets won't get hurt if they're still in their cover. + return TRUE + + return FALSE + +// Override this for special targeting criteria. +// If it returns true, the mob will always select it as the target. +/datum/ai_holder/proc/found(atom/movable/the_target) + return FALSE + +//We can't see the target, go look or attack where they were last seen. +/datum/ai_holder/proc/lose_target() + if(target) + target = null + lose_target_time = world.time + + give_up_movement() + + +//Target is no longer valid (?) +/datum/ai_holder/proc/lost_target() + set_stance(STANCE_IDLE) + lose_target_position() + lose_target() + +// Check if target is visible to us. +/datum/ai_holder/proc/can_see_target(atom/movable/the_target, view_range = vision_range) + ai_log("can_see_target() : Entering.", AI_LOG_TRACE) + + if(!the_target) // Nothing to target. + ai_log("can_see_target() : There is no target. Exiting.", AI_LOG_WARNING) + return FALSE + + if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well. + ai_log("can_see_target() : Target ([the_target]) was invisible to holder. Exiting.", AI_LOG_TRACE) + return FALSE + + if(get_dist(holder, the_target) > view_range) // Too far away. + ai_log("can_see_target() : Target ([the_target]) was too far from holder. Exiting.", AI_LOG_TRACE) + return FALSE + + if(!can_see(holder, the_target, view_range)) + ai_log("can_see_target() : Target ([the_target]) failed can_see(). Exiting.", AI_LOG_TRACE) + return FALSE + + ai_log("can_see_target() : Target ([the_target]) can be seen. Exiting.", AI_LOG_TRACE) + return TRUE + +// Updates the last known position of the target. +/datum/ai_holder/proc/track_target_position() + if(!target) + lose_target_position() + + if(last_turf_display && target_last_seen_turf) + target_last_seen_turf.overlays -= last_turf_overlay + + target_last_seen_turf = get_turf(target) + + if(last_turf_display) + target_last_seen_turf.overlays += last_turf_overlay + +// Resets the last known position to null. +/datum/ai_holder/proc/lose_target_position() + if(last_turf_display && target_last_seen_turf) + target_last_seen_turf.overlays -= last_turf_overlay + ai_log("lose_target_position() : Last position is being reset.", AI_LOG_INFO) + target_last_seen_turf = null \ No newline at end of file diff --git a/code/modules/ai/interfaces.dm b/code/modules/ai/interfaces.dm index 06b1b6f825..b38b1ce7a3 100644 --- a/code/modules/ai/interfaces.dm +++ b/code/modules/ai/interfaces.dm @@ -3,17 +3,23 @@ // since some actions work very differently between mob types (e.g. executing an attack as a simple animal compared to a human). // The AI can just call holder.IAttack(target) and the mob is responsible for determining how to actually attack the target. -/mob/living/proc/IAttack(atom/movable/AM) +/mob/living/proc/IAttack(atom/A) + return FALSE -/mob/living/simple_animal/IAttack(atom/movable/AM) - target_mob = AM - return PunchTarget(AM) // TODO: Clean up on SA side. +/mob/living/simple_animal/IAttack(atom/A) + return attack_target(A) -/mob/living/proc/IRangedAttack(atom/movable/AM) +/mob/living/proc/IRangedAttack(atom/A) + return FALSE -/mob/living/simple_animal/IRangedAttack(atom/movable/AM) - target_mob = AM - return ShootTarget(AM, src.loc, src) +/mob/living/simple_animal/IRangedAttack(atom/A) + return shoot_target(A) + +/mob/living/proc/ISpecialAttack(atom/A) + return FALSE + +/mob/living/simple_animal/ISpecialAttack(atom/A) + return special_attack_target() /mob/living/proc/ISay(message) diff --git a/code/modules/ai/say_list.dm b/code/modules/ai/say_list.dm new file mode 100644 index 0000000000..29cd476eee --- /dev/null +++ b/code/modules/ai/say_list.dm @@ -0,0 +1,34 @@ +// A simple datum that just holds many lists of lines for AI mobs to pick from. +// This is its own datum in order to be able to have different types of mobs be able to use the same lines if desired, +// even when inheritence wouldn't be able to do so. + +// Also note this also contains emotes, despite its name. + +/datum/say_list + var/list/speak = list() // Things the mob might say if it talks while idle. + var/list/emote_hear = list() // Hearable emotes it might perform + var/list/emote_see = list() // Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps + var/list/say_understood = list() // When accepting an order. + var/list/say_cannot = list() // When they cannot comply. + var/list/say_maybe_target = list() // When they briefly see something. + var/list/say_got_target = list() // When a target is first assigned. + var/list/say_threaten = list() // When threatening someone. + var/list/say_stand_down = list() // When the threatened thing goes away. + var/list/say_escalate = list() // When the threatened thing doesn't go away. + + + + + +// Subtypes. + +// This one's pretty dumb, but pirates are dumb anyways and it makes for a good test. +/datum/say_list/pirate + speak = list("Yarr!") + say_understood = list("Alright, matey.") + say_cannot = list("No, matey.") + say_maybe_target = list("Eh?") + say_got_target = list("Yarrrr!") + say_threaten = list("You best leave, this booty is mine.", "No plank to walk on, just walk away.") + say_stand_down = list("Good.") + say_escalate = list("Yarr! The booty is mine!") \ No newline at end of file diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm index e4c5a10d35..3eb2cf03f4 100644 --- a/code/modules/client/client defines.dm +++ b/code/modules/client/client defines.dm @@ -17,7 +17,7 @@ //OTHER// ///////// var/datum/preferences/prefs = null - var/move_delay = 1 + //var/move_delay = 1 var/moving = null var/adminobs = null var/area = null diff --git a/code/modules/mob/_modifiers/modifiers_misc.dm b/code/modules/mob/_modifiers/modifiers_misc.dm index 0a02f6b096..cb8d24f333 100644 --- a/code/modules/mob/_modifiers/modifiers_misc.dm +++ b/code/modules/mob/_modifiers/modifiers_misc.dm @@ -29,7 +29,7 @@ Berserk is a somewhat rare modifier to obtain freely (and for good reason), howe - Red Slimes will berserk if they go rabid. - Red slime core reactions will berserk slimes that can see the user in addition to making them go rabid. - Red slime core reactions will berserk prometheans that can see the user. -- Bears will berserk when losing a fight. +- Saviks will berserk when losing a fight. - Changelings can evolve a 2 point ability to use a changeling-specific variant of Berserk, that replaces the text with a 'we' variant. Recursive Enhancement allows the changeling to instead used an improved variant that features less exhaustion time and less nutrition drain. - Xenoarch artifacts may have forced berserking as one of their effects. This is especially fun if an artifact that makes hostile mobs is nearby. diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm index 4de1dcf595..7c4d1a2612 100644 --- a/code/modules/mob/emote.dm +++ b/code/modules/mob/emote.dm @@ -46,6 +46,13 @@ if(O) O.see_emote(src, message, m_type) +// Shortcuts for above proc +/mob/proc/visible_emote(var/act_desc) + custom_emote(1, act_desc) + +/mob/proc/audible_emote(var/act_desc) + custom_emote(2, act_desc) + /mob/proc/emote_dead(var/message) if(client.prefs.muted & MUTE_DEADCHAT) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index b36cd29d21..2be87863ac 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -21,6 +21,7 @@ var/list/atom/hallucinations = list() //A list of hallucinated people that try to attack the mob. See /obj/effect/fake_attacker in hallucinations.dm var/last_special = 0 //Used by the resist verb, likely used to prevent players from bypassing next_move by logging in/out. + var/base_attack_cooldown = DEFAULT_ATTACK_COOLDOWN var/t_phoron = null var/t_oxygen = null diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 173c2c8387..2d5116edf9 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -555,12 +555,6 @@ if(act) ..(act, type, desc) -/mob/living/simple_animal/proc/visible_emote(var/act_desc) - custom_emote(1, act_desc) - -/mob/living/simple_animal/proc/audible_emote(var/act_desc) - custom_emote(2, act_desc) - /mob/living/simple_animal/bullet_act(var/obj/item/projectile/Proj) ai_log("bullet_act() I was shot by: [Proj.firer]",2) @@ -1239,8 +1233,11 @@ /mob/living/simple_animal/proc/PunchTarget() if(!Adjacent(target_mob)) return - if(!client) - sleep(rand(melee_attack_minDelay, melee_attack_maxDelay)) + if(!canClick()) + return + setClickCooldown(get_attack_speed()) +// if(!client) +// sleep(rand(melee_attack_minDelay, melee_attack_maxDelay)) if(isliving(target_mob)) var/mob/living/L = target_mob @@ -1282,13 +1279,18 @@ //The actual top-level ranged attack proc /mob/living/simple_animal/proc/ShootTarget() + if(!canClick()) + return FALSE + + setClickCooldown(get_attack_speed()) + var/target = target_mob var/tturf = get_turf(target) if((firing_lines && !client) && !CheckFiringLine(tturf)) step_rand(src) face_atom(tturf) - return 0 + return FALSE visible_message("[src] fires at [target]!") if(rapid) @@ -1309,7 +1311,7 @@ if(casingtype) new casingtype - return 1 + return TRUE //Check firing lines for faction_friends (if we're not cooperative, we don't care) /mob/living/simple_animal/proc/CheckFiringLine(var/turf/tturf) diff --git a/code/modules/mob/living/simple_animal/simple_animal2.dm b/code/modules/mob/living/simple_animal/simple_animal2.dm new file mode 100644 index 0000000000..04522eb101 --- /dev/null +++ b/code/modules/mob/living/simple_animal/simple_animal2.dm @@ -0,0 +1,74 @@ +// Reorganized and somewhat cleaned up. +// AI code has been made into a datum, inside the AI module folder. + +// Does a melee attack. +/mob/living/simple_animal/proc/attack_target(atom/A) + set waitfor = FALSE // For attack animations, if they're ever added. Don't want the AI processor to get held up. + + if(!A.Adjacent(src)) + return FALSE + if(!canClick()) // Still on cooldown from a "click". + return FALSE + setClickCooldown(get_attack_speed()) + + return do_attack(A) + +// This does the actual attack. +/mob/living/simple_animal/proc/do_attack(atom/A) + if(!A.Adjacent(src)) // They could've moved in the meantime. + return FALSE + + var/damage_to_do = rand(melee_damage_lower, melee_damage_upper) + + for(var/datum/modifier/M in modifiers) + if(!isnull(M.outgoing_melee_damage_percent)) + damage_to_do *= M.outgoing_melee_damage_percent + + if(isliving(A)) // Check defenses. + var/mob/living/L = A + + if(prob(melee_miss_chance)) // This is stupid (The logging part). + src.attack_log += text("\[[time_stamp()]\] attacked [L.name] ([L.ckey])") + L.attack_log += text("\[[time_stamp()]\] was attacked by [src.name] ([src.ckey])") + visible_message("[src] misses [L]!") + do_attack_animation(src) + return FALSE // We missed. + + if(ishuman(L)) + var/mob/living/carbon/human/H = L + if(H.check_shields(damage = damage_to_do, damage_source = src, attacker = src, def_zone = null, attack_text = "the attack")) + return FALSE // We were blocked. + + if(A.attack_generic(src, damage_to_do, pick(attacktext)) && attack_sound) + playsound(src, attack_sound, 75, 1) + + return TRUE + +//The actual top-level ranged attack proc +/mob/living/simple_animal/proc/shoot_target(atom/A) + if(!canClick()) + return FALSE + + setClickCooldown(get_attack_speed()) + + visible_message("\The [src] fires at \the [A]!") + shoot(A, src.loc, src) + if(casingtype) + new casingtype + + return TRUE + +//Shoot a bullet at someone (idk why user is an argument when src would fit???) +/mob/living/simple_animal/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0) + if(A == start) + return + + var/obj/item/projectile/P = new projectiletype(user.loc) + playsound(user, projectilesound, 100, 1) + if(!P) + return + P.launch(A) + +//Special attacks, like grenades or blinding spit or whatever +/mob/living/simple_animal/proc/special_attack_target(atom/A) + return FALSE \ No newline at end of file diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 917790df41..95e1980f67 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -730,13 +730,12 @@ /mob/proc/facedir(var/ndir) - if(!canface() || (client && (client.moving || (world.time < client.move_delay)))) + if(!canface() || (client && (client.moving || (world.time < move_delay)))) return 0 set_dir(ndir) if(buckled && buckled.buckle_movable) buckled.set_dir(ndir) - if(client) - client.move_delay += movement_delay() + move_delay += movement_delay() return 1 diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index cc446a0488..c9dd5b2162 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -6,6 +6,8 @@ var/datum/mind/mind var/stat = 0 //Whether a mob is alive or dead. TODO: Move this to living - Nodrak + var/move_delay = null // For movement speed delays. + var/next_move = null // For click delay, despite the misleading name. //Not in use yet var/obj/effect/organstructure/organStructure = null @@ -61,7 +63,6 @@ var/sdisabilities = 0 //Carbon var/disabilities = 0 //Carbon var/atom/movable/pulling = null - var/next_move = null var/transforming = null //Carbon var/other = 0.0 var/eye_blind = null //Carbon diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index b59bd402d3..2841003152 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -11,8 +11,7 @@ return /mob/proc/setMoveCooldown(var/timeout) - if(client) - client.move_delay = max(world.time + timeout, client.move_delay) + move_delay = max(world.time + timeout, move_delay) /client/North() ..() @@ -199,7 +198,7 @@ if(moving) return 0 - if(world.time < move_delay) return + if(world.time < mob.move_delay) return if(locate(/obj/effect/stop/, mob.loc)) for(var/obj/effect/stop/S in mob.loc) @@ -271,27 +270,27 @@ src << "You're pinned to a wall by [mob.pinned[1]]!" return 0 - move_delay = world.time//set move delay + mob.move_delay = world.time//set move delay switch(mob.m_intent) if("run") if(mob.drowsyness > 0) - move_delay += 6 - move_delay += config.run_speed + mob.move_delay += 6 + mob.move_delay += config.run_speed if("walk") - move_delay += config.walk_speed - move_delay += mob.movement_delay() + mob.move_delay += config.walk_speed + mob.move_delay += mob.movement_delay() var/tickcomp = 0 //moved this out here so we can use it for vehicles if(config.Tickcomp) // move_delay -= 1.3 //~added to the tickcomp calculation below tickcomp = ((1/(world.tick_lag))*1.3) - 1.3 - move_delay = move_delay + tickcomp + mob.move_delay = mob.move_delay + tickcomp if(istype(mob.buckled, /obj/vehicle)) //manually set move_delay for vehicles so we don't inherit any mob movement penalties //specific vehicle move delays are set in code\modules\vehicles\vehicle.dm - move_delay = world.time + tickcomp + mob.move_delay = world.time + tickcomp //drunk driving if(mob.confused && prob(20)) //vehicles tend to keep moving in the same direction direct = turn(direct, pick(90, -90)) @@ -320,14 +319,14 @@ if(prob(50)) direct = turn(direct, pick(90, -90)) if("walk") if(prob(25)) direct = turn(direct, pick(90, -90)) - move_delay += 2 + mob.move_delay += 2 return mob.buckled.relaymove(mob,direct) //We are now going to move moving = 1 //Something with pulling things if(locate(/obj/item/weapon/grab, mob)) - move_delay = max(move_delay, world.time + 7) + mob.move_delay = max(mob.move_delay, world.time + 7) var/list/L = mob.ret_grab() if(istype(L, /list)) if(L.len == 2) diff --git a/code/stylesheet.dm b/code/stylesheet.dm index 650d659d31..f7a200bf9e 100644 --- a/code/stylesheet.dm +++ b/code/stylesheet.dm @@ -103,4 +103,11 @@ h1.alert, h2.alert {color: #000000;} BIG IMG.icon {width: 32px; height: 32px;} +/* Debug Logs */ +.debug_error {color:#FF0000; font-weight:bold} +.debug_warning {color:#FF0000;} +.debug_info {} +.debug_debug {color:#0000FF;} +.debug_trace {color:#888888;} + "} diff --git a/polaris.dme b/polaris.dme index fdc6854414..325c93333f 100644 --- a/polaris.dme +++ b/polaris.dme @@ -1233,13 +1233,20 @@ #include "code\modules\admin\view_variables\helpers.dm" #include "code\modules\admin\view_variables\topic.dm" #include "code\modules\admin\view_variables\view_variables.dm" +#include "code\modules\ai\__readme.dm" #include "code\modules\ai\_defines.dm" #include "code\modules\ai\ai_holder.dm" #include "code\modules\ai\ai_holder_combat.dm" #include "code\modules\ai\ai_holder_communication.dm" +#include "code\modules\ai\ai_holder_cooperation.dm" #include "code\modules\ai\ai_holder_debug.dm" +#include "code\modules\ai\ai_holder_fleeing.dm" +#include "code\modules\ai\ai_holder_follow.dm" #include "code\modules\ai\ai_holder_movement.dm" +#include "code\modules\ai\ai_holder_pathfinding.dm" +#include "code\modules\ai\ai_holder_targeting.dm" #include "code\modules\ai\interfaces.dm" +#include "code\modules\ai\say_list.dm" #include "code\modules\alarm\alarm.dm" #include "code\modules\alarm\alarm_handler.dm" #include "code\modules\alarm\atmosphere_alarm.dm" @@ -1857,6 +1864,7 @@ #include "code\modules\mob\living\silicon\robot\subtypes\syndicate.dm" #include "code\modules\mob\living\simple_animal\corpse.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" +#include "code\modules\mob\living\simple_animal\simple_animal2.dm" #include "code\modules\mob\living\simple_animal\simple_hud.dm" #include "code\modules\mob\living\simple_animal\aliens\alien.dm" #include "code\modules\mob\living\simple_animal\aliens\creature.dm" @@ -2415,7 +2423,7 @@ #include "code\ZAS\Zone.dm" #include "interface\interface.dm" #include "interface\skin.dmf" -#include "maps\example\example.dm" +#include "maps\plane\plane.dm" #include "maps\submaps\_readme.dm" #include "maps\submaps\space_submaps\space.dm" #include "maps\submaps\surface_submaps\mountains\mountains.dm"