mirror of
https://github.com/PolarisSS13/Polaris.git
synced 2025-12-17 13:42:44 +00:00
Begins work on new AI.
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
#define BORGTHERM 0x2
|
||||
#define BORGXRAY 0x4
|
||||
#define BORGMATERIAL 8
|
||||
|
||||
/*
|
||||
#define STANCE_IDLE 1 // Looking for targets if hostile. Does idle wandering.
|
||||
#define STANCE_ALERT 2 // Bears
|
||||
#define STANCE_ATTACK 3 // Attempting to get into attack position
|
||||
@@ -33,6 +33,17 @@
|
||||
#define STANCE_TIRED 5 // Bears
|
||||
#define STANCE_FOLLOW 6 // Following somone
|
||||
#define STANCE_BUSY 7 // Do nothing on life ticks (Other code is running)
|
||||
*/
|
||||
#define STANCE_SLEEP 0 // Doing (almost) nothing, to save on CPU because nobody is around to notice or the mob died.
|
||||
#define STANCE_IDLE 1 // The more or less default state. Wanders around, looks for baddies, and spouts one-liners.
|
||||
#define STANCE_ALERT 2 // A baddie is visible but not too close, and essentially we tell them to go away or die.
|
||||
#define STANCE_ATTACK 3 // Attempting to get into range to attack them.
|
||||
#define STANCE_ATTACKING 4 // Actually fighting, with melee or ranged.
|
||||
#define STANCE_REPOSITION 5 // Relocating to a better position while in combat. Only used for ranged mobs since melee only has one better position, which STANCE_ATTACK already handles.
|
||||
#define STANCE_MOVE 6 // Similar to above but for out of combat. If a baddie is seen, they'll cancel and fight them.
|
||||
#define STANCE_FOLLOW 7 // Following somone, without trying to murder them.
|
||||
#define STANCE_FLEE 8 // Run away from the target because they're too spooky/we're dying/some other reason.
|
||||
#define STANCE_STUNNED 9 // Do nothing, because the mob is unable to act in some form. Can be applied by other disabling effects besides stuns.
|
||||
|
||||
#define LEFT 0x1
|
||||
#define RIGHT 0x2
|
||||
|
||||
30
code/controllers/subsystems/ai.dm
Normal file
30
code/controllers/subsystems/ai.dm
Normal file
@@ -0,0 +1,30 @@
|
||||
SUBSYSTEM_DEF(ai)
|
||||
name = "AI"
|
||||
priority = 9
|
||||
wait = 5 // This gets run twice a second, however this is technically two loops in one, with the second loop being run every four iterations.
|
||||
flags = SS_NO_INIT|SS_TICKER
|
||||
|
||||
var/list/processing = list()
|
||||
var/list/currentrun = list()
|
||||
|
||||
/datum/controller/subsystem/ai/stat_entry()
|
||||
..("P:[processing.len]")
|
||||
|
||||
/datum/controller/subsystem/ai/fire(resumed = 0)
|
||||
if (!resumed)
|
||||
src.currentrun = processing.Copy()
|
||||
|
||||
//cache for sanic speed (lists are references anyways)
|
||||
var/list/currentrun = src.currentrun
|
||||
|
||||
while(currentrun.len)
|
||||
// var/mob/living/L = currentrun[currentrun.len]
|
||||
var/datum/ai_holder/A = currentrun[currentrun.len]
|
||||
--currentrun.len
|
||||
if(times_fired % 4 == 0 && A.holder.stat != DEAD)
|
||||
A.handle_strategicals()
|
||||
if(A.holder.stat != DEAD) // The /TG/ version checks stat twice, presumably in-case processing somehow got the mob killed in that instant.
|
||||
A.handle_tactics()
|
||||
|
||||
if(MC_TICK_CHECK)
|
||||
return
|
||||
@@ -47,6 +47,7 @@
|
||||
var/check_synth = 0 //if active, will shoot at anything not an AI or cyborg
|
||||
var/check_all = 0 //If active, will fire on anything, including synthetics.
|
||||
var/ailock = 0 // AI cannot use this
|
||||
var/faction = null //if set, will not fire at people in the same faction for any reason.
|
||||
|
||||
var/attacked = 0 //if set to 1, the turret gets pissed off and shoots at people nearby (unless they have sec access!)
|
||||
|
||||
@@ -80,6 +81,11 @@
|
||||
lethal = 1
|
||||
installation = /obj/item/weapon/gun/energy/laser
|
||||
|
||||
/obj/machinery/porta_turret/stationary/syndie // Generic turrets for POIs that need to not shoot their buddies.
|
||||
enabled = TRUE
|
||||
check_all = TRUE
|
||||
faction = "syndicate" // Make sure this equals the faction that the mobs in the POI have or they will fight each other.
|
||||
|
||||
/obj/machinery/porta_turret/ai_defense
|
||||
name = "defense turret"
|
||||
desc = "This variant appears to be much more durable."
|
||||
@@ -533,6 +539,9 @@ var/list/turret_icons
|
||||
if(!L)
|
||||
return TURRET_NOT_TARGET
|
||||
|
||||
if(faction && L.faction == faction)
|
||||
return TURRET_NOT_TARGET
|
||||
|
||||
if(!emagged && issilicon(L) && check_all == 0) // Don't target silica, unless told to neutralize everything.
|
||||
return TURRET_NOT_TARGET
|
||||
|
||||
|
||||
3
code/modules/ai/_defines.dm
Normal file
3
code/modules/ai/_defines.dm
Normal file
@@ -0,0 +1,3 @@
|
||||
#define AI_DUMB 1
|
||||
#define AI_NORMAL 2
|
||||
#define AI_SMART 3
|
||||
186
code/modules/ai/ai_holder.dm
Normal file
186
code/modules/ai/ai_holder.dm
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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
|
||||
|
||||
|
||||
|
||||
/datum/ai_holder/hostile
|
||||
hostile = TRUE
|
||||
|
||||
/datum/ai_holder/retaliate
|
||||
hostile = TRUE
|
||||
retaliate = TRUE
|
||||
|
||||
/datum/ai_holder/test
|
||||
hostile = TRUE
|
||||
use_astar = TRUE
|
||||
|
||||
/datum/ai_holder/New(var/new_holder)
|
||||
ASSERT(new_holder)
|
||||
holder = new_holder
|
||||
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.
|
||||
|
||||
/datum/ai_holder/proc/go_sleep()
|
||||
if(stance == STANCE_SLEEP)
|
||||
return
|
||||
stance = STANCE_SLEEP
|
||||
SSai.processing -= src
|
||||
|
||||
/datum/ai_holder/proc/go_wake()
|
||||
if(stance != STANCE_SLEEP)
|
||||
return
|
||||
stance = STANCE_IDLE
|
||||
SSai.processing += src
|
||||
|
||||
|
||||
// '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()
|
||||
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()
|
||||
world << "[holder.name] Strategicals!"
|
||||
handle_stance_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)
|
||||
stance = new_stance
|
||||
|
||||
/datum/ai_holder/proc/handle_stance_tactical(var/new_stance)
|
||||
if(new_stance)
|
||||
set_stance(new_stance)
|
||||
|
||||
if(attack_cooldown_left > 0)
|
||||
attack_cooldown_left--
|
||||
|
||||
switch(stance)
|
||||
if(STANCE_SLEEP)
|
||||
go_sleep()
|
||||
return
|
||||
if(STANCE_IDLE)
|
||||
holder.a_intent = I_HELP
|
||||
|
||||
// if(hostile)
|
||||
// find_target()
|
||||
if(STANCE_ALERT)
|
||||
threaten_target()
|
||||
|
||||
if(STANCE_ATTACK)
|
||||
if(target)
|
||||
walk_to_target()
|
||||
|
||||
if(STANCE_ATTACKING)
|
||||
engage_target()
|
||||
|
||||
/datum/ai_holder/proc/handle_stance_strategical()
|
||||
switch(stance)
|
||||
if(STANCE_IDLE)
|
||||
if(hostile)
|
||||
find_target()
|
||||
if(STANCE_ATTACK)
|
||||
if(target)
|
||||
calculate_path(target)
|
||||
|
||||
/*
|
||||
// For proccessing the current stance, or setting and processing a new one
|
||||
/mob/living/simple_animal/proc/handle_stance(var/new_stance)
|
||||
if(ai_inactive)
|
||||
stance = STANCE_IDLE
|
||||
return
|
||||
|
||||
if(new_stance)
|
||||
set_stance(new_stance)
|
||||
|
||||
switch(stance)
|
||||
if(STANCE_IDLE)
|
||||
target_mob = null
|
||||
a_intent = I_HELP
|
||||
annoyed = max(0,annoyed--)
|
||||
|
||||
//Yes I'm breaking this into two if()'s for ease of reading
|
||||
//If we ARE ALLOWED TO
|
||||
if(returns_home && home_turf && !astarpathing && (world.time - stance_changed) > 10 SECONDS)
|
||||
if(get_dist(src,home_turf) > wander_distance)
|
||||
move_to_delay = initial(move_to_delay)*2 //Walk back.
|
||||
GoHome()
|
||||
else
|
||||
stop_automated_movement = 0
|
||||
|
||||
//Search for targets while idle
|
||||
if(hostile)
|
||||
FindTarget()
|
||||
if(STANCE_FOLLOW)
|
||||
annoyed = 15
|
||||
FollowTarget()
|
||||
if(follow_until_time && world.time > follow_until_time)
|
||||
LoseFollow()
|
||||
return
|
||||
if(hostile)
|
||||
FindTarget()
|
||||
if(STANCE_ATTACK)
|
||||
annoyed = 50
|
||||
a_intent = I_HURT
|
||||
RequestHelp()
|
||||
MoveToTarget()
|
||||
if(STANCE_ATTACKING)
|
||||
annoyed = 50
|
||||
AttackTarget()
|
||||
*/
|
||||
|
||||
|
||||
211
code/modules/ai/ai_holder_combat.dm
Normal file
211
code/modules/ai/ai_holder_combat.dm
Normal file
@@ -0,0 +1,211 @@
|
||||
/datum/ai_holder
|
||||
var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing.
|
||||
var/retaliate = FALSE // Requires hostile to be true to work. If both this and hostile are true, the mob won't attack unless attacked first.
|
||||
var/cooperative = FALSE // If true, asks allies to help when fighting something.
|
||||
var/firing_lanes = FALSE // If ture, tries to refrain from shooting allies or the wall.
|
||||
var/conserve_ammo = FALSE // If true, the mob will avoid shooting anything that does not have a chance to hit a mob. Requires firing_lanes to be true.
|
||||
|
||||
var/atom/movable/target // The thing (mob or object) we're trying to kill.
|
||||
var/attack_cooldown = 2 // If set, the mob will wait for the specified amount of ticks before attempting another attack.
|
||||
var/attack_cooldown_left = 0 // Actual var for tracking if attacks are off cooldown or not. Note that melee and ranged attacks share this.
|
||||
|
||||
var/ranged = FALSE // If true, attempts to shoot at the enemy instead of charging at them wildly.
|
||||
var/shoot_range = 5 // How close the mob needs to be to attempt to shoot at the enemy.
|
||||
var/pointblank = FALSE // If ranged is true, and this is true, people adjacent to the mob will suffer the ranged instead of using a melee attack.
|
||||
|
||||
var/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.
|
||||
|
||||
/**************
|
||||
* Strategical *
|
||||
**************/
|
||||
|
||||
// Step 1, find out what we can see.
|
||||
/datum/ai_holder/proc/list_targets()
|
||||
. = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
|
||||
|
||||
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
|
||||
|
||||
for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
|
||||
if(can_see(holder, HM, vision_range))
|
||||
. += HM
|
||||
|
||||
// Step 2, filter down possible targets to things we actually care about.
|
||||
/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
|
||||
. = list()
|
||||
if(!has_targets_list)
|
||||
possible_targets = list_targets()
|
||||
for(var/possible_target in possible_targets)
|
||||
var/atom/A = possible_target
|
||||
// if(Found(A)) // In case people want to override this.
|
||||
// . = list(A)
|
||||
// break
|
||||
if(can_attack(A)) // Can we attack it?
|
||||
. += A
|
||||
continue
|
||||
|
||||
var/new_target = pick_target(.)
|
||||
give_target(new_target)
|
||||
return new_target
|
||||
|
||||
// Step 3, pick among the possible, attackable targets.
|
||||
/datum/ai_holder/proc/pick_target(list/targets)
|
||||
if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
|
||||
for(var/possible_target in targets)
|
||||
var/atom/A = possible_target
|
||||
var/target_dist = get_dist(holder, target)
|
||||
var/possible_target_distance = get_dist(holder, A)
|
||||
if(target_dist < possible_target_distance)
|
||||
targets -= A
|
||||
if(!targets.len) // We found nothing.
|
||||
return
|
||||
var/chosen_target = pick(targets)
|
||||
return chosen_target
|
||||
|
||||
// Step 4, give us our selected target.
|
||||
/datum/ai_holder/proc/give_target(new_target)
|
||||
target = new_target
|
||||
//LosePatience()
|
||||
if(target != null)
|
||||
//GainPatience()
|
||||
//Aggro()
|
||||
if(should_threaten())
|
||||
set_stance(STANCE_ALERT)
|
||||
else
|
||||
set_stance(STANCE_ATTACK)
|
||||
return TRUE
|
||||
|
||||
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
|
||||
if(!the_target) // Nothing to attack.
|
||||
return FALSE
|
||||
|
||||
if(holder.see_invisible < the_target.invisibility) // Invisible, can't see it, oh well.
|
||||
return FALSE
|
||||
|
||||
if(isliving(the_target))
|
||||
var/mob/living/L = the_target
|
||||
if(L.stat)
|
||||
return FALSE
|
||||
if(holder.IIsAlly(L))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
if(istype(the_target, /obj/mecha))
|
||||
var/obj/mecha/M = the_target
|
||||
if(M.occupant)
|
||||
return can_attack(M.occupant)
|
||||
|
||||
if(istype(the_target, /obj/machinery/porta_turret))
|
||||
var/obj/machinery/porta_turret/P = the_target
|
||||
if(P.stat & BROKEN)
|
||||
return FALSE // Already dead.
|
||||
if(P.faction == holder.faction)
|
||||
return FALSE // Don't shoot allied turrets.
|
||||
if(!P.raised && !P.raising)
|
||||
return FALSE // Turrets won't get hurt if they're still in their cover.
|
||||
return TRUE
|
||||
|
||||
return FALSE
|
||||
|
||||
/***********
|
||||
* Tactical *
|
||||
***********/
|
||||
|
||||
// This does the actual attacking.
|
||||
/datum/ai_holder/proc/engage_target()
|
||||
if(!target || !can_attack(target) || (!(target in list_targets())) )
|
||||
lose_target()
|
||||
return
|
||||
|
||||
var/distance = get_dist(holder, target)
|
||||
holder.face_atom(target)
|
||||
last_conflict_time = world.time
|
||||
|
||||
// Stab them.
|
||||
if(distance <= 1 && !pointblank)
|
||||
on_engagement(target)
|
||||
if(attack_cooldown_left <= 0)
|
||||
|
||||
pre_melee_attack(target)
|
||||
|
||||
if(melee_attack(target))
|
||||
post_melee_attack(target)
|
||||
if(attack_cooldown)
|
||||
attack_cooldown_left = attack_cooldown
|
||||
|
||||
// Shoot them.
|
||||
else if(ranged && (distance <= shoot_range) )
|
||||
on_engagement(target)
|
||||
if(attack_cooldown_left <= 0)
|
||||
|
||||
if(firing_lanes && !test_projectile_safety(target))
|
||||
// Nudge them a bit, maybe they can shoot next time.
|
||||
step_rand(holder)
|
||||
holder.face_atom(target)
|
||||
return
|
||||
|
||||
pre_ranged_attack(target)
|
||||
|
||||
if(ranged_attack(target))
|
||||
post_ranged_attack(target)
|
||||
if(attack_cooldown)
|
||||
attack_cooldown_left = attack_cooldown
|
||||
|
||||
// Run after them.
|
||||
else
|
||||
set_stance(STANCE_ATTACK)
|
||||
|
||||
// We're not entirely sure how holder will do melee attacks since any /mob/living could be holder, but we don't have to care because Interfaces.
|
||||
/datum/ai_holder/proc/melee_attack(atom/movable/AM)
|
||||
return holder.IAttack(AM)
|
||||
|
||||
// Ditto.
|
||||
/datum/ai_holder/proc/ranged_attack(atom/movable/AM)
|
||||
return holder.IRangedAttack(AM)
|
||||
|
||||
// Called when within striking distance, however cooldown is not considered.
|
||||
/datum/ai_holder/proc/on_engagement(atom/movable/AM)
|
||||
|
||||
// These two are called before an attack attempt.
|
||||
/datum/ai_holder/proc/pre_melee_attack(atom/movable/AM)
|
||||
|
||||
/datum/ai_holder/proc/pre_ranged_attack(atom/movable/AM)
|
||||
|
||||
// These two are called after a successful(ish) attack.
|
||||
/datum/ai_holder/proc/post_melee_attack(atom/movable/AM)
|
||||
|
||||
/datum/ai_holder/proc/post_ranged_attack(atom/movable/AM)
|
||||
|
||||
// Used to make sure projectiles will probably hit the target and not the wall or a friend.
|
||||
/datum/ai_holder/proc/test_projectile_safety(atom/movable/AM)
|
||||
var/mob/living/L = check_trajectory(AM, holder) // This isn't always reliable but its better than the previous method.
|
||||
// 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.
|
||||
|
||||
//We can't see the target
|
||||
/datum/ai_holder/proc/lose_target()
|
||||
target = null
|
||||
set_stance(STANCE_IDLE)
|
||||
give_up_movement()
|
||||
|
||||
//Target is no longer valid (?)
|
||||
/datum/ai_holder/proc/lost_target()
|
||||
set_stance(STANCE_IDLE)
|
||||
give_up_movement()
|
||||
|
||||
// Can be used to conditionally do a ranged or melee attack.
|
||||
/datum/ai_holder/proc/closest_distance()
|
||||
return ranged ? shoot_range - 1 : 1 // Shoot range -1 just because we don't want to constantly get kited
|
||||
59
code/modules/ai/ai_holder_communication.dm
Normal file
59
code/modules/ai/ai_holder_communication.dm
Normal file
@@ -0,0 +1,59 @@
|
||||
/datum/ai_holder
|
||||
var/threaten = FALSE // If hostile and sees a valid target, gives a 'warning' to the target before beginning the attack.
|
||||
var/threatened = FALSE // If the mob actually gave the warning, checked so it doesn't constantly yell every tick.
|
||||
var/threaten_delay = 3 SECONDS // How long a 'threat' lasts, until actual fighting starts. If null, the mob never starts the fight but still does the threat.
|
||||
var/threaten_timeout = 1 MINUTE // If the mob threatens someone, they leave, and then come back before this timeout period, the mob escalates to fighting immediately.
|
||||
var/last_conflict_time = null // Last occurance of threatening or fighting being used, in world.time.
|
||||
|
||||
var/threaten_sound = null // Sound file played when the mob calls threaten_target() for the first time.
|
||||
var/stand_down_sound = null // Sound file played when the mob loses sight of the threatened target.
|
||||
|
||||
|
||||
/datum/ai_holder/proc/should_threaten()
|
||||
if(!threaten)
|
||||
return FALSE // We don't negotiate.
|
||||
if(!will_threaten(target))
|
||||
return FALSE // Pointless to threaten an animal, a mindless drone, or an object.
|
||||
if(!(stance in list(STANCE_IDLE, STANCE_MOVE, STANCE_FOLLOW)))
|
||||
return FALSE // We're probably already fighting or recently fought if not in these stances.
|
||||
if(last_conflict_time && threaten_delay && last_conflict_time + threaten_timeout > world.time)
|
||||
return FALSE // We threatened someone recently, so lets show them we mean business.
|
||||
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(!threatened) // First tick.
|
||||
threatened = TRUE
|
||||
last_conflict_time = world.time
|
||||
//TODO: Actual speech.
|
||||
holder.say("Oi, fuck off mate.")
|
||||
playsound(holder.loc, threaten_sound, 75, 1) // We do this twice to make the sound -very- noticable to the target.
|
||||
playsound(target.loc, threaten_sound, 75, 1) // Actual aim-mode also does that so at least it's consistant.
|
||||
else // Otherwise we are waiting for them to go away or to wait long enough for escalate.
|
||||
if(target in list_targets()) // Are they still visible?
|
||||
|
||||
if(threaten_delay && last_conflict_time + threaten_delay < world.time) // Waited too long.
|
||||
threatened = FALSE
|
||||
set_stance(STANCE_ATTACK)
|
||||
holder.say("Fine, now you die!") //WIP
|
||||
else
|
||||
return // Wait a bit.
|
||||
|
||||
else // They left, or so we think.
|
||||
threatened = FALSE
|
||||
set_stance(STANCE_IDLE)
|
||||
holder.say("Good riddence.") //WIP
|
||||
playsound(holder.loc, stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target.
|
||||
playsound(target.loc, stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant.
|
||||
|
||||
// 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.
|
||||
if(istype(the_target, /mob/living/simple_animal) && istype(holder, /mob/living/simple_animal))
|
||||
var/mob/living/simple_animal/us = holder
|
||||
var/mob/living/simple_animal/them = target
|
||||
if(them.intelligence_level < us.intelligence_level) // Todo: Bitflag these.
|
||||
return FALSE // Humanoids don't care about drones/animals/etc. Drones don't care about animals, and so on.
|
||||
return TRUE
|
||||
39
code/modules/ai/ai_holder_debug.dm
Normal file
39
code/modules/ai/ai_holder_debug.dm
Normal file
@@ -0,0 +1,39 @@
|
||||
/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
|
||||
|
||||
/datum/ai_holder/New()
|
||||
..()
|
||||
path_overlay = new(path_icon,path_icon_state)
|
||||
|
||||
/datum/ai_holder/Destroy()
|
||||
path_overlay = null
|
||||
return ..()
|
||||
|
||||
|
||||
|
||||
// Remove this when finished.
|
||||
|
||||
/mob/living/simple_animal/corgi
|
||||
ai_holder_type = /datum/ai_holder/test
|
||||
|
||||
/datum/ai_holder/hostile/ranged
|
||||
ranged = TRUE
|
||||
cooperative = TRUE
|
||||
firing_lanes = TRUE
|
||||
conserve_ammo = TRUE
|
||||
threaten = TRUE
|
||||
|
||||
threaten_sound = 'sound/weapons/TargetOn.ogg'
|
||||
stand_down_sound = 'sound/weapons/TargetOff.ogg'
|
||||
|
||||
/mob/living/simple_animal/hostile/pirate
|
||||
hostile = FALSE
|
||||
ai_inactive = TRUE
|
||||
ai_holder_type = /datum/ai_holder/hostile/ranged
|
||||
|
||||
/datum/ai_holder/hostile/ranged/robust/on_engagement(atom/movable/AM)
|
||||
step_rand(holder)
|
||||
holder.face_atom(AM)
|
||||
159
code/modules/ai/ai_holder_movement.dm
Normal file
159
code/modules/ai/ai_holder_movement.dm
Normal file
@@ -0,0 +1,159 @@
|
||||
/datum/ai_holder
|
||||
var/use_astar = FALSE // Do we use the more expensive A* implementation or stick with BYOND's default step_to()?
|
||||
var/using_astar = FALSE // Are we currently using an A* path?
|
||||
var/list/path = list() // A list of tiles that A* gave us as a solution to reach the target.
|
||||
var/list/obstacles = list() // Things A* will try to avoid.
|
||||
var/astar_adjacent_proc = /turf/proc/CardinalTurfsWithAccess // Proc to use when A* pathfinding. Default makes them bound to cardinals.
|
||||
var/failed_steps = 0 // If move_once() fails to move the mob onto the correct tile, this increases. When it reaches 3, the path is recalc'd since they're probably stuck.
|
||||
|
||||
var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so.
|
||||
var/return_home = FALSE // If true, makes the mob go to its 'home' if it strays too far.
|
||||
|
||||
/**************
|
||||
* Strategical *
|
||||
**************/
|
||||
|
||||
//Giving up on moving
|
||||
/datum/ai_holder/proc/give_up_movement()
|
||||
// ai_log("GiveUpMoving()",1)
|
||||
forget_path()
|
||||
// stop_automated_movement = 0
|
||||
|
||||
//Forget the path entirely
|
||||
/datum/ai_holder/proc/forget_path()
|
||||
// ai_log("ForgetPath()",2)
|
||||
if(path_display)
|
||||
for(var/turf/T in path)
|
||||
T.overlays -= path_overlay
|
||||
using_astar = FALSE
|
||||
// walk_list.Cut()
|
||||
path.Cut()
|
||||
|
||||
/datum/ai_holder/proc/calculate_path(atom/A, get_to = 1)
|
||||
if(!A)
|
||||
return
|
||||
|
||||
if(!use_astar) // If we don't use A* then this is pointless.
|
||||
return
|
||||
|
||||
get_path(get_turf(A), get_to)
|
||||
|
||||
//A* now, try to a path to a target
|
||||
/datum/ai_holder/proc/get_path(var/turf/target,var/get_to = 1, var/max_distance = world.view*6)
|
||||
// ai_log("GetPath([target],[get_to],[max_distance])",2)
|
||||
forget_path()
|
||||
var/list/new_path = AStar(get_turf(holder.loc), target, astar_adjacent_proc, /turf/proc/Distance, min_target_dist = get_to, max_node_depth = max_distance, id = holder.IGetID(), exclude = obstacles)
|
||||
|
||||
if(new_path && new_path.len)
|
||||
path = new_path
|
||||
if(path_display)
|
||||
for(var/turf/T in path)
|
||||
T.overlays |= path_overlay
|
||||
else
|
||||
return 0
|
||||
|
||||
return path.len
|
||||
|
||||
/*
|
||||
/datum/ai_holder/proc/walk_to_target()
|
||||
//If we were chasing someone and we can't anymore, give up.
|
||||
if(!target_mob)
|
||||
// ai_log("MoveToTarget() Losing target at top",2)
|
||||
lose_target()
|
||||
return
|
||||
|
||||
//We recompute our path every time we're called if we can still see them
|
||||
if(target in list_targets(vision_range))
|
||||
|
||||
if(using_astar)
|
||||
forget_path()
|
||||
|
||||
// Find out where we're going.
|
||||
var/get_to = 1 // TODO
|
||||
var/distance = get_dist(holder, target)
|
||||
|
||||
//We're here!
|
||||
if(distance <= get_to)
|
||||
// ai_log("MoveToTarget() [src] attack range",2)
|
||||
set_stance(STANCE_ATTACKING)
|
||||
return
|
||||
|
||||
//We're just setting out, making a new path, or we can't path with A*
|
||||
if(!path.len)
|
||||
// ai_log("SA: MoveToTarget() pathing to [target_mob]",2)
|
||||
|
||||
//GetPath failed for whatever reason, just smash into things towards them
|
||||
if(run_at_them || !GetPath(get_turf(target_mob),get_to))
|
||||
|
||||
//We try the built-in way to stay close
|
||||
walk_to(src, target_mob, get_to, move_to_delay)
|
||||
// ai_log("MoveToTarget() walk_to([src],[target_mob],[get_to],[move_to_delay])",3)
|
||||
|
||||
//Break shit in their direction! LEME SMAHSH
|
||||
var/dir_to_mob = get_dir(src,target_mob)
|
||||
face_atom(target_mob)
|
||||
// DestroySurroundings(dir_to_mob)
|
||||
// ai_log("MoveToTarget() DestroySurroundings([get_dir(src,target_mob)])",3)
|
||||
*/
|
||||
|
||||
/***********
|
||||
* Tactical *
|
||||
***********/
|
||||
|
||||
// Goes to the target, to attack them.
|
||||
// Called when in STANCE_ATTACK.
|
||||
/datum/ai_holder/proc/walk_to_target()
|
||||
// Make sure we can still chase/attack them.
|
||||
if(!target || !can_attack(target))
|
||||
lose_target()
|
||||
return
|
||||
|
||||
// Find out where we're going.
|
||||
var/get_to = closest_distance()
|
||||
var/distance = get_dist(holder, target)
|
||||
|
||||
// We're here!
|
||||
if(distance <= get_to)
|
||||
// ai_log("MoveToTarget() [src] attack range",2)
|
||||
forget_path()
|
||||
set_stance(STANCE_ATTACKING)
|
||||
return
|
||||
|
||||
// Otherwise keep walking.
|
||||
walk_path(target, get_to)
|
||||
|
||||
// Walk towards whatever.
|
||||
/datum/ai_holder/proc/walk_path(atom/A, get_to = 1)
|
||||
if(use_astar)
|
||||
if(!path.len) // If we're missing a path, make a new one.
|
||||
calculate_path(A, get_to)
|
||||
|
||||
if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us.
|
||||
return
|
||||
|
||||
if(!move_once()) // Start walking the path.
|
||||
++failed_steps
|
||||
if(failed_steps > 3) // We're probably stuck.
|
||||
forget_path() // So lets try again with a new path.
|
||||
failed_steps = 0
|
||||
|
||||
else
|
||||
step_to(holder, A)
|
||||
|
||||
//Take one step along a path
|
||||
/datum/ai_holder/proc/move_once()
|
||||
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.loc != src.path[1])
|
||||
// ai_log("MoveOnce() step_towards returning 0",3)
|
||||
return FALSE
|
||||
else
|
||||
path -= src.path[1]
|
||||
// ai_log("MoveOnce() step_towards returning 1",3)
|
||||
return TRUE
|
||||
32
code/modules/ai/interfaces.dm
Normal file
32
code/modules/ai/interfaces.dm
Normal file
@@ -0,0 +1,32 @@
|
||||
// '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/movable/AM)
|
||||
|
||||
/mob/living/simple_animal/IAttack(atom/movable/AM)
|
||||
target_mob = AM
|
||||
return PunchTarget(AM) // TODO: Clean up on SA side.
|
||||
|
||||
/mob/living/proc/IRangedAttack(atom/movable/AM)
|
||||
|
||||
/mob/living/simple_animal/IRangedAttack(atom/movable/AM)
|
||||
target_mob = AM
|
||||
return ShootTarget(AM, src.loc, src)
|
||||
|
||||
/mob/living/proc/ISay(message)
|
||||
|
||||
/mob/living/proc/IIsAlly(mob/living/L)
|
||||
return src.faction == L.faction
|
||||
|
||||
/mob/living/simple_animal/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_animal/IGetID()
|
||||
if(myid)
|
||||
return myid.GetID()
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
var/evasion = 0 // Makes attacks harder to land. Each number equals 15% more likely to miss. Negative numbers increase hit chance.
|
||||
var/force_max_speed = 0 // If 1, the mob runs extremely fast and cannot be slowed.
|
||||
|
||||
|
||||
var/image/dsoverlay = null //Overlay used for darksight eye adjustments
|
||||
|
||||
var/glow_toggle = 0 // If they're glowing!
|
||||
|
||||
@@ -470,7 +470,8 @@
|
||||
return 2
|
||||
//just assume we can shoot through glass and stuff. No big deal, the player can just choose to not target someone
|
||||
//on the other side of a window if it makes a difference. Or if they run behind a window, too bad.
|
||||
return check_trajectory(target, user)
|
||||
if(check_trajectory(target, user))
|
||||
return 1 // Magic numbers are fun.
|
||||
|
||||
//called if there was no projectile to shoot
|
||||
/obj/item/weapon/gun/proc/handle_click_empty(mob/user)
|
||||
|
||||
@@ -416,6 +416,7 @@
|
||||
yo = null
|
||||
xo = null
|
||||
var/result = 0 //To pass the message back to the gun.
|
||||
var/atom/movable/result_ref = null // The thing that got hit that made the check return true.
|
||||
|
||||
/obj/item/projectile/test/Bump(atom/A as mob|obj|turf|area)
|
||||
if(A == firer)
|
||||
@@ -426,6 +427,7 @@
|
||||
if(istype(A, /obj/structure/foamedmetal)) //Turrets can detect through foamed metal, but will have to blast through it. Similar to windows, if someone runs behind it, a person should probably just not shoot.
|
||||
return
|
||||
if(istype(A, /mob/living) || istype(A, /obj/mecha) || istype(A, /obj/vehicle))
|
||||
result_ref = A
|
||||
result = 2 //We hit someone, return 1!
|
||||
return
|
||||
result = 1
|
||||
@@ -446,7 +448,8 @@
|
||||
/obj/item/projectile/test/process(var/turf/targloc)
|
||||
while(src) //Loop on through!
|
||||
if(result)
|
||||
return (result - 1)
|
||||
return result_ref
|
||||
// return (result - 1)
|
||||
if((!( targloc ) || loc == targloc))
|
||||
targloc = locate(min(max(x + xo, 1), world.maxx), min(max(y + yo, 1), world.maxy), z) //Finding the target turf at map edge
|
||||
|
||||
@@ -457,11 +460,13 @@
|
||||
|
||||
var/mob/living/M = locate() in get_turf(src)
|
||||
if(istype(M)) //If there is someting living...
|
||||
return 1 //Return 1
|
||||
result_ref = M
|
||||
return result_ref //Return 1
|
||||
else
|
||||
M = locate() in get_step(src,targloc)
|
||||
if(istype(M))
|
||||
return 1
|
||||
result_ref = M
|
||||
return result_ref
|
||||
|
||||
//Helper proc to check if you can hit them or not.
|
||||
/proc/check_trajectory(atom/target as mob|obj, atom/firer as mob|obj, var/pass_flags=PASSTABLE|PASSGLASS|PASSGRILLE, flags=null)
|
||||
|
||||
10
polaris.dme
10
polaris.dme
@@ -171,6 +171,7 @@
|
||||
#include "code\controllers\Processes\vote.dm"
|
||||
#include "code\controllers\ProcessScheduler\core\process.dm"
|
||||
#include "code\controllers\ProcessScheduler\core\processScheduler.dm"
|
||||
#include "code\controllers\subsystems\ai.dm"
|
||||
#include "code\controllers\subsystems\air.dm"
|
||||
#include "code\controllers\subsystems\airflow.dm"
|
||||
#include "code\controllers\subsystems\atoms.dm"
|
||||
@@ -1230,6 +1231,13 @@
|
||||
#include "code\modules\admin\view_variables\helpers.dm"
|
||||
#include "code\modules\admin\view_variables\topic.dm"
|
||||
#include "code\modules\admin\view_variables\view_variables.dm"
|
||||
#include "code\modules\ai\_defines.dm"
|
||||
#include "code\modules\ai\ai_holder.dm"
|
||||
#include "code\modules\ai\ai_holder_combat.dm"
|
||||
#include "code\modules\ai\ai_holder_communication.dm"
|
||||
#include "code\modules\ai\ai_holder_debug.dm"
|
||||
#include "code\modules\ai\ai_holder_movement.dm"
|
||||
#include "code\modules\ai\interfaces.dm"
|
||||
#include "code\modules\alarm\alarm.dm"
|
||||
#include "code\modules\alarm\alarm_handler.dm"
|
||||
#include "code\modules\alarm\atmosphere_alarm.dm"
|
||||
@@ -2404,7 +2412,7 @@
|
||||
#include "code\ZAS\Zone.dm"
|
||||
#include "interface\interface.dm"
|
||||
#include "interface\skin.dmf"
|
||||
#include "maps\southern_cross\southern_cross.dm"
|
||||
#include "maps\example\example.dm"
|
||||
#include "maps\submaps\_readme.dm"
|
||||
#include "maps\submaps\space_submaps\space.dm"
|
||||
#include "maps\submaps\surface_submaps\mountains\mountains.dm"
|
||||
|
||||
Reference in New Issue
Block a user