mirror of
https://github.com/PolarisSS13/Polaris.git
synced 2026-08-26 06:39:22 +01:00
Fixes loads of AI bugs.
This commit is contained in:
@@ -170,10 +170,21 @@
|
||||
qdel(buildquit)
|
||||
buildquit = null
|
||||
throw_atom = null
|
||||
cl = null
|
||||
for(var/mob/living/unit in selected_mobs)
|
||||
deselect_AI_mob(cl, unit)
|
||||
selected_mobs.Cut()
|
||||
cl = null
|
||||
return ..()
|
||||
|
||||
/obj/effect/bmode/buildholder/proc/select_AI_mob(client/C, mob/living/unit)
|
||||
selected_mobs += unit
|
||||
C.images += unit.selected_image
|
||||
|
||||
/obj/effect/bmode/buildholder/proc/deselect_AI_mob(client/C, mob/living/unit)
|
||||
selected_mobs -= unit
|
||||
C.images -= unit.selected_image
|
||||
|
||||
|
||||
/obj/effect/bmode/buildmode
|
||||
icon_state = "buildmode1"
|
||||
screen_loc = "NORTH,WEST+2"
|
||||
@@ -460,48 +471,58 @@
|
||||
// Select/Deselect
|
||||
if(!isnull(L.get_AI_stance()))
|
||||
if(L in holder.selected_mobs)
|
||||
// Todo: Select graphic only the admin can see?
|
||||
holder.selected_mobs -= L
|
||||
user.client.images -= L.selected_image
|
||||
holder.deselect_AI_mob(user.client, L)
|
||||
to_chat(user, span("notice", "Deselected \the [L]."))
|
||||
else
|
||||
holder.selected_mobs += L
|
||||
user.client.images += L.selected_image
|
||||
holder.select_AI_mob(user.client, L)
|
||||
to_chat(user, span("notice", "Selected \the [L]."))
|
||||
else
|
||||
to_chat(user, span("warning", "\The [L] is not AI controlled."))
|
||||
|
||||
if(pa.Find("right"))
|
||||
if(istype(object, /atom/movable)) // Force attack.
|
||||
var/atom/movable/AM = object
|
||||
if(istype(object, /atom)) // Force attack.
|
||||
var/atom/A = object
|
||||
|
||||
if(pa.Find("alt"))
|
||||
for(var/thing in holder.selected_mobs)
|
||||
var/mob/living/unit = thing
|
||||
var/i = 0
|
||||
for(var/mob/living/unit in holder.selected_mobs)
|
||||
var/datum/ai_holder/AI = unit.ai_holder
|
||||
AI.give_target(AM)
|
||||
to_chat(user, span("notice", "Commanded [holder.selected_mobs.len] mob\s to attack \the [AM]."))
|
||||
AI.give_target(A)
|
||||
i++
|
||||
to_chat(user, span("notice", "Commanded [i] mob\s to attack \the [A]."))
|
||||
return
|
||||
|
||||
if(isliving(object)) // Follow or attack.
|
||||
var/mob/living/L = object
|
||||
|
||||
for(var/thing in holder.selected_mobs)
|
||||
var/mob/living/unit = thing
|
||||
var/i = 0 // Attacking mobs.
|
||||
var/j = 0 // Following mobs.
|
||||
for(var/mob/living/unit in holder.selected_mobs)
|
||||
var/datum/ai_holder/AI = unit.ai_holder
|
||||
if(L.IIsAlly(unit) || !AI.hostile || pa.Find("shift"))
|
||||
AI.set_follow(L)
|
||||
j++
|
||||
else
|
||||
AI.give_target(L)
|
||||
to_chat(user, span("notice", "Commanded [holder.selected_mobs.len] mob\s to attack or follow \the [L]."))
|
||||
i++
|
||||
var/message = "Commanded "
|
||||
if(i)
|
||||
message += "[i] mob\s to attack \the [L]"
|
||||
if(j)
|
||||
message += ", and "
|
||||
else
|
||||
message += "."
|
||||
if(j)
|
||||
message += "[j] mob\s to follow \the [L]."
|
||||
to_chat(user, span("notice", message))
|
||||
|
||||
if(isturf(object)) // Move or reposition.
|
||||
var/turf/T = object
|
||||
|
||||
for(var/thing in holder.selected_mobs)
|
||||
var/mob/living/unit = thing
|
||||
var/i = 0
|
||||
for(var/mob/living/unit in holder.selected_mobs)
|
||||
var/datum/ai_holder/AI = unit.ai_holder
|
||||
AI.give_destination(T, 1, pa.Find("shift"))
|
||||
to_chat(user, span("notice", "Commanded [holder.selected_mobs.len] mob\s to move to \the [T]."))
|
||||
AI.give_destination(T, 1, pa.Find("shift")) // If shift is held, the mobs will not stop moving to attack a visible enemy.
|
||||
i++
|
||||
to_chat(user, span("notice", "Commanded [i] mob\s to move to \the [T]."))
|
||||
|
||||
|
||||
/obj/effect/bmode/buildmode/proc/get_path_from_partial_text(default_path)
|
||||
|
||||
+178
-34
@@ -1,57 +1,201 @@
|
||||
/*
|
||||
[Summary]
|
||||
|
||||
This module contains an AI implementation designed to be (mostly) mobtype-agnostic, by being held inside a datum instead of being on the mob directly.
|
||||
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:
|
||||
[Seperation]
|
||||
|
||||
The ai_holder datum is designed to be fairly distant from its mob holder, in terms of coupling.
|
||||
This presents some advantages.
|
||||
* Not being tied to the mob's Life() cycle allows for a different tick rate.
|
||||
* Being seperate from the mob simplifies mob code greatly.
|
||||
* It allows for better encapsulation and seperation of duties from the mob.
|
||||
* It is more logical to think that the mob is the 'body', where as its ai_holder is the 'mind'.
|
||||
* It can be made mobtype-independant with the use of Interfaces.
|
||||
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 datum is held by the mob that it should control, called the holder, and is not processed by the mob itself (as Life() did so in previous implementations).
|
||||
Instead, each instance of /datum/ai_holder is processed by the 'AI' master controller subsystem. The datum itself has two seperate process tracks instead of one,
|
||||
as most other objects ( process() ) or mobs ( Life() ) do.
|
||||
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.
|
||||
|
||||
|
||||
Flow of Execution:
|
||||
[Stances]
|
||||
|
||||
AI Subsystem
|
||||
- > Every 0.5s - > /datum/ai_holder/handle_tactics() - > /datum/ai_holder/handle_stance_tactical() - > switch(stance)...
|
||||
- > Every 2.0s - > /datum/ai_holder/handle_strategicals() - > /datum/ai_holder/handle_stance_strategical() - > switch(stance)...
|
||||
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.
|
||||
|
||||
The datum is not driven by its mob, as previous implementations did, meaning Life() is not involved. Instead, it is processed by a specific Master Controller Subsystem
|
||||
titled 'AI', which by default ticks every half a second. Each instance of the ai_holder datum that is not 'asleep' is part of a list, containing ai_holders that are awake.
|
||||
When the subsystem runs, each ai_holder instance inside the list is iterated on, and calls one or two procs on it.
|
||||
Every tick, each instance has handle_tactics() called on it, and every four ticks, handle_strategicals(), meaning every half a second and very two seconds, respectively.
|
||||
This means that the ai_holder datum has two seperate types of processing, for the purposes of efficency.
|
||||
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.
|
||||
|
||||
handle_tactics() is used for 'cheap' processing that needs to happen fast, such as walking along a path, initiating an attack, or firing a gun.
|
||||
The rate that it is called allows for the ai_holder to interact with the world through its mob very often, giving a more convincing appearance of intelligence,
|
||||
allowing for faster reaction times to certain events, and allowing for variable attack speeds that would not be possible when bound to a two second Life() cycle.
|
||||
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.
|
||||
|
||||
handle_strategicals() on the other hand, is for 'expensive' processing that might be too demanding on the CPU to do every half a second, such as re/calculating an A* path, or
|
||||
running a complicated tension assessment to determine how brave the mob is feeling. This is the same delay used for certain tasks in the old implementation.
|
||||
[Interfaces]
|
||||
|
||||
Having two seperate process procs allows for 'fast' and 'slow' actions to be more easily encapsulated, and ensures that all ai_holders are syncronized with each other,
|
||||
as opposed to having individual tick counters inside all of the ai_holder instances. It should be noted that handle_tactics() is called first,
|
||||
before handle_strategicals() every two seconds.
|
||||
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.
|
||||
|
||||
Both process procs work in a similar fashion, using a large amount of 'stances' to act in a specific way, effectively creating a state pattern.
|
||||
There are 10 stances implemented, and more can easily be added by defining them and switching to them with the set_stance() proc.
|
||||
This is similar to the old implementation, except with a vastly larger amount of options for the AI to make use of.
|
||||
See code/__defines/mob.dm for the stance defines and descriptions about their purpose.
|
||||
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.
|
||||
|
||||
Each stance is evaluated once per tick, meaning that switching the stance takes effect on the next tick.
|
||||
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.
|
||||
|
||||
|
||||
*/
|
||||
@@ -2,7 +2,7 @@
|
||||
// Controls if the mob will do 'advanced tactics' like running from grenades.
|
||||
#define AI_DUMB 1 // Be dumber than usual.
|
||||
#define AI_NORMAL 2 // Default level.
|
||||
#define AI_SMART 3 // Will do more processing to perceive threats such as active grenades. Use sparingly.
|
||||
#define AI_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)
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
#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.
|
||||
#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.
|
||||
#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.
|
||||
@@ -27,5 +27,3 @@
|
||||
#define AI_TARGET_ALLY 3 // They are an ally. Find a new target.
|
||||
#define AI_TARGET_DEAD 4 // They're dead. Find a new target.
|
||||
#define AI_TARGET_INVINCIBLE 5 // Target is currently unable to receive damage for whatever reason. Find a new target or wait.
|
||||
|
||||
//TRACE DEBUG INFO WARN ERROR OFF
|
||||
@@ -13,6 +13,7 @@
|
||||
/datum/ai_holder/simple_mob/passive
|
||||
hostile = FALSE
|
||||
can_flee = TRUE
|
||||
violent_breakthrough = FALSE
|
||||
|
||||
// For parrots like Poly.
|
||||
// They modify their say_list datum based on what their mob hears.
|
||||
@@ -21,10 +22,14 @@
|
||||
base_wander_delay = 8
|
||||
|
||||
/datum/ai_holder/simple_mob/passive/parrot/on_hear_say(mob/living/speaker, message)
|
||||
if(holder.stat || !holder.say_list || !message)
|
||||
if(holder.stat || !holder.say_list || !message || speaker == holder)
|
||||
return
|
||||
var/datum/say_list/S = holder.say_list
|
||||
S.speak += message
|
||||
S.speak |= message
|
||||
|
||||
// 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
|
||||
@@ -34,6 +39,7 @@
|
||||
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
|
||||
@@ -63,6 +69,9 @@
|
||||
/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().
|
||||
@@ -73,6 +82,11 @@
|
||||
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
|
||||
@@ -151,12 +165,11 @@
|
||||
|
||||
// If we're surrounded, Electric Defense will quickly fix that.
|
||||
var/tally = 0
|
||||
for(var/mob/living/L in hearers(electric_defense_radius, holder))
|
||||
if(holder == L)
|
||||
var/list/potential_targets = list_targets() // Returns list of mobs and certain objects like mechs and turrets.
|
||||
for(var/atom/movable/AM in potential_targets)
|
||||
if(get_dist(holder, AM) > electric_defense_radius)
|
||||
continue
|
||||
if(L.IIsAlly(holder))
|
||||
continue
|
||||
if(L.stat)
|
||||
if(!can_attack(AM))
|
||||
continue
|
||||
tally++
|
||||
|
||||
@@ -168,14 +181,15 @@
|
||||
// Otherwise they're a fair distance away and we're not getting mobbed up close.
|
||||
// See if we should use missiles or microsingulo.
|
||||
tally = 0 // Let's recycle the var.
|
||||
for(var/mob/living/L in hearers(microsingulo_radius, target))
|
||||
if(holder == L)
|
||||
for(var/atom/movable/AM in potential_targets)
|
||||
if(get_dist(target, AM) > microsingulo_radius) // Deliberately tests distance between target and nearby targets and not the holder.
|
||||
continue
|
||||
if(L.IIsAlly(holder))
|
||||
if(!can_attack(AM))
|
||||
continue
|
||||
if(L.stat)
|
||||
continue
|
||||
tally++
|
||||
if(AM.anchored) // Microsingulo doesn't do anything to anchored things.
|
||||
tally--
|
||||
else
|
||||
tally++
|
||||
|
||||
// Lots of people means minisingulo would be more useful.
|
||||
if(tally >= microsingulo_threshold)
|
||||
@@ -187,6 +201,10 @@
|
||||
holder.a_intent = I_HURT // Fire rockets if it's an obj/turf.
|
||||
|
||||
|
||||
// These try to avoid collateral damage.
|
||||
/datum/ai_holder/simple_mob/restrained
|
||||
violent_breakthrough = FALSE
|
||||
conserve_ammo = TRUE
|
||||
|
||||
// Melee mobs.
|
||||
|
||||
@@ -222,6 +240,7 @@
|
||||
/datum/ai_holder/simple_mob/melee/nurse_spider
|
||||
wander = TRUE
|
||||
base_wander_delay = 8
|
||||
cooperative = FALSE // So we don't ask our spider friends to attack things we're webbing. This might also make them stay at the base if their friends find tasty explorers.
|
||||
|
||||
// Get us unachored objects as an option as well.
|
||||
/datum/ai_holder/simple_mob/melee/nurse_spider/list_targets()
|
||||
@@ -333,6 +352,7 @@
|
||||
*/
|
||||
|
||||
/datum/ai_holder/simple_mob/hivebot
|
||||
pointblank = TRUE
|
||||
conserve_ammo = TRUE
|
||||
firing_lanes = TRUE
|
||||
can_flee = FALSE // Fearless dumb machines.
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
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 slowly, potentially making it not decay at all.
|
||||
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
|
||||
@@ -255,3 +256,7 @@
|
||||
*/
|
||||
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 ..()
|
||||
@@ -7,7 +7,8 @@
|
||||
var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy, if the mob is capable of ranged attacks.
|
||||
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 break things like windows or other structures in the way.
|
||||
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.
|
||||
|
||||
|
||||
@@ -196,7 +197,8 @@
|
||||
|
||||
|
||||
// Otherwise keep walking.
|
||||
walk_path(target, get_to)
|
||||
if(!stand_ground)
|
||||
walk_path(target, get_to)
|
||||
|
||||
ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG)
|
||||
|
||||
@@ -206,53 +208,102 @@
|
||||
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)
|
||||
|
||||
// First, try to break things directly in front of us.
|
||||
var/result = destroy_surroundings(dir_to_target)
|
||||
// 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)
|
||||
)
|
||||
|
||||
// If that doesn't work, we might be trying to attack something diagonally.
|
||||
// If so, we can try again with some adjustments to avoid invalid diagonal directions.
|
||||
if(!result)
|
||||
result = destroy_surroundings(turn(dir_to_target, 45))
|
||||
ai_log("breakthrough() : Starting peaceful pass.", AI_LOG_DEBUG)
|
||||
|
||||
// One last time, going the other way.
|
||||
if(!result)
|
||||
result = destroy_surroundings(turn(dir_to_target, -45))
|
||||
var/result = FALSE
|
||||
|
||||
// Welp.
|
||||
// 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
|
||||
|
||||
/datum/ai_holder/proc/destroy_surroundings(direction)
|
||||
// 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, kill windows in the way.
|
||||
for(var/obj/structure/window/W in problem_turf)
|
||||
if(W.dir == reverse_dir[holder.dir]) // So that windows get smashed in the right order
|
||||
ai_log("destroy_surroundings() : Attacking diagonal window.", AI_LOG_INFO)
|
||||
return holder.IAttack(W)
|
||||
// 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()
|
||||
|
||||
else if(W.is_fulltile())
|
||||
ai_log("destroy_surroundings() : Attacking full tile window.", AI_LOG_INFO)
|
||||
return holder.IAttack(W)
|
||||
// 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)
|
||||
|
||||
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)
|
||||
else if(W.is_fulltile())
|
||||
ai_log("destroy_surroundings() : Attacking full tile window.", AI_LOG_INFO)
|
||||
return holder.IAttack(W)
|
||||
|
||||
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)
|
||||
// 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
|
||||
@@ -92,6 +92,9 @@
|
||||
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
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
var/follow_until_time = 0 // world.time when the mob will stop following leader. 0 means it won't time out.
|
||||
|
||||
/datum/ai_holder/proc/walk_to_leader()
|
||||
ai_log("walk_to_leader() : Entering.",AI_LOG_DEBUG)
|
||||
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_DEBUG)
|
||||
ai_log("walk_to_leader() : Exiting.", AI_LOG_TRACE)
|
||||
return
|
||||
|
||||
// Did we time out?
|
||||
@@ -20,7 +20,7 @@
|
||||
ai_log("walk_to_leader() : Follow timed out, losing leader.", AI_LOG_INFO)
|
||||
lose_follow()
|
||||
set_stance(STANCE_IDLE)
|
||||
ai_log("walk_to_leader() : Exiting.", AI_LOG_DEBUG)
|
||||
ai_log("walk_to_leader() : Exiting.", AI_LOG_TRACE)
|
||||
return
|
||||
|
||||
var/get_to = follow_distance
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
var/min_distance_to_destination = 1 // Holds how close the mob should go to destination until they're done.
|
||||
|
||||
// Home.
|
||||
var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so.
|
||||
var/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().
|
||||
@@ -18,12 +18,12 @@
|
||||
|
||||
|
||||
/datum/ai_holder/proc/walk_to_destination()
|
||||
ai_log("walk_to_destination() : Entering.",AI_LOG_DEBUG)
|
||||
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_DEBUG)
|
||||
ai_log("walk_to_destination() : Exiting.", AI_LOG_TRACE)
|
||||
return
|
||||
|
||||
var/get_to = min_distance_to_destination
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
ai_log("walk_to_destination() : Walking.", AI_LOG_TRACE)
|
||||
walk_path(destination, get_to)
|
||||
ai_log("walk_to_destination() : Exiting.",AI_LOG_DEBUG)
|
||||
ai_log("walk_to_destination() : Exiting.",AI_LOG_TRACE)
|
||||
|
||||
/datum/ai_holder/proc/should_go_home()
|
||||
if(!returns_home || !home_turf)
|
||||
@@ -67,7 +67,7 @@
|
||||
min_distance_to_destination = min_distance
|
||||
|
||||
if(new_destination != null)
|
||||
ai_log("give_destination() : Going to new destination.", AI_LOG_TRACE)
|
||||
ai_log("give_destination() : Going to new destination.", AI_LOG_INFO)
|
||||
set_stance(combat ? STANCE_REPOSITION : STANCE_MOVE)
|
||||
return TRUE
|
||||
else
|
||||
@@ -78,18 +78,18 @@
|
||||
|
||||
// Walk towards whatever.
|
||||
/datum/ai_holder/proc/walk_path(atom/A, get_to = 1)
|
||||
ai_log("walk_path() : Entered.", AI_LOG_DEBUG)
|
||||
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_TRACE)
|
||||
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_TRACE)
|
||||
ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_INFO)
|
||||
breakthrough(A) // We failed to move, time to smash things.
|
||||
return
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
ai_log("walk_path() : Failed to step.", AI_LOG_TRACE)
|
||||
++failed_steps
|
||||
if(failed_steps > 3) // We're probably stuck.
|
||||
ai_log("walk_path() : Too many failed_steps.", AI_LOG_INFO)
|
||||
ai_log("walk_path() : Too many failed_steps.", AI_LOG_DEBUG)
|
||||
forget_path() // So lets try again with a new path.
|
||||
failed_steps = 0
|
||||
|
||||
@@ -105,15 +105,15 @@
|
||||
// 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_TRACE)
|
||||
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_DEBUG)
|
||||
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_DEBUG)
|
||||
ai_log("move_once() : Entered.", AI_LOG_TRACE)
|
||||
if(!path.len)
|
||||
return
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
// Wanders randomly in cardinal directions.
|
||||
/datum/ai_holder/proc/handle_wander_movement()
|
||||
ai_log("handle_wander_movement() : Entered.", AI_LOG_DEBUG)
|
||||
ai_log("handle_wander_movement() : Entered.", AI_LOG_TRACE)
|
||||
if(isturf(holder.loc) && can_act())
|
||||
wander_delay--
|
||||
if(wander_delay <= 0)
|
||||
@@ -151,4 +151,4 @@
|
||||
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)
|
||||
ai_log("handle_wander_movement() : Exited.", AI_LOG_TRACE)
|
||||
|
||||
@@ -66,13 +66,21 @@
|
||||
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))
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
while (i <= num_groups)
|
||||
var/group_size = rand(group_size_min, group_size_max)
|
||||
for (var/j = 1, j <= group_size, j++)
|
||||
spawned_carp.Add(new /mob/living/simple_mob/animal/space/carp(spawn_locations[i]))
|
||||
spawned_carp.Add(new /mob/living/simple_mob/animal/space/carp/event(spawn_locations[i]))
|
||||
i++
|
||||
|
||||
/datum/event/carp_migration/end()
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
else
|
||||
num = rand(2,6)
|
||||
for(var/i=0, i<num, i++)
|
||||
var/mob/living/simple_mob/mechanical/combat_drone/D = new(get_turf(pick(possible_spawns)))
|
||||
var/mob/living/simple_mob/mechanical/combat_drone/event/D = new(get_turf(pick(possible_spawns)))
|
||||
drones_list.Add(D)
|
||||
|
||||
/datum/event/rogue_drone/announce()
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
while (i <= carp_amount)
|
||||
var/group_size = rand(group_size_min, group_size_max)
|
||||
for (var/j = 1, j <= group_size, j++)
|
||||
spawned_carp.Add(new /mob/living/simple_mob/animal/space/carp(spawn_locations[i]))
|
||||
spawned_carp.Add(new /mob/living/simple_mob/animal/space/carp/event(spawn_locations[i]))
|
||||
i++
|
||||
message_admins("[spawned_carp.len] carp spawned by event.")
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
health = 20
|
||||
maxHealth = 20
|
||||
|
||||
mob_bump_flag = SIMPLE_ANIMAL
|
||||
mob_swap_flags = MONKEY|SLIME|HUMAN
|
||||
mob_push_flags = MONKEY|SLIME|HUMAN
|
||||
// Generally we don't want simple_mobs to get displaced when bumped into due to it trivializing combat with windup attacks.
|
||||
// Some subtypes allow displacement, like passive animals.
|
||||
mob_bump_flag = HEAVY
|
||||
mob_swap_flags = ~HEAVY
|
||||
mob_push_flags = ~HEAVY
|
||||
|
||||
var/tt_desc = "Uncataloged Life Form" //Tooltip description
|
||||
|
||||
@@ -222,6 +224,11 @@
|
||||
if(!isnull(M.slowdown))
|
||||
tally += M.slowdown
|
||||
|
||||
// Turf related slowdown
|
||||
var/turf/T = get_turf(src)
|
||||
if(T && T.movement_cost && !hovering) // Flying mobs ignore turf-based slowdown.
|
||||
tally += T.movement_cost
|
||||
|
||||
if(purge)//Purged creatures will move more slowly. The more time before their purge stops, the slower they'll move.
|
||||
if(tally <= 0)
|
||||
tally = 1
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
health = 120
|
||||
|
||||
poison_per_bite = 5
|
||||
melee_damage_lower = 9
|
||||
melee_damage_upper = 15
|
||||
|
||||
movement_cooldown = 0 // Hunters are FAST.
|
||||
|
||||
@@ -19,7 +21,8 @@
|
||||
|
||||
player_msg = "You are very fast, and <b>can perform a leaping attack</b> by clicking on someone from a short distance away.<br>\
|
||||
If the leap succeeds, the target will be knocked down briefly and you will be on top of them.<br>\
|
||||
Note that there is a short delay before you leap!"
|
||||
Note that there is a short delay before you leap!<br>\
|
||||
In addition, you will do more damage to incapacitated opponents."
|
||||
|
||||
// Leaping is a special attack, so these values determine when leap can happen.
|
||||
// Leaping won't occur if its on cooldown.
|
||||
@@ -28,6 +31,16 @@
|
||||
special_attack_cooldown = 10 SECONDS
|
||||
|
||||
var/leap_warmup = 1 SECOND // How long the leap telegraphing is.
|
||||
var/leap_sound = 'sound/weapons/spiderlunge.ogg'
|
||||
|
||||
// Multiplies damage if the victim is stunned in some form, including a successful leap.
|
||||
/mob/living/simple_mob/animal/giant_spider/hunter/apply_bonus_melee_damage(atom/A, damage_amount)
|
||||
if(isliving(A))
|
||||
var/mob/living/L = A
|
||||
if(L.incapacitated(INCAPACITATION_DISABLED))
|
||||
return damage_amount * 1.5
|
||||
return ..()
|
||||
|
||||
|
||||
// The actual leaping attack.
|
||||
/mob/living/simple_mob/animal/giant_spider/hunter/do_special_attack(atom/A)
|
||||
@@ -42,7 +55,7 @@
|
||||
status_flags |= LEAPING // Lets us pass over everything.
|
||||
visible_message(span("danger","\The [src] leaps at \the [A]!"))
|
||||
throw_at(get_step(get_turf(A), get_turf(src)), special_attack_max_range+1, 1, src)
|
||||
playsound(src, 'sound/weapons/spiderlunge.ogg', 75, 1)
|
||||
playsound(src, leap_sound, 75, 1)
|
||||
|
||||
sleep(5) // For the throw to complete. It won't hold up the AI ticker due to waitfor being false.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
maxHealth = 5
|
||||
health = 5
|
||||
|
||||
mob_size = MOB_SMALL
|
||||
mob_size = MOB_MINISCULE
|
||||
pass_flags = PASSTABLE
|
||||
// can_pull_size = ITEMSIZE_TINY
|
||||
// can_pull_mobs = MOB_PULL_NONE
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Passive mobs can't attack things, and will run away instead.
|
||||
// They can also be displaced by all mobs.
|
||||
/mob/living/simple_mob/animal/passive
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/passive
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/passive
|
||||
mob_bump_flag = 0
|
||||
@@ -57,6 +57,7 @@
|
||||
icon_state = "commonblackbird"
|
||||
icon_dead = "commonblackbird-dead"
|
||||
tt_desc = "E Turdus merula"
|
||||
icon_scale = 0.5
|
||||
|
||||
/mob/living/simple_mob/animal/passive/bird/azure_tit
|
||||
name = "azure tit"
|
||||
@@ -64,6 +65,7 @@
|
||||
icon_state = "azuretit"
|
||||
icon_dead = "azuretit-dead"
|
||||
tt_desc = "E Cyanistes cyanus"
|
||||
icon_scale = 0.5
|
||||
|
||||
/mob/living/simple_mob/animal/passive/bird/european_robin
|
||||
name = "european robin"
|
||||
@@ -71,6 +73,7 @@
|
||||
icon_state = "europeanrobin"
|
||||
icon_dead = "europeanrobin-dead"
|
||||
tt_desc = "E Erithacus rubecula"
|
||||
icon_scale = 0.5
|
||||
|
||||
/mob/living/simple_mob/animal/passive/bird/goldcrest
|
||||
name = "goldcrest"
|
||||
@@ -79,6 +82,7 @@
|
||||
icon_state = "goldcrest"
|
||||
icon_dead = "goldcrest-dead"
|
||||
tt_desc = "E Regulus regulus"
|
||||
icon_scale = 0.5
|
||||
|
||||
/mob/living/simple_mob/animal/passive/bird/ringneck_dove
|
||||
name = "ringneck dove"
|
||||
@@ -86,3 +90,4 @@
|
||||
icon_state = "ringneckdove"
|
||||
icon_dead = "ringneckdove-dead"
|
||||
tt_desc = "E Streptopelia risoria" // This is actually disputed IRL but since we can't tell the future it'll stay the same for 500+ years.
|
||||
icon_scale = 0.5
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/mob/living/simple_mob/animal/passive/bird/parrot
|
||||
name = "parrot"
|
||||
description_info = "You can give it a headset by clicking on it with a headset. \
|
||||
To remove it, click-drag the bird to you while adjacent to them."
|
||||
To remove it, click the bird while on grab intent."
|
||||
has_langs = list("Galactic Common", "Bird")
|
||||
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/passive/parrot
|
||||
@@ -25,7 +25,7 @@
|
||||
"Meteors have been detected on a collision course with the station!"
|
||||
)
|
||||
|
||||
// Let's the AI use headsets.
|
||||
// Lets the AI use headsets.
|
||||
// Player-controlled parrots will need to do it manually.
|
||||
/mob/living/simple_mob/animal/passive/bird/parrot/ISay(message)
|
||||
if(my_headset && prob(50))
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
health = 200
|
||||
movement_cooldown = 10
|
||||
movement_sound = 'sound/weapons/heavysmash.ogg'
|
||||
movement_shake_radius = 5
|
||||
|
||||
taser_kill = FALSE
|
||||
armor = list(
|
||||
"melee" = 40,
|
||||
@@ -49,8 +51,8 @@
|
||||
melee_damage_lower = 22
|
||||
melee_damage_upper = 35
|
||||
attack_armor_pen = 35
|
||||
attack_sharp = 1
|
||||
attack_edge = 1
|
||||
attack_sharp = TRUE
|
||||
attack_edge = TRUE
|
||||
melee_attack_delay = 1 SECOND
|
||||
|
||||
meat_type = /obj/item/weapon/reagent_containers/food/snacks/meat
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
L.visible_message(span("danger", "\The [src] knocks down \the [L]!"))
|
||||
|
||||
// Subtypes.
|
||||
|
||||
// Won't wander away.
|
||||
/mob/living/simple_mob/animal/space/carp/event
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/event
|
||||
|
||||
|
||||
/mob/living/simple_mob/animal/space/carp/large
|
||||
name = "elder carp"
|
||||
desc = "An older, more matured carp. Few survive to this age due to their aggressiveness."
|
||||
@@ -55,7 +61,7 @@
|
||||
mob_size = MOB_LARGE
|
||||
|
||||
pixel_x = -16
|
||||
old_x = -16
|
||||
default_pixel_x = -16
|
||||
|
||||
meat_amount = 3
|
||||
|
||||
@@ -75,8 +81,8 @@
|
||||
melee_damage_lower = 15 // About 20 DPS.
|
||||
melee_damage_upper = 25
|
||||
|
||||
old_y = -16
|
||||
pixel_y = -16
|
||||
default_pixel_x = -16
|
||||
|
||||
meat_amount = 10
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
continue
|
||||
if(!IIsAlly(S)) // Only friendly spores make us stronger.
|
||||
continue
|
||||
// Friendly spores contribute half of their averaged attack power to our attack.
|
||||
damage_to_do += ((S.melee_damage_lower + S.melee_damage_upper) / 2) / 2
|
||||
// Friendly spores contribute 1/4th of their averaged attack power to our attack.
|
||||
damage_to_do += ((S.melee_damage_lower + S.melee_damage_upper) / 2) / 4
|
||||
helpers++
|
||||
|
||||
if(helpers)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
poison_resist = 1
|
||||
|
||||
movement_cooldown = 0
|
||||
mob_bump_flag = 0 // If the illusion can't be swapped it will be obvious.
|
||||
|
||||
response_help = "pushes a hand through"
|
||||
response_disarm = "tried to disarm"
|
||||
@@ -31,6 +32,7 @@
|
||||
return FALSE
|
||||
appearance = thing_to_copy.appearance
|
||||
copying = thing_to_copy
|
||||
density = thing_to_copy.density // So you can't bump into objects that aren't supposed to be dense.
|
||||
return TRUE
|
||||
|
||||
// Because we can't perfectly duplicate some examine() output, we directly examine the AM it is copying. It's messy but
|
||||
|
||||
@@ -68,4 +68,10 @@
|
||||
// Difference is that it should not be faster than you.
|
||||
/mob/living/simple_mob/mechanical/combat_drone/lesser
|
||||
desc = "An automated combat drone with an aged apperance."
|
||||
movement_cooldown = 10
|
||||
movement_cooldown = 10
|
||||
|
||||
|
||||
// This one is the type spawned by the random event.
|
||||
// It won't wander away from its spawn point
|
||||
/mob/living/simple_mob/mechanical/combat_drone/event
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/ranged/kiting/threatening/event
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
icon = 'icons/mob/hivebot.dmi'
|
||||
icon_state = "basic"
|
||||
icon_living = "basic"
|
||||
icon_dead = "basic"
|
||||
|
||||
faction = "hivebot"
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
/mob/living/simple_mob/mechanical/hivebot/ranged_damage/ion
|
||||
name = "ionic hivebot"
|
||||
desc = "A robot with an electromagnetic pulse projector."
|
||||
icon_state = "yellow"
|
||||
icon_living = "yellow"
|
||||
|
||||
projectiletype = /obj/item/projectile/ion
|
||||
projectilesound = 'sound/weapons/Laser.ogg'
|
||||
player_msg = "You have a <b>ranged ion attack</b>, which is very strong against other synthetics.<br>\
|
||||
@@ -58,6 +61,9 @@
|
||||
/mob/living/simple_mob/mechanical/hivebot/ranged_damage/dot
|
||||
name = "ember hivebot"
|
||||
desc = "A robot that appears to utilize fire to cook their enemies."
|
||||
icon_state = "red"
|
||||
icon_living = "red"
|
||||
|
||||
projectiletype = /obj/item/projectile/fire
|
||||
heat_resist = 1
|
||||
player_msg = "Your attacks inflict a <b>damage over time</b> effect, that will \
|
||||
@@ -100,6 +106,9 @@
|
||||
desc = "A large robot capable of delivering long range bombardment."
|
||||
projectiletype = /obj/item/projectile/arc/test
|
||||
icon_scale = 2
|
||||
icon_state = "red"
|
||||
icon_living = "red"
|
||||
|
||||
player_msg = "You are capable of firing <b>very long range bombardment attacks</b>.<br>\
|
||||
To use, click on a tile or enemy at a long range. Note that the projectile arcs in the air, \
|
||||
so it will fly over everything inbetween you and the target.<br>\
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// These hivebots help their team in various ways, and can be very powerful with allies, but are otherwise very weak when alone.
|
||||
|
||||
/mob/living/simple_mob/mechanical/hivebot/support
|
||||
icon_state = "white"
|
||||
icon_living = "white"
|
||||
attacktext = list("prodded")
|
||||
movement_cooldown = 5
|
||||
melee_damage_lower = 2
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
special_attack_max_range = 7
|
||||
special_attack_cooldown = 10 SECONDS
|
||||
projectiletype = /obj/item/projectile/force_missile
|
||||
projectilesound = 'sound/weapons/wave.ogg'
|
||||
var/obj/effect/overlay/energy_ball/energy_ball = null
|
||||
|
||||
/mob/living/simple_mob/mechanical/mecha/combat/gygax/dark/advanced/Destroy()
|
||||
@@ -71,14 +72,23 @@
|
||||
for(var/i = 1 to 10)
|
||||
energy_ball.adjust_scale(0.5 + (i/10))
|
||||
energy_ball.set_light(i/2, i/2, "#0000FF")
|
||||
for(var/mob/living/L in range(3, src))
|
||||
if(L == src)
|
||||
continue
|
||||
if(L.stat)
|
||||
continue // Otherwise it can get pretty laggy if there's loads of corpses around.
|
||||
L.inflict_shock_damage(i * 2)
|
||||
if(L && L.has_AI()) // Some mobs delete themselves when dying.
|
||||
L.ai_holder.react_to_attack(src)
|
||||
for(var/thing in range(3, src))
|
||||
// This is stupid because mechs are stupid and not mobs.
|
||||
if(isliving(thing))
|
||||
var/mob/living/L = thing
|
||||
|
||||
if(L == src)
|
||||
continue
|
||||
if(L.stat)
|
||||
continue // Otherwise it can get pretty laggy if there's loads of corpses around.
|
||||
L.inflict_shock_damage(i * 2)
|
||||
if(L && L.has_AI()) // Some mobs delete themselves when dying.
|
||||
L.ai_holder.react_to_attack(src)
|
||||
|
||||
else if(istype(thing, /obj/mecha))
|
||||
var/obj/mecha/M = thing
|
||||
M.take_damage(i * 2, "energy") // Mechs don't have a concept for siemens so energy armor check is the best alternative.
|
||||
|
||||
sleep(1 SECOND)
|
||||
|
||||
// Shoot a tesla bolt, and flashes people who are looking at the mecha without sufficent eye protection.
|
||||
@@ -107,7 +117,7 @@
|
||||
set waitfor = FALSE
|
||||
|
||||
// Telegraph our next move.
|
||||
Beam(target, icon_state = "sat_beam", time = 3.5 SECONDS)
|
||||
Beam(target, icon_state = "sat_beam", time = 3.5 SECONDS, maxdistance = INFINITY)
|
||||
visible_message(span("warning", "\The [src] deploys a missile rack!"))
|
||||
playsound(src, 'sound/effects/turret/move1.wav', 50, 1)
|
||||
sleep(0.5 SECONDS)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
/mob/living/simple_mob/mechanical/ward/monitor
|
||||
desc = "It's a little flying drone. This one seems to be watching you..."
|
||||
icon_state = "ward"
|
||||
glow_color = "#00FF00"
|
||||
see_invisible = SEE_INVISIBLE_LEVEL_TWO
|
||||
|
||||
has_eye_glow = TRUE
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
movement_cooldown = 0
|
||||
hovering = TRUE
|
||||
|
||||
pass_flags = PASSTABLE
|
||||
mob_swap_flags = 0
|
||||
mob_push_flags = 0
|
||||
mob_bump_flag = 0
|
||||
|
||||
melee_damage_lower = 0
|
||||
melee_damage_upper = 0
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
movement_cooldown = 10
|
||||
melee_attack_delay = 0.5 SECONDS
|
||||
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/ranged/pointblank
|
||||
|
||||
|
||||
// Slimebatoning/xenotasing it just makes it mad at you (which can be good if you're heavily armored and your friends aren't).
|
||||
/mob/living/simple_mob/slime/feral/slimebatoned(mob/living/user, amount)
|
||||
@@ -46,9 +48,10 @@
|
||||
cold_damage_per_tick = 0
|
||||
|
||||
projectiletype = /obj/item/projectile/icicle
|
||||
base_attack_cooldown = 3 SECONDS
|
||||
base_attack_cooldown = 2 SECONDS
|
||||
ranged_attack_delay = 1 SECOND
|
||||
|
||||
player_msg = "You can fire an icicle projectile every three seconds. It hits hard, and armor has a hard time resisting it.<br>\
|
||||
player_msg = "You can fire an icicle projectile every two seconds. It hits hard, and armor has a hard time resisting it.<br>\
|
||||
You are also immune to the cold, and you cause enemies around you to suffer periodic harm from the cold, if unprotected.<br>\
|
||||
Unprotected enemies are also Chilled, making them slower, less evasive, and to suffer disabling effects for longer."
|
||||
|
||||
@@ -68,7 +71,7 @@
|
||||
return ..()
|
||||
|
||||
/obj/item/projectile/icicle/get_structure_damage()
|
||||
return 0 // They're really deadly against mobs, but not walls.
|
||||
return damage / 2 // They're really deadly against mobs, but less effective against solid things.
|
||||
|
||||
/mob/living/simple_mob/slime/feral/dark_blue/handle_special()
|
||||
if(stat != DEAD)
|
||||
|
||||
@@ -97,24 +97,22 @@
|
||||
/mob/living/simple_mob/slime/update_icon()
|
||||
..() // Do the regular stuff first.
|
||||
|
||||
var/mutable_appearance/MA = new(src)
|
||||
|
||||
if(stat != DEAD)
|
||||
// General slime shine.
|
||||
var/image/I = image(icon, src, "slime light")
|
||||
I.appearance_flags = RESET_COLOR
|
||||
MA.overlays += I
|
||||
add_overlay(I)
|
||||
|
||||
// 'Shiny' overlay, for gemstone-slimes.
|
||||
if(shiny)
|
||||
I = image(icon, src, "slime shiny")
|
||||
I.appearance_flags = RESET_COLOR
|
||||
MA.overlays += I
|
||||
add_overlay(I)
|
||||
|
||||
// Mood overlay.
|
||||
I = image(icon, src, "aslime-[mood]")
|
||||
I.appearance_flags = RESET_COLOR
|
||||
MA.overlays += I
|
||||
add_overlay(I)
|
||||
|
||||
// Hat simulator.
|
||||
if(hat)
|
||||
@@ -122,9 +120,7 @@
|
||||
var/image/I = image('icons/mob/head.dmi', src, hat_state)
|
||||
I.pixel_y = -7 // Slimes are small.
|
||||
I.appearance_flags = RESET_COLOR
|
||||
MA.overlays += I
|
||||
|
||||
appearance = MA
|
||||
add_overlay(I)
|
||||
|
||||
// Controls the 'mood' overlay. Overrided in subtypes for specific behaviour.
|
||||
/mob/living/simple_mob/slime/proc/update_mood()
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
shock_resist = 1
|
||||
|
||||
projectiletype = /obj/item/projectile/beam/lightning/slime
|
||||
projectilesound = 'sound/weapons/gauss_shoot.ogg' // Closest thing to a 'thunderstrike' sound we have.
|
||||
projectilesound = 'sound/effects/lightningbolt.ogg'
|
||||
glow_toggle = TRUE
|
||||
|
||||
description_info = "In addition to being immune to electrical shocks, this slime will fire ranged lightning attacks at \
|
||||
@@ -165,6 +165,7 @@
|
||||
|
||||
/obj/item/projectile/beam/lightning/slime
|
||||
power = 10
|
||||
fire_sound = 'sound/effects/lightningbolt.ogg'
|
||||
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/dark_purple
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
..() //extend the zap
|
||||
explode()
|
||||
|
||||
/obj/mecha/tesla_act(power)
|
||||
..()
|
||||
take_damage(power / 200, "energy") // A surface lightning strike will do 100 damage.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
user.setClickCooldown(user.get_attack_speed(W))
|
||||
..()
|
||||
|
||||
/obj/effect/energy_field/attack_generic(mob/user, damage)
|
||||
if(damage)
|
||||
adjust_strength(-damage / 20)
|
||||
user.do_attack_animation(src)
|
||||
user.setClickCooldown(user.get_attack_speed())
|
||||
|
||||
/obj/effect/energy_field/attack_hand(var/mob/living/user)
|
||||
impact_effect(3) // Harmless, but still produces the 'impact' effect.
|
||||
..()
|
||||
|
||||
Reference in New Issue
Block a user