Merges AI Branch into Master

This commit is contained in:
Atermonera
2018-12-01 00:06:38 -05:00
committed by VirgoBot
parent 15017e893e
commit 2df5e9fe0e
379 changed files with 22460 additions and 4751 deletions
+201
View File
@@ -0,0 +1,201 @@
/*
[Summary]
This module contains an AI implementation designed to be (at the base level) mobtype-agnostic,
by being held inside a datum instead of being written into the mob directly. More specialized
subtypes of the base AI may be designed with a specific mob type in mind, but the base system
should be compatible with most types of mobs which have the needed Interfaces in place to
support them.
When designing a new mob, all that is needed to give a mob an AI is to set
its 'ai_holder_type' variable to the path of the AI that is desired.
[Seperation]
In previous iterations of AI systems, the AI is generally written into the mob's code directly,
which has some advantages, but often makes the code rigid, and also tied the speed of the AI
to the mob's own ticker, meaning it could only decide every two seconds.
Instead, this version has the code for the AI held inside an /datum/ai_holder object,
which is carried by the mob it controls. This gives some advantages;
All /mob/living mobs can potentially have an AI applied to them, and utilize the
same base code while adding specialized code on top.
Interfaces allow the base AI code to not need to know what particular mode it's controlling.
The processing of the AI is independant of the mob's Life() cycle, which allows for a
different clock rate.
Seperating the AI from the mob simplies the mob's code greatly.
It is more logical to think that a mob is the 'body', where as its ai_holder is
the 'mind'.
AIs can be applied or disabled on the fly by instantiating or deleting the
ai_holder, if needed.
The current implementation also has some disadvantages, but they can perhaps be resolved
in the future.
AI-driven mob movement and attack speed is tied to half-second delays due to the
AI subsystem ticking at that rate. Porting the timer subsystem and integrating
callbacks into basic AI actions (moving, attacking) can potentially resolve that.
It can be difficult to modify AI variables at mob instantiation without an ugly
delay, as the ai_holder might not exist yet.
[Flow of Processing]
Terrible visual representation here;
AI Subsystem -> Every 0.5s -> /datum/ai_holder/handle_tactics() -> switch(stance)...
-> Every 2.0s -> /datum/ai_holder/handle_strategicals() -> switch(stance)...
The AI datum is not processed by the mob itself, but instead it is directly processed
by a new AI subsystem. The AI subsystem contains a list of all active ai_holder
objects, which is iterated every tick to process each individual ai_holder
object attached to a mob.
Each ai_holder actually has two 'tracks' for processing, a 'fast' track
and a 'slow' track.
The fast track is named handle_tactics(), and is called every 0.5 seconds.
The slow track is named handle_strategicals(), and is called every 2 seconds.
When an ai_holder is iterated on inside the AI subsystem's list, it first
calls that ai_holder's handle_tactics(). It will then call that ai_holder's
handle_strategicals() every fourth tick, effectively doing so every two seconds.
Both functions do different things depending on which 'stance' the
ai_holder is in. See the Stances section for more information.
The fast track is 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.
The slow track, 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 (if the mob uses A*), 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, but it is less
noticable due to the mob appearing to do things inbetween those two seconds.
The purpose of having two tracks is to allow 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 always
called first, before handle_strategicals() every two seconds.
[Process Skipping]
An ai_holder object can choose to enter a 'busy' state, or a 'sleep' state,
in order to avoid processing.
When busy, the AI subsystem will skip over the ai_holder until it is no
longer busy. The busy state is intended to be short-term, and is usually
toggled by the mob when doing something with a delay, so that the ai_holder
does not accidentally do something to inturrupt something important, like
a special attack.
The longer term alternative to the busy state is the sleep state. Unlike
being busy, an ai_holder set to sleep will remove itself from the
AI subsystem's list, meaning it will no longer process until something
else 'wakes' it. This is usually done when the mob dies or a client
logs into an AI-controlled mob (and the AI is not set to ignore that,
with the autopilot variable). If the mob is revived, the AI will be
awakened automatically.
The ai_holder functions, and mob functions that are called by the
ai_holder, should not be sleep()ed, as it will block the AI Subsystem
from processing the other ai_holders until the sleep() finishes.
Delays on the mob typically have set waitfor = FALSE, or spawn() is used.
[Stances]
The AI has a large number of states that it can be in, called stances.
The AI will act in a specific way depending on which stance it is in,
and only one stance can be active at a time. This effectively creates
a state pattern.
To change the stance, set_stance() is used, with the new stance as
the first argument. It should be noted that the change is not immediate,
and it will react to the change next tick instead of immediately switching
to the new stance and acting on that in the same tick. This is done to help
avoid infinite loops (I.E. Stance A switches to Stance B, which then
switches to Stance A, and so on...), and the delay is very short so
it should not be an issue.
See code/__defines/mob.dm for a list of stance defines, and descriptions
about their purpose. Generally, each stance has its own file in the AI
module folder and are mostly self contained, however some files instead
deal with general things that other stances may require, such as targeting
or movement.
[Interfaces]
Interfaces are a concept that is used to help bridge the gap between
the ai_holder, and its mob. Because the (base) ai_holder is explicitly
designed to not be specific to any type of mob, all that it knows is
that it is controlling a /mob/living mob. Some mobs work very differently,
between mob types such as /mob/living/simple_mob, /mob/living/silicon/robot,
/mob/living/carbon/human, and more.
The solution to the vast differences between mob types is to have the
mob itself deal with how to handle a specific task, such as attacking
something, talking, moving, etc. Interfaces exist to do this.
Interfaces are applied on the mob-side, and are generally specific to
that mob type. This lets the ai_holder not have to worry about specific
implementations and instead just tell the Interface that it wants to attack
something, or move into a tile. The AI does not need to know if the mob its
controlling has hands, instead that is the mob's responsibility.
Interface functions have an uppercase I at the start of the function name,
and then the function they are bridging between the AI and the mob
(if it exists), e.g. IMove(), IAttack(), ISay().
Interfaces are also used for the AI to ask its mob if it can do certain
things, without having to actually know what type of mob it is attached to.
For example, ICheckRangedAttack() tells the AI if it is possible to do a
ranged attack. For simple_mobs, they can if a ranged projectile type was set,
where as for a human mob, it could check if a gun is in a hand. For a borg,
it could check if a gun is inside their current module.
[Say List]
A /datum/say_list is a very light datum that holds a list of strings for the
AI to have their mob say based on certain conditions, such as when threatening
to kill another mob. Despite the name, a say_list also can contain emotes
and some sounds.
The reason that it is in a seperate datum is to allow for multiple mob types
to have the same text, even when inheritence cannot do that, such as
mercenaries and fake piloted mecha mobs.
The say_list datum is applied to the mob itself and not held inside the AI datum.
[Subtypes]
Some subtypes of ai_holder are more specialized, but remain compatible with
most mob types. There are many different subtypes that make the AI act different
by overriding a function, such as kiting their target, moving up close while
using ranged attacks, or running away if not cloaked.
Other subtypes are very specific about what kind of mob it controls, and trying
to apply them to a different type of mob will likely result in a lot of bugs
or ASSERT() failures. The xenobio slime AI is an example of the latter.
To use a specific subtype on a mob, all that is needed is setting the mob's
ai_holder_type to the subtype desired, and it will create that subtype when
the mob is initialize()d. Switching to a subtype 'live' will require additional
effort on the coder.
*/
+29
View File
@@ -0,0 +1,29 @@
// 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 be a little smarter, like not walking while confused if it could risk stepping randomly onto a bad tile.
#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. Will absolutely flood your chatlog.
// Results of pre-movement checks.
// Todo: Move outside AI code?
#define MOVEMENT_ON_COOLDOWN -1 // Recently moved and needs to try again soon.
#define MOVEMENT_FAILED 0 // Move() returned false for whatever reason and the mob didn't move.
#define MOVEMENT_SUCCESSFUL 1 // Move() returned true and the mob hopefully moved.
// 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.
@@ -0,0 +1,136 @@
// Base AIs for simple mobs.
// Mob-specific AIs are in their mob's file.
/datum/ai_holder/simple_mob
hostile = TRUE // The majority of simplemobs are hostile.
cooperative = TRUE
returns_home = FALSE
can_flee = FALSE
speak_chance = 1 // If the mob's saylist is empty, nothing will happen.
wander = TRUE
base_wander_delay = 4
// For non-hostile animals, and pets like Ian and Runtime.
/datum/ai_holder/simple_mob/passive
hostile = FALSE
can_flee = TRUE
violent_breakthrough = FALSE
// Won't wander away, ideal for event-spawned mobs like carp or drones.
/datum/ai_holder/simple_mob/event
wander = FALSE
// Doesn't really act until told to by something on the outside.
/datum/ai_holder/simple_mob/inert
hostile = FALSE
retaliate = FALSE
can_flee = FALSE
wander = FALSE
speak_chance = 0
cooperative = FALSE
violent_breakthrough = FALSE // So it can open doors but not attack windows and shatter the literal illusion.
// Used for technomancer illusions, to resemble player movement better.
/datum/ai_holder/simple_mob/inert/astar
use_astar = TRUE
// Ranged mobs.
/datum/ai_holder/simple_mob/ranged
// ranged = TRUE
// Tries to not waste ammo.
/datum/ai_holder/simple_mob/ranged/careful
conserve_ammo = TRUE
/datum/ai_holder/simple_mob/ranged/pointblank
pointblank = TRUE
// Runs away from its target if within a certain distance.
/datum/ai_holder/simple_mob/ranged/kiting
pointblank = TRUE // So we don't need to copypaste post_melee_attack().
var/run_if_this_close = 4 // If anything gets within this range, it'll try to move away.
var/moonwalk = TRUE // If true, mob turns to face the target while kiting, otherwise they turn in the direction they moved towards.
/datum/ai_holder/simple_mob/ranged/kiting/threatening
threaten = TRUE
threaten_delay = 1 SECOND // Less of a threat and more of pre-attack notice.
threaten_timeout = 30 SECONDS
conserve_ammo = TRUE
// For event-spawned malf drones.
/datum/ai_holder/simple_mob/ranged/kiting/threatening/event
wander = FALSE
/datum/ai_holder/simple_mob/ranged/kiting/no_moonwalk
moonwalk = FALSE
/datum/ai_holder/simple_mob/ranged/kiting/on_engagement(atom/A)
if(get_dist(holder, A) < run_if_this_close)
holder.IMove(get_step_away(holder, A, run_if_this_close))
if(moonwalk)
holder.face_atom(A)
// Closes distance from the target even while in range.
/datum/ai_holder/simple_mob/ranged/aggressive
pointblank = TRUE
var/closest_distance = 1 // How close to get to the target. By default they will get into melee range (and then pointblank them).
/datum/ai_holder/simple_mob/ranged/aggressive/on_engagement(atom/A)
if(get_dist(holder, A) > closest_distance)
holder.IMove(get_step_towards(holder, A))
holder.face_atom(A)
// Yakkity saxes while firing at you.
/datum/ai_holder/hostile/ranged/robust/on_engagement(atom/movable/AM)
step_rand(holder)
holder.face_atom(AM)
// Switches intents based on specific criteria.
// Used for special mobs who do different things based on intents (and aren't slimes).
// Intent switching is generally done in pre_[ranged/special]_attack(), so that the mob can use the right attack for the right time.
/datum/ai_holder/simple_mob/intentional
// These try to avoid collateral damage.
/datum/ai_holder/simple_mob/restrained
violent_breakthrough = FALSE
conserve_ammo = TRUE
// Melee mobs.
/datum/ai_holder/simple_mob/melee
// Dances around the enemy its fighting, making it harder to fight back.
/datum/ai_holder/simple_mob/melee/evasive
/datum/ai_holder/simple_mob/melee/evasive/post_melee_attack(atom/A)
if(holder.Adjacent(A))
holder.IMove(get_step(holder, pick(alldirs)))
holder.face_atom(A)
// This AI hits something, then runs away for awhile.
// It will (almost) always flee if they are uncloaked, AND their target is not stunned.
/datum/ai_holder/simple_mob/melee/hit_and_run
can_flee = TRUE
// Used for the 'running' part of hit and run.
/datum/ai_holder/simple_mob/melee/hit_and_run/special_flee_check()
if(!holder.is_cloaked())
if(isliving(target))
var/mob/living/L = target
return !L.incapacitated(INCAPACITATION_DISABLED) // Don't flee if our target is stunned in some form, even if uncloaked. This is so the mob keeps attacking a stunned opponent.
return TRUE // We're out in the open, uncloaked, and our target isn't stunned, so lets flee.
return FALSE
// Simple mobs that aren't hostile, but will fight back.
/datum/ai_holder/simple_mob/retaliate
hostile = FALSE
retaliate = TRUE
// Simple mobs that retaliate and support others in their faction who get attacked.
/datum/ai_holder/simple_mob/retaliate/cooperative
cooperative = TRUE
@@ -0,0 +1,262 @@
// Specialized AI for slime simplemobs.
// Unlike the parent AI code, this will probably break a lot of things if you put it on something that isn't /mob/living/simple_mob/slime/xenobio
/datum/ai_holder/simple_mob/xenobio_slime
hostile = TRUE
cooperative = TRUE
firing_lanes = TRUE
var/rabid = FALSE // Will attack regardless of discipline.
var/discipline = 0 // Beating slimes makes them less likely to lash out. In theory.
var/resentment = 0 // 'Unjustified' beatings make this go up, and makes it more likely for abused slimes to go rabid.
var/obedience = 0 // Conversely, 'justified' beatings make this go up, and makes discipline decay slower, potentially making it not decay at all.
var/always_stun = FALSE // If true, the slime will elect to attempt to permastun the target.
/datum/ai_holder/simple_mob/xenobio_slime/sapphire
always_stun = TRUE // They know that stuns are godly.
intelligence_level = AI_SMART // Also knows not to walk while confused if it risks death.
/datum/ai_holder/simple_mob/xenobio_slime/light_pink
discipline = 5
obedience = 5
/datum/ai_holder/simple_mob/xenobio_slime/passive/New() // For Kendrick.
..()
pacify()
/datum/ai_holder/simple_mob/xenobio_slime/New()
..()
ASSERT(istype(holder, /mob/living/simple_mob/slime/xenobio))
// Checks if disciplining the slime would be 'justified' right now.
/datum/ai_holder/simple_mob/xenobio_slime/proc/is_justified_to_discipline()
if(rabid)
return TRUE
if(target)
if(ishuman(target))
var/mob/living/carbon/human/H = target
if(istype(H.species, /datum/species/monkey))
return FALSE // Attacking monkeys is okay.
return TRUE // Otherwise attacking other things is bad.
return FALSE // Not attacking anything.
/datum/ai_holder/simple_mob/xenobio_slime/proc/can_command(mob/living/commander)
if(rabid)
return FALSE
if(!hostile)
return SLIME_COMMAND_OBEY
// if(commander in friends)
// return SLIME_COMMAND_FRIEND
if(holder.IIsAlly(commander))
return SLIME_COMMAND_FACTION
if(discipline > resentment && obedience >= 5)
return SLIME_COMMAND_OBEY
return FALSE
/datum/ai_holder/simple_mob/xenobio_slime/proc/adjust_discipline(amount, silent)
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
if(amount > 0)
if(rabid)
return
var/justified = is_justified_to_discipline()
lost_target() // Stop attacking.
if(justified)
obedience++
if(!silent)
holder.say(pick("Fine...", "Okay...", "Sorry...", "I yield...", "Mercy..."))
else
if(prob(resentment * 20))
enrage()
holder.say(pick("Evil...", "Kill...", "Tyrant..."))
else
if(!silent)
holder.say(pick("Why...?", "I don't understand...?", "Cruel...", "Stop...", "Nooo..."))
resentment++ // Done after check so first time will never enrage.
discipline = between(0, discipline + amount, 10)
my_slime.update_mood()
// This slime always enrages if disciplined.
/datum/ai_holder/simple_mob/xenobio_slime/red/adjust_discipline(amount, silent)
if(amount > 0 && !rabid)
holder.say("Grrr...")
holder.add_modifier(/datum/modifier/berserk, 30 SECONDS)
enrage()
/datum/ai_holder/simple_mob/xenobio_slime/handle_special_strategical()
discipline_decay()
// Handles decay of discipline.
/datum/ai_holder/simple_mob/xenobio_slime/proc/discipline_decay()
if(discipline > 0)
if(!prob(75 + (obedience * 5)))
adjust_discipline(-1)
/datum/ai_holder/simple_mob/xenobio_slime/handle_special_tactic()
evolve_and_reproduce()
// Hit the correct verbs to keep the slime species going.
/datum/ai_holder/simple_mob/xenobio_slime/proc/evolve_and_reproduce()
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
if(my_slime.amount_grown >= 10)
// Press the correct verb when we can.
if(my_slime.is_adult)
my_slime.reproduce() // Splits into four new baby slimes.
else
my_slime.evolve() // Turns our holder into an adult slime.
// Called when pushed too far (or a red slime core was used).
/datum/ai_holder/simple_mob/xenobio_slime/proc/enrage()
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
if(my_slime.harmless)
return
rabid = TRUE
my_slime.update_mood()
my_slime.visible_message(span("danger", "\The [src] enrages!"))
// Called when using a pacification agent (or it's Kendrick being initalized).
/datum/ai_holder/simple_mob/xenobio_slime/proc/pacify()
lost_target() // So it stops trying to kill them.
rabid = FALSE
hostile = FALSE
retaliate = FALSE
cooperative = FALSE
// The holder's attack changes based on intent. This lets the AI choose what effect is desired.
/datum/ai_holder/simple_mob/xenobio_slime/pre_melee_attack(atom/A)
if(istype(A, /mob/living))
var/mob/living/L = A
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
if( (!L.lying && prob(30 + (my_slime.power_charge * 7) ) || (!L.lying && always_stun) ))
my_slime.a_intent = I_DISARM // Stun them first.
else if(my_slime.can_consume(L) && L.lying)
my_slime.a_intent = I_GRAB // Then eat them.
else
my_slime.a_intent = I_HURT // Otherwise robust them.
/datum/ai_holder/simple_mob/xenobio_slime/closest_distance(atom/movable/AM)
if(istype(AM, /mob/living))
var/mob/living/L = AM
if(ishuman(L))
var/mob/living/carbon/human/H = L
if(istype(H.species, /datum/species/monkey))
return 1 // Otherwise ranged slimes will eat a lot less often.
if(L.stat >= UNCONSCIOUS)
return 1 // Melee (eat) the target if dead/dying, don't shoot it.
return ..()
/datum/ai_holder/simple_mob/xenobio_slime/can_attack(atom/movable/AM)
. = ..()
if(.) // Do some additional checks because we have Special Code(tm).
if(ishuman(AM))
var/mob/living/carbon/human/H = AM
if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too.
return TRUE // Monkeys are always food (sorry Pun Pun).
else if(H.species && H.species.name == SPECIES_PROMETHEAN)
return FALSE // Prometheans are always our friends.
if(discipline && !rabid)
return FALSE // We're a good slime.
// Commands, reactions, etc
/datum/ai_holder/simple_mob/xenobio_slime/on_hear_say(mob/living/speaker, message)
ai_log("xenobio_slime/on_hear_say([speaker], [message]) : Entered.", AI_LOG_DEBUG)
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
if((findtext(message, num2text(my_slime.number)) || findtext(message, my_slime.name) || findtext(message, "slimes"))) // Talking to us.
// First, make sure it's actually a player saying something and not an AI, or else we risk infinite loops.
if(!speaker.client)
return
// Are all slimes being referred to?
// var/mass_order = FALSE
// if(findtext(message, "slimes"))
// mass_order = TRUE
// Say hello back.
if(findtext(message, "hello") || findtext(message, "hi") || findtext(message, "greetings"))
delayed_say(pick("Hello...", "Hi..."), speaker)
// Follow request.
if(findtext(message, "follow") || findtext(message, "come with me"))
if(!can_command(speaker))
delayed_say(pick("No...", "I won't follow..."), speaker)
return
delayed_say("Yes... I follow \the [speaker]...", speaker)
set_follow(speaker)
// Squish request.
if(findtext(message , "squish"))
if(!can_command(speaker))
delayed_say("No...", speaker)
return
spawn(rand(1 SECOND, 2 SECONDS))
if(!src || !holder || !can_act()) // We might've died/got deleted/etc in the meantime.
return
my_slime.squish()
// Stop request.
if(findtext(message, "stop") || findtext(message, "halt") || findtext(message, "cease"))
if(my_slime.victim) // We're being asked to stop eatting someone.
if(!can_command(speaker) || !is_justified_to_discipline())
delayed_say("No...", speaker)
return
else
delayed_say("Fine...", speaker)
adjust_discipline(1, TRUE)
my_slime.stop_consumption()
if(target) // We're being asked to stop chasing someone.
if(!can_command(speaker) || !is_justified_to_discipline())
delayed_say("No...", speaker)
return
else
delayed_say("Fine...", speaker)
adjust_discipline(1, TRUE) // This must come before losing the target or it will be unjustified.
lost_target()
if(leader) // We're being asked to stop following someone.
if(can_command(speaker) == SLIME_COMMAND_FRIEND || leader == speaker)
delayed_say("Yes... I'll stop...", speaker)
lose_follow()
else
delayed_say("No... I'll keep following \the [leader]...", speaker)
/* // Commented out since its mostly useless now due to slimes refusing to attack if it would make them naughty.
// Murder request
if(findtext(message, "harm") || findtext(message, "attack") || findtext(message, "kill") || findtext(message, "murder") || findtext(message, "eat") || findtext(message, "consume") || findtext(message, "absorb"))
if(can_command(speaker) < SLIME_COMMAND_FACTION)
delayed_say("No...", speaker)
return
for(var/mob/living/L in view(7, my_slime) - list(my_slime, speaker))
if(L == src)
continue // Don't target ourselves.
var/list/valid_names = splittext(L.name, " ") // Should output list("John", "Doe") as an example.
for(var/line in valid_names) // Check each part of someone's name.
if(findtext(message, lowertext(line))) // If part of someone's name is in the command, the slime targets them if allowed to.
if(!(mass_order && line == "slime")) //don't think random other slimes are target
if(can_attack(L))
delayed_say("Okay... I attack \the [L]...", speaker)
give_target(L)
return
else
delayed_say("No... I won't attack \the [L].", speaker)
return
// If we're here, it couldn't find anyone with that name.
delayed_say("No... I don't know who to attack...", speaker)
*/
ai_log("xenobio_slime/on_hear_say() : Exited.", AI_LOG_DEBUG)
/datum/ai_holder/simple_mob/xenobio_slime/can_violently_breakthrough()
if(discipline && !rabid) // Good slimes don't shatter the windows because their buddy in an adjacent cell decided to piss off Slimesky.
return FALSE
return ..()
+290
View File
@@ -0,0 +1,290 @@
// This is a datum-based artificial intelligence for simple mobs (and possibly others) to use.
// The neat thing with having this here instead of on the mob is that it is independant of Life(), and that different mobs
// can use a more or less complex AI by giving it a different datum.
/mob/living
var/datum/ai_holder/ai_holder = null
var/ai_holder_type = null // Which ai_holder datum to give to the mob when initialized. If null, nothing happens.
/mob/living/initialize()
if(ai_holder_type)
ai_holder = new ai_holder_type(src)
return ..()
/mob/living/Destroy()
QDEL_NULL(ai_holder)
return ..()
/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 // Adjust to make the AI be intentionally dumber, or make it more robust (e.g. dodging grenades).
var/autopilot = FALSE // If true, the AI won't be deactivated if a client gets attached to the AI's mob.
var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the
// mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time,
// consider sleeping the AI instead.
/datum/ai_holder/hostile
hostile = TRUE
/datum/ai_holder/retaliate
hostile = TRUE
retaliate = TRUE
/datum/ai_holder/New(var/new_holder)
ASSERT(new_holder)
holder = new_holder
SSai.processing += src
home_turf = get_turf(holder)
..()
/datum/ai_holder/Destroy()
holder = null
SSai.processing -= src // We might've already been asleep and removed, but byond won't care if we do this again and it saves a conditional.
home_turf = null
return ..()
// 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
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
if(!should_wake())
return
set_stance(STANCE_IDLE)
SSai.processing += src
/datum/ai_holder/proc/should_wake()
if(holder.client && !autopilot)
return FALSE
if(holder.stat >= DEAD)
return FALSE
return TRUE
// 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()
if(busy)
return
handle_special_tactic()
handle_stance_tactical()
// '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()
if(busy)
return
handle_special_strategical()
handle_stance_strategical()
// Override these for special things without polluting the main loop.
/datum/ai_holder/proc/handle_special_tactic()
/datum/ai_holder/proc/handle_special_strategical()
/*
//AI Actions
if(!ai_inactive)
//Stanceyness
handle_stance()
//Movement
if(!stop_automated_movement && wander && !anchored) //Allowed to move?
handle_wander_movement()
//Speaking
if(speak_chance && stance == STANCE_IDLE) // Allowed to chatter?
handle_idle_speaking()
//Resisting out buckles
if(stance != STANCE_IDLE && incapacitated(INCAPACITATION_BUCKLED_PARTIALLY))
handle_resist()
//Resisting out of closets
if(istype(loc,/obj/structure/closet))
var/obj/structure/closet/C = loc
if(C.welded)
resist()
else
C.open()
*/
// 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()
// 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(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(stance != STANCE_DISABLED && is_disabled()) // Stunned/confused/etc
ai_log("handle_stance_tactical() : Disabled.", AI_LOG_TRACE)
set_stance(STANCE_DISABLED)
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.sealed)
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_IDLE)
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(STANCE_ALERT)
ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE)
threaten_target()
if(STANCE_APPROACH)
ai_log("handle_stance_tactical() : STANCE_APPROACH, going to walk_to_target().", AI_LOG_TRACE)
walk_to_target()
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_DISABLED)
ai_log("handle_stance_tactical() : STANCE_DISABLED.", AI_LOG_TRACE)
if(!is_disabled())
ai_log("handle_stance_tactical() : No longer disabled.", AI_LOG_TRACE)
set_stance(STANCE_IDLE)
else
handle_disabled()
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_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)
// Helper proc to turn AI 'busy' mode on or off without having to check if there is an AI, to simplify writing code.
/mob/living/proc/set_AI_busy(value)
if(ai_holder)
ai_holder.busy = value
/mob/living/proc/is_AI_busy()
if(!ai_holder)
return FALSE
return ai_holder.busy
// Helper proc to check for the AI's stance.
// Returns null if there's no AI holder, or the mob has a player and autopilot is not on.
// Otherwise returns the stance.
/mob/living/proc/get_AI_stance()
if(!ai_holder)
return null
if(client && !ai_holder.autopilot)
return null
return ai_holder.stance
// Similar to above but only returns 1 or 0.
/mob/living/proc/has_AI()
return get_AI_stance() ? TRUE : FALSE
// 'Taunts' the AI into attacking the taunter.
/mob/living/proc/taunt(atom/movable/taunter, force_target_switch = FALSE)
if(ai_holder)
ai_holder.receive_taunt(taunter, force_target_switch)
+308
View File
@@ -0,0 +1,308 @@
// This file is for actual fighting. Targeting is in a seperate file.
/datum/ai_holder
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/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/can_breakthrough = TRUE // If false, the AI will not try to open a path to its goal, like opening doors.
var/violent_breakthrough = TRUE // If false, the AI is not allowed to destroy things like windows or other structures in the way. Requires above var to be true.
var/stand_ground = FALSE // If true, the AI won't try to get closer to an enemy if out of range.
// 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())) )
if(!target || !can_attack(target))
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
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))
if(holder.ICheckSpecialAttack(target))
ai_log("engage_target() : Attempting a special attack.", AI_LOG_TRACE)
on_engagement(target)
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
// 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(holder.ICheckRangedAttack(target) && (distance <= max_range(target)) )
on_engagement(target)
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
ai_log("engage_target() : Attempting a ranged attack.", AI_LOG_TRACE)
ranged_attack(target)
// Run after them.
else if(!stand_ground)
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/A)
pre_melee_attack(A)
. = holder.IAttack(A)
if(.)
post_melee_attack(A)
// Ditto.
/datum/ai_holder/proc/ranged_attack(atom/A)
pre_ranged_attack(A)
. = holder.IRangedAttack(A)
if(.)
post_ranged_attack(A)
// Most mobs probably won't have this defined but we don't care.
/datum/ai_holder/proc/special_attack(atom/movable/AM)
pre_special_attack(AM)
. = holder.ISpecialAttack(AM)
if(.)
post_special_attack(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/A)
// Called before a ranged attack is attempted.
/datum/ai_holder/proc/pre_ranged_attack(atom/A)
// Called before a melee attack is attempted.
/datum/ai_holder/proc/pre_melee_attack(atom/A)
// Called before a 'special' attack is attempted.
/datum/ai_holder/proc/pre_special_attack(atom/A)
// Called after a successful (IE not on cooldown) ranged attack.
// Note that this is not whether the projectile actually hit, just that one was launched.
/datum/ai_holder/proc/post_ranged_attack(atom/A)
// Ditto but for melee.
/datum/ai_holder/proc/post_melee_attack(atom/A)
// And one more for special snowflake attacks.
/datum/ai_holder/proc/post_special_attack(atom/A)
// 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.
// world << "Checked trajectory, would hit [L]."
if(istype(L)) // Did we hit a mob?
// world << "Hit [L]."
if(holder.IIsAlly(L))
// world << "Would hit ally, canceling."
return FALSE // We would hit a friend!
// world << "Won't threaten ally, firing."
return TRUE // Otherwise we don't care, even if its not the intended target.
else
if(!isliving(AM)) // If the original target was an object, then let it happen if it doesn't threaten an ally.
// world << "Targeting object, ignoring and firing."
return TRUE
// world << "Not sure."
return !conserve_ammo // If we have infinite ammo than shooting the wall isn't so bad, but otherwise lets not.
// 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(holder.ICheckRangedAttack(AM) && distance <= max_range(AM))
return TRUE // Can shoot.
return FALSE
// Determines how close the AI will move to its target.
/datum/ai_holder/proc/closest_distance(atom/movable/AM)
return max(max_range(AM) - 1, 1) // Max range -1 just because we don't want to constantly get kited
// Can be used to conditionally do a ranged or melee attack.
/datum/ai_holder/proc/max_range(atom/movable/AM)
return holder.ICheckRangedAttack(AM) ? 7 : 1
// 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(target)
var/distance = get_dist(holder, target)
ai_log("walk_to_target() : get_to is [get_to].", AI_LOG_TRACE)
// We're here!
// Special case: Our holder has a special attack that is ranged, but normally the holder uses melee.
// If that happens, we'll switch to STANCE_FIGHT so they can use it. If the special attack is limited, they'll likely switch back next tick.
if(distance <= get_to || holder.ICheckSpecialAttack(target))
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.
if(!stand_ground)
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.
// This does two passes, so that if its just a public access door, the windows nearby don't need to be smashed.
/datum/ai_holder/proc/breakthrough(atom/target_atom)
ai_log("breakthrough() : Entering", AI_LOG_TRACE)
if(!can_breakthrough)
ai_log("breakthrough() : Not allowed to breakthrough. Exiting.", AI_LOG_TRACE)
return FALSE
if(!isturf(holder.loc))
ai_log("breakthrough() : Trapped inside \the [holder.loc]. Exiting.", AI_LOG_TRACE)
return FALSE
var/dir_to_target = get_dir(holder, target_atom)
holder.face_atom(target_atom)
ai_log("breakthrough() : Exiting", AI_LOG_DEBUG)
// Sometimes the mob will try to hit something diagonally, and generally this fails.
// So instead we will try two more times with some adjustments if the attack fails.
var/list/directions_to_try = list(
dir_to_target,
turn(dir_to_target, 45),
turn(dir_to_target, -45)
)
ai_log("breakthrough() : Starting peaceful pass.", AI_LOG_DEBUG)
var/result = FALSE
// First, we will try to peacefully make a path, I.E opening a door we have access to.
for(var/direction in directions_to_try)
result = destroy_surroundings(direction, violent = FALSE)
if(result)
break
// Alright, lets smash some shit instead, if it didn't work and we're allowed to be violent.
if(!result && can_violently_breakthrough())
ai_log("breakthrough() : Starting violent pass.", AI_LOG_DEBUG)
for(var/direction in directions_to_try)
result = destroy_surroundings(direction, violent = TRUE)
if(result)
break
ai_log("breakthrough() : Exiting with [result].", AI_LOG_TRACE)
return result
// Despite the name, this can also be used to help clear a path without any destruction.
/datum/ai_holder/proc/destroy_surroundings(direction, violent = TRUE)
ai_log("destroy_surroundings() : Entering.", AI_LOG_TRACE)
if(!direction)
direction = pick(cardinal) // FLAIL WILDLY
ai_log("destroy_surroundings() : No direction given, picked [direction] randomly.", AI_LOG_DEBUG)
var/turf/problem_turf = get_step(holder, direction)
// First, give peace a chance.
if(!violent)
ai_log("destroy_surroundings() : Going to try to peacefully clear [problem_turf].", AI_LOG_DEBUG)
for(var/obj/machinery/door/D in problem_turf)
if(D.density && holder.Adjacent(D) && D.allowed(holder) && D.operable())
// First, try to open the door if possible without smashing it. We might have access.
ai_log("destroy_surroundings() : Opening closed door.", AI_LOG_INFO)
return D.open()
// Peace has failed us, can we just smash the things in the way?
else
ai_log("destroy_surroundings() : Going to try to violently clear [problem_turf].", AI_LOG_DEBUG)
// 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 side 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)
// Kill hull shields in the way.
for(var/obj/effect/energy_field/shield in problem_turf)
if(shield.density) // Don't attack shields that are already down.
ai_log("destroy_surroundings() : Attacking hull shield.", AI_LOG_INFO)
return holder.IAttack(shield)
// Kill common obstacle in the way like tables.
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)
ai_log("destroy_surroundings() : Exiting due to nothing to attack.", AI_LOG_INFO)
return FALSE // Nothing to attack.
// Override for special behaviour.
/datum/ai_holder/proc/can_violently_breakthrough()
return violent_breakthrough
@@ -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) > max_range(targeted_turf))
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)
+134
View File
@@ -0,0 +1,134 @@
// 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/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 fighting being used, in world.time.
var/last_threaten_time = null // Ditto but only for threats.
var/speak_chance = 0 // Probability that the mob talks (this is 'X in 200' chance since even 1/100 is pretty noisy)
/datum/ai_holder/proc/should_threaten()
if(!threaten)
return FALSE // We don't negotiate.
if(target in attackers)
return FALSE // They (or someone like them) attacked us before, escalate immediately.
if(!will_threaten(target))
return FALSE // Pointless to threaten an animal, a mindless drone, or an object.
if(stance in STANCES_COMBAT)
return FALSE // We're probably already fighting or recently fought if not in these stances.
if(last_threaten_time && threaten_delay && last_conflict_time + threaten_timeout > world.time)
return FALSE // We threatened someone recently, so lets show them we mean business.
return TRUE // Lets give them a chance to choose wisely and walk away.
/datum/ai_holder/proc/threaten_target()
holder.face_atom(target) // Constantly face the target.
if(!threatening) // First tick.
threatening = TRUE
last_threaten_time = world.time
if(holder.say_list)
holder.ISay(safepick(holder.say_list.say_threaten))
playsound(holder.loc, holder.say_list.threaten_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
playsound(target.loc, holder.say_list.threaten_sound, 50, 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?
var/should_escalate = FALSE
if(threaten_delay && last_threaten_time + threaten_delay < world.time) // Waited too long.
should_escalate = TRUE
else if(last_conflict_time + threaten_timeout > world.time) // We got attacked while threatening them.
should_escalate = TRUE
if(should_escalate)
threatening = FALSE
set_stance(STANCE_APPROACH)
if(holder.say_list)
holder.ISay(safepick(holder.say_list.say_escalate))
else
return // Wait a bit.
else // They left, or so we think.
threatening = FALSE
set_stance(STANCE_IDLE)
if(holder.say_list)
holder.ISay(safepick(holder.say_list.say_stand_down))
playsound(holder.loc, holder.say_list.stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
playsound(target.loc, holder.say_list.stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
// Determines what is deserving of a warning when STANCE_ALERT is active.
/datum/ai_holder/proc/will_threaten(mob/living/the_target)
if(!isliving(the_target))
return FALSE // Turrets don't give a fuck so neither will we.
/*
// Find a nice way of doing this later.
if(istype(the_target, /mob/living/simple_mob) && istype(holder, /mob/living/simple_mob))
var/mob/living/simple_mob/us = holder
var/mob/living/simple_mob/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
// 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)
// Check if anyone is around to 'appreciate' what we say.
var/alone = TRUE
for(var/m in viewers(holder))
var/mob/M = m
if(M.client)
alone = FALSE
break
if(alone) // Forever alone. No point doing anything else.
return
var/list/comm_types = list() // What kinds of things can we do?
if(!holder.say_list)
return
if(holder.say_list.speak.len)
comm_types += COMM_SAY
if(holder.say_list.emote_hear.len)
comm_types += COMM_AUDIBLE_EMOTE
if(holder.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.ISay(safepick(holder.say_list.speak))
if(COMM_AUDIBLE_EMOTE)
holder.audible_emote(safepick(holder.say_list.emote_hear))
if(COMM_VISUAL_EMOTE)
holder.visible_emote(safepick(holder.say_list.emote_see))
#undef COMM_SAY
#undef COMM_AUDIBLE_EMOTE
#undef COMM_VISUAL_EMOTE
// Handles the holder hearing a mob's say()
// Does nothing by default, override this proc for special behavior.
/datum/ai_holder/proc/on_hear_say(mob/living/speaker, message)
return
// This is to make responses feel a bit more natural and not instant.
/datum/ai_holder/proc/delayed_say(var/message, var/mob/speak_to)
spawn(rand(1 SECOND, 2 SECONDS))
if(!src || !holder || !can_act()) // We might've died/got deleted/etc in the meantime.
return
if(speak_to)
holder.face_atom(speak_to)
holder.ISay(message)
+115
View File
@@ -0,0 +1,115 @@
// 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(holder.z != L.z) // On seperate z-level.
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, "<span class='critical'>\The [holder] [called_player_message]</span>")
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(friend.ai_holder && friend.ai_holder.target && !can_attack(friend.ai_holder.target))
ai_log("help_requested() : Help requested by [friend] but we don't want to fight their target.", 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)
+89
View File
@@ -0,0 +1,89 @@
// 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().
/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("<span class='[span_type]'>AI: ([holder]:\ref[holder] | [holder.x],[holder.y],[holder.z])(@[world.time]): [msg] </span>")
// 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_DISABLED)
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
/datum/ai_holder/hostile/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
+95
View File
@@ -0,0 +1,95 @@
// Handles AI while stunned or otherwise disabled.
/datum/ai_holder
var/respect_confusion = TRUE // If false, the mob won't wander around recklessly.
// 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
// Test if we should switch to STANCE_DISABLE.
// Currently tests for death, stuns, and confusion.
/datum/ai_holder/proc/is_disabled()
if(!can_act())
return TRUE
if(is_confused())
return TRUE
return FALSE
/datum/ai_holder/proc/is_confused()
return holder.confused > 0 && respect_confusion
// Called by the main loop.
/datum/ai_holder/proc/handle_disabled()
if(!can_act())
return // Just sit there and take it.
else if(is_confused())
dangerous_wander() // Let's bump into allies and hit them.
// Similar to normal wander, but will walk into tiles that are harmful, and attack anything they bump into, including allies.
// Occurs when confused.
/datum/ai_holder/proc/dangerous_wander()
ai_log("dangerous_wander() : Entered.", AI_LOG_DEBUG)
if(isturf(holder.loc) && can_act())
// Test if we should refrain from falling/attacking allies, if we're smart enough to realize that.
if(intelligence_level > AI_NORMAL)
var/unsafe = FALSE
tile_test:
for(var/dir_tested in cardinal)
var/turf/turf_tested = get_step(holder, dir_tested)
// Look for unsafe tiles.
if(!turf_tested.is_safe_to_enter(holder))
unsafe = TRUE
break
// Look for allies.
for(var/mob/living/L in turf_tested)
if(holder.IIsAlly(L))
unsafe = TRUE
break tile_test
if(unsafe)
ai_log("dangerous_wander() : Staying still due to risk of harm to self or allies.", AI_LOG_TRACE)
return // Just stay still.
var/moving_to = 0
moving_to = pick(cardinal)
var/turf/T = get_step(holder, moving_to)
var/mob/living/L = locate() in T
if(L)
// Attack whoever's on the tile. Even if it's an ally.
ai_log("dangerous_wander() : Going to confuse-attack [L].", AI_LOG_TRACE)
melee_attack(L)
else
// Move to the tile. Even if it's unsafe.
ai_log("dangerous_wander() : Going to confuse-walk to [T] ([T.x],[T.y],[T.z]).", AI_LOG_TRACE)
holder.IMove(T, safety = FALSE)
ai_log("dangerous_wander() : Exited.", AI_LOG_DEBUG)
/*
// 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.IMove(get_step(holder,moving_to))
wander_delay = base_wander_delay
ai_log("handle_wander_movement() : Exited.", AI_LOG_DEBUG)
*/
+45
View File
@@ -0,0 +1,45 @@
// 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(special_flee_check())
return TRUE
if(!hostile && !retaliate)
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
// Override for special fleeing conditionally.
/datum/ai_holder/proc/special_flee_check()
return FALSE
/datum/ai_holder/proc/flee_from_target()
ai_log("flee_from_target() : Entering.", AI_LOG_DEBUG)
if(!target || !should_flee() || !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()
set_stance(STANCE_IDLE)
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)
+68
View File
@@ -0,0 +1,68 @@
// 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_TRACE)
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_TRACE)
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_TRACE)
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()
if(!leader)
return FALSE
if(follow_until_time && world.time > follow_until_time)
lose_follow()
set_stance(STANCE_IDLE)
return FALSE
if(get_dist(holder, leader) > follow_distance)
return TRUE
return FALSE
+154
View File
@@ -0,0 +1,154 @@
/datum/ai_holder
// 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. This is the turf the AI was initialized on by default.
var/returns_home = FALSE // If true, makes the mob go to its 'home' if it strays too far.
var/home_low_priority = FALSE // If true, the mob will not go home unless it has nothing better to do, e.g. its following someone.
var/max_home_distance = 3 // How far the mob can go away from its home before being told to go_home().
// Note that there is a 'BYOND cap' of 14 due to limitations of get_/step_to().
// 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.
/datum/ai_holder/proc/walk_to_destination()
ai_log("walk_to_destination() : Entering.",AI_LOG_TRACE)
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_TRACE)
return
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)
give_up_movement()
set_stance(stance == STANCE_REPOSITION ? STANCE_APPROACH : STANCE_IDLE)
ai_log("walk_to_destination() : Destination reached. Exiting.", AI_LOG_INFO)
return
ai_log("walk_to_destination() : Walking.", AI_LOG_TRACE)
walk_path(destination, get_to)
ai_log("walk_to_destination() : Exiting.",AI_LOG_TRACE)
/datum/ai_holder/proc/should_go_home()
if(!returns_home || !home_turf)
return FALSE
if(get_dist(holder, home_turf) > max_home_distance)
if(!home_low_priority)
return TRUE
else if(!leader && !target)
return TRUE
return FALSE
// 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_INFO)
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_TRACE)
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_DEBUG)
calculate_path(A, get_to)
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 get_step_to() instead.", AI_LOG_INFO)
// step_to(holder, A)
if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED)
ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_INFO)
breakthrough(A) // We failed to move, time to smash things.
return
if(move_once() == FALSE) // 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_DEBUG)
forget_path() // So lets try again with a new path.
failed_steps = 0
else
// step_to(holder, A)
ai_log("walk_path() : Going to IMove().", AI_LOG_TRACE)
if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED )
ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_INFO)
breakthrough(A) // We failed to move, time to smash things.
ai_log("walk_path() : Exited.", AI_LOG_TRACE)
//Take one step along a path
/datum/ai_holder/proc/move_once()
ai_log("move_once() : Entered.", AI_LOG_TRACE)
if(!path.len)
return
if(path_display)
var/turf/T = src.path[1]
T.overlays -= path_overlay
// step_towards(holder, src.path[1])
if(holder.IMove(get_step_towards(holder, src.path[1])) != MOVEMENT_ON_COOLDOWN)
if(holder.loc != src.path[1])
ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE)
return MOVEMENT_FAILED
else
path -= src.path[1]
ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE)
return MOVEMENT_SUCCESSFUL
ai_log("move_once() : Mob movement on cooldown. Exiting.", AI_LOG_TRACE)
return MOVEMENT_ON_COOLDOWN
/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_TRACE)
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.IMove(get_step(holder,moving_to))
wander_delay = base_wander_delay
ai_log("handle_wander_movement() : Exited.", AI_LOG_TRACE)
+58
View File
@@ -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
+237
View File
@@ -0,0 +1,237 @@
// Used for assigning a target for attacking.
/datum/ai_holder
var/hostile = FALSE // Do we try to hurt others?
var/retaliate = FALSE // Attacks whatever struck it first. Mobs will still attack back if this is false but hostile is true.
var/atom/movable/target = null // The thing (mob or object) we're trying to kill.
var/atom/movable/preferred_target = null// If set, and if given the chance, we will always prefer to target this over other options.
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/respect_alpha = TRUE // If true, mobs with a sufficently low alpha will be treated as invisible.
var/alpha_vision_threshold = 127 // Targets with an alpha less or equal to this will be considered invisible. Requires above var to be true.
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.
var/list/attackers = list() // List of strings of names of people who attacked us before in our life.
// This uses strings and not refs to allow for disguises, and to avoid needing to use weakrefs.
// 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)
if(!hostile) // So retaliating mobs only attack the thing that hit it.
return null
. = 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.
targets = target_filter_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
if(preferred_target && preferred_target in targets)
chosen_target = preferred_target
else
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
// Filters return one or more 'preferred' targets.
// This one is for closest targets.
/datum/ai_holder/proc/target_filter_distance(list/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
return targets
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
if(!can_see_target(the_target))
return FALSE
if(istype(the_target, /mob/zshadow))
return FALSE // no
if(isliving(the_target))
var/mob/living/L = the_target
if(L.stat == DEAD)
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 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) // Real invis.
ai_log("can_see_target() : Target ([the_target]) was invisible to holder. Exiting.", AI_LOG_TRACE)
return FALSE
if(respect_alpha && the_target.alpha <= alpha_vision_threshold) // Fake invis.
ai_log("can_see_target() : Target ([the_target]) was sufficently transparent to holder and is hidden. 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
// Responds to a hostile action against its mob.
/datum/ai_holder/proc/react_to_attack(atom/movable/attacker)
if(holder.stat) // We're dead.
ai_log("react_to_attack() : Was attacked by [attacker], but we are dead/unconscious.", AI_LOG_TRACE)
return FALSE
if(!hostile && !retaliate) // Not allowed to defend ourselves.
ai_log("react_to_attack() : Was attacked by [attacker], but we are not allowed to attack back.", AI_LOG_TRACE)
return FALSE
if(holder.IIsAlly(attacker)) // I'll overlook it THIS time...
ai_log("react_to_attack() : Was attacked by [attacker], but they were an ally.", AI_LOG_TRACE)
return FALSE
if(target) // Already fighting someone. Switching every time we get hit would impact our combat performance.
ai_log("react_to_attack() : Was attacked by [attacker], but we already have a target.", AI_LOG_TRACE)
on_attacked(attacker) // So we attack immediately and not threaten.
return FALSE
if(stance == STANCE_SLEEP) // If we're asleep, try waking up if someone's wailing on us.
ai_log("react_to_attack() : AI is asleep. Waking up.", AI_LOG_TRACE)
go_wake()
ai_log("react_to_attack() : Was attacked by [attacker].", AI_LOG_INFO)
on_attacked(attacker) // So we attack immediately and not threaten.
return give_target(attacker) // Also handles setting the appropiate stance.
// Sets a few vars so mobs that threaten will react faster to an attacker or someone who attacked them before.
/datum/ai_holder/proc/on_attacked(atom/movable/AM)
last_conflict_time = world.time
if(isliving(AM))
var/mob/living/L = AM
attackers |= L.name
// Causes targeting to prefer targeting the taunter if possible.
// This generally occurs if more than one option is within striking distance, including the taunter.
// Otherwise the default filter will prefer the closest target.
/datum/ai_holder/proc/receive_taunt(atom/movable/taunter, force_target_switch = FALSE)
ai_log("receive_taunt() : Was taunted by [taunter].", AI_LOG_INFO)
preferred_target = taunter
if(force_target_switch)
give_target(taunter)
/datum/ai_holder/proc/lose_taunt()
ai_log("lose_taunt() : Resetting preferred_target.", AI_LOG_INFO)
preferred_target = null
+92
View File
@@ -0,0 +1,92 @@
// 'Interfaces' are procs that the ai_holder datum uses to communicate its will to the mob its attached.
// The reason for using this proc in the middle is to ensure the AI has some form of compatibility with most mob types,
// 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/A)
return FALSE
/mob/living/simple_mob/IAttack(atom/A)
if(!canClick()) // Still on cooldown from a "click".
return FALSE
return attack_target(A) // This will set click cooldown.
/mob/living/proc/IRangedAttack(atom/A)
return FALSE
/mob/living/simple_mob/IRangedAttack(atom/A)
if(!canClick()) // Still on cooldown from a "click".
return FALSE
return shoot_target(A)
// Test if the AI is allowed to attempt a ranged attack.
/mob/living/proc/ICheckRangedAttack(atom/A)
return FALSE
/mob/living/simple_mob/ICheckRangedAttack(atom/A)
if(needs_reload)
if(reload_count >= reload_max)
try_reload()
return FALSE
return projectiletype ? TRUE : FALSE
/mob/living/proc/ISpecialAttack(atom/A)
return FALSE
/mob/living/simple_mob/ISpecialAttack(atom/A)
return special_attack_target(A)
// Is the AI allowed to attempt to do it?
/mob/living/proc/ICheckSpecialAttack(atom/A)
return FALSE
/mob/living/simple_mob/ICheckSpecialAttack(atom/A)
return can_special_attack(A) && should_special_attack(A) // Just because we can doesn't mean we should.
/mob/living/proc/ISay(message)
return say(message)
/mob/living/proc/IIsAlly(mob/living/L)
return src.faction == L.faction
/mob/living/simple_mob/IIsAlly(mob/living/L)
. = ..()
if(!.) // Outside the faction, try to see if they're friends.
return L in friends
/mob/living/proc/IGetID()
/mob/living/simple_mob/IGetID()
if(myid)
return myid.GetID()
// Respects move cooldowns as if it had a client.
// Also tries to avoid being superdumb with moving into certain tiles (unless that's desired).
/mob/living/proc/IMove(turf/newloc, safety = TRUE)
if(check_move_cooldown())
// if(!newdir)
// newdir = get_dir(get_turf(src), newloc)
// Check to make sure moving to newloc won't actually kill us. e.g. we're a slime and trying to walk onto water.
if(istype(newloc))
if(safety && !newloc.is_safe_to_enter(src))
return MOVEMENT_FAILED
// Move()ing to another tile successfully returns 32 because BYOND. Would rather deal with TRUE/FALSE-esque terms.
// Note that moving to the same tile will be 'successful'.
var/turf/old_T = get_turf(src)
// An adjacency check to avoid mobs phasing diagonally past windows.
// This might be better in general movement code but I'm too scared to add it, and most things don't move diagonally anyways.
if(!old_T.Adjacent(newloc))
return MOVEMENT_FAILED
. = SelfMove(newloc) ? MOVEMENT_SUCCESSFUL : MOVEMENT_FAILED
if(. == MOVEMENT_SUCCESSFUL)
set_dir(get_dir(old_T, newloc))
// Apply movement delay.
// Player movement has more factors but its all in the client and fixing that would be its own project.
setMoveCooldown(movement_delay())
return
. = MOVEMENT_ON_COOLDOWN // To avoid superfast mobs that aren't meant to be superfast. Is actually -1.
+119
View File
@@ -0,0 +1,119 @@
// A simple datum that just holds many lists of lines for 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.
// and now sounds because its probably better that way.
/mob/living
var/datum/say_list/say_list = null
var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent.
/mob/living/initialize()
if(say_list_type)
say_list = new say_list_type(src)
return ..()
/mob/living/Destroy()
QDEL_NULL(say_list)
return ..()
/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.
var/threaten_sound = null // Sound file played when the mob's AI calls threaten_target() for the first time.
var/stand_down_sound = null // Sound file played when the mob's AI loses sight of the threatened target.
// 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!")
// Mercs!
/datum/say_list/merc
speak = list("When are we gonna get out of this chicken-shit outfit?",
"Wish I had better equipment...",
"I knew I should have been a line chef...",
"Fuckin' helmet keeps fogging up.",
"Anyone else smell that?")
emote_see = list("sniffs", "coughs", "taps his foot", "looks around", "checks his equipment")
say_understood = list("Understood!", "Affirmative!")
say_cannot = list("Negative!")
say_maybe_target = list("Who's there?")
say_got_target = list("Engaging!")
say_threaten = list("Get out of here!", "Hey! Private Property!")
say_stand_down = list("Good.")
say_escalate = list("Your funeral!", "Bring it!")
/datum/say_list/malf_drone
speak = list("ALERT.","Hostile-ile-ile entities dee-twhoooo-wected.","Threat parameterszzzz- szzet.","Bring sub-sub-sub-systems uuuup to combat alert alpha-a-a.")
emote_see = list("beeps menacingly","whirrs threateningly","scans its immediate vicinity")
say_understood = list("Affirmative.", "Positive.")
say_cannot = list("Denied.", "Negative.")
say_maybe_target = list("Possible threat detected. Investigating.", "Motion detected.", "Investigating.")
say_got_target = list("Threat detected.", "New task: Remove threat.", "Threat removal engaged.", "Engaging target.")
say_threaten = list("Motion detected, judging target...")
say_stand_down = list("Visual lost.", "Error: Target not found.")
say_escalate = list("Viable target found. Removing.", "Engaging target.", "Target judgement complete. Removal required.")
threaten_sound = 'sound/effects/turret/move1.wav'
stand_down_sound = 'sound/effects/turret/move2.wav'
/datum/say_list/mercenary
threaten_sound = 'sound/weapons/TargetOn.ogg'
stand_down_sound = 'sound/weapons/TargetOff.ogg'
/datum/say_list/crab
emote_hear = list("clicks")
emote_see = list("clacks")
/datum/say_list/spider
emote_hear = list("chitters")
/datum/say_list/hivebot
speak = list(
"Resuming task: Protect area.",
"No threats found.",
"Error: No targets found."
)
emote_hear = list("hums ominously", "whirrs softly", "grinds a gear")
emote_see = list("looks around the area", "turns from side to side")
say_understood = list("Affirmative.", "Positive.")
say_cannot = list("Denied.", "Negative.")
say_maybe_target = list("Possible threat detected. Investigating.", "Motion detected.", "Investigating.")
say_got_target = list("Threat detected.", "New task: Remove threat.", "Threat removal engaged.", "Engaging target.")
/datum/say_list/lizard
emote_hear = list("hisses")
/datum/say_list/crab
emote_hear = list("hisses")