mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-25 04:57:47 +01:00
Merges AI Branch into Master
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
*/
|
||||
@@ -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 ..()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user