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"