mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 10:43:20 +00:00
Fixes several bugs with AI mobs
This commit is contained in:
141
code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
Normal file
141
code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm
Normal file
@@ -0,0 +1,141 @@
|
||||
// Base AIs for simple mobs.
|
||||
// Mob-specific AIs are in their mob's file.
|
||||
|
||||
/datum/ai_holder/simple_mob
|
||||
hostile = TRUE // The majority of simplemobs are hostile.
|
||||
cooperative = TRUE
|
||||
returns_home = FALSE
|
||||
can_flee = FALSE
|
||||
speak_chance = 1 // If the mob's saylist is empty, nothing will happen.
|
||||
wander = TRUE
|
||||
base_wander_delay = 4
|
||||
|
||||
// For non-hostile animals, and pets like Ian and Runtime.
|
||||
/datum/ai_holder/simple_mob/passive
|
||||
hostile = FALSE
|
||||
can_flee = TRUE
|
||||
violent_breakthrough = FALSE
|
||||
|
||||
// Won't wander away as quickly, ideal for event-spawned mobs like carp or drones.
|
||||
/datum/ai_holder/simple_mob/event
|
||||
base_wander_delay = 8
|
||||
|
||||
// Doesn't really act until told to by something on the outside.
|
||||
/datum/ai_holder/simple_mob/inert
|
||||
hostile = FALSE
|
||||
retaliate = FALSE
|
||||
can_flee = FALSE
|
||||
wander = FALSE
|
||||
speak_chance = 0
|
||||
cooperative = FALSE
|
||||
violent_breakthrough = FALSE // So it can open doors but not attack windows and shatter the literal illusion.
|
||||
|
||||
// Used for technomancer illusions, to resemble player movement better.
|
||||
/datum/ai_holder/simple_mob/inert/astar
|
||||
use_astar = TRUE
|
||||
|
||||
// Ranged mobs.
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged
|
||||
// ranged = TRUE
|
||||
|
||||
// Tries to not waste ammo.
|
||||
/datum/ai_holder/simple_mob/ranged/careful
|
||||
conserve_ammo = TRUE
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged/pointblank
|
||||
pointblank = TRUE
|
||||
|
||||
// Runs away from its target if within a certain distance.
|
||||
/datum/ai_holder/simple_mob/ranged/kiting
|
||||
pointblank = TRUE // So we don't need to copypaste post_melee_attack().
|
||||
var/run_if_this_close = 4 // If anything gets within this range, it'll try to move away.
|
||||
var/moonwalk = TRUE // If true, mob turns to face the target while kiting, otherwise they turn in the direction they moved towards.
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged/kiting/threatening
|
||||
threaten = TRUE
|
||||
threaten_delay = 1 SECOND // Less of a threat and more of pre-attack notice.
|
||||
threaten_timeout = 30 SECONDS
|
||||
conserve_ammo = TRUE
|
||||
|
||||
// For event-spawned malf drones.
|
||||
/datum/ai_holder/simple_mob/ranged/kiting/threatening/event
|
||||
base_wander_delay = 8
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged/kiting/no_moonwalk
|
||||
moonwalk = FALSE
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged/kiting/on_engagement(atom/A)
|
||||
if(get_dist(holder, A) < run_if_this_close)
|
||||
holder.IMove(get_step_away(holder, A, run_if_this_close))
|
||||
if(moonwalk)
|
||||
holder.face_atom(A)
|
||||
|
||||
// Closes distance from the target even while in range.
|
||||
/datum/ai_holder/simple_mob/ranged/aggressive
|
||||
pointblank = TRUE
|
||||
var/closest_distance = 1 // How close to get to the target. By default they will get into melee range (and then pointblank them).
|
||||
|
||||
/datum/ai_holder/simple_mob/ranged/aggressive/on_engagement(atom/A)
|
||||
if(get_dist(holder, A) > closest_distance)
|
||||
holder.IMove(get_step_towards(holder, A))
|
||||
holder.face_atom(A)
|
||||
|
||||
// Yakkity saxes while firing at you.
|
||||
/datum/ai_holder/hostile/ranged/robust/on_engagement(atom/movable/AM)
|
||||
step_rand(holder)
|
||||
holder.face_atom(AM)
|
||||
|
||||
// Switches intents based on specific criteria.
|
||||
// Used for special mobs who do different things based on intents (and aren't slimes).
|
||||
// Intent switching is generally done in pre_[ranged/special]_attack(), so that the mob can use the right attack for the right time.
|
||||
/datum/ai_holder/simple_mob/intentional
|
||||
|
||||
|
||||
// These try to avoid collateral damage.
|
||||
/datum/ai_holder/simple_mob/restrained
|
||||
violent_breakthrough = FALSE
|
||||
conserve_ammo = TRUE
|
||||
destructive = FALSE
|
||||
|
||||
// This does the opposite of the above subtype.
|
||||
/datum/ai_holder/simple_mob/destructive
|
||||
destructive = TRUE
|
||||
|
||||
// Melee mobs.
|
||||
|
||||
/datum/ai_holder/simple_mob/melee
|
||||
|
||||
// Dances around the enemy its fighting, making it harder to fight back.
|
||||
/datum/ai_holder/simple_mob/melee/evasive
|
||||
|
||||
/datum/ai_holder/simple_mob/melee/evasive/post_melee_attack(atom/A)
|
||||
if(holder.Adjacent(A))
|
||||
holder.IMove(get_step(holder, pick(alldirs)))
|
||||
holder.face_atom(A)
|
||||
|
||||
|
||||
|
||||
// This AI hits something, then runs away for awhile.
|
||||
// It will (almost) always flee if they are uncloaked, AND their target is not stunned.
|
||||
/datum/ai_holder/simple_mob/melee/hit_and_run
|
||||
can_flee = TRUE
|
||||
|
||||
// Used for the 'running' part of hit and run.
|
||||
/datum/ai_holder/simple_mob/melee/hit_and_run/special_flee_check()
|
||||
if(!holder.is_cloaked())
|
||||
if(isliving(target))
|
||||
var/mob/living/L = target
|
||||
return !L.incapacitated(INCAPACITATION_DISABLED) // Don't flee if our target is stunned in some form, even if uncloaked. This is so the mob keeps attacking a stunned opponent.
|
||||
return TRUE // We're out in the open, uncloaked, and our target isn't stunned, so lets flee.
|
||||
return FALSE
|
||||
|
||||
|
||||
// Simple mobs that aren't hostile, but will fight back.
|
||||
/datum/ai_holder/simple_mob/retaliate
|
||||
hostile = FALSE
|
||||
retaliate = TRUE
|
||||
|
||||
// Simple mobs that retaliate and support others in their faction who get attacked.
|
||||
/datum/ai_holder/simple_mob/retaliate/cooperative
|
||||
cooperative = TRUE
|
||||
266
code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm
Normal file
266
code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm
Normal file
@@ -0,0 +1,266 @@
|
||||
// Specialized AI for slime simplemobs.
|
||||
// Unlike the parent AI code, this will probably break a lot of things if you put it on something that isn't /mob/living/simple_mob/slime/xenobio
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime
|
||||
hostile = TRUE
|
||||
cooperative = TRUE
|
||||
firing_lanes = TRUE
|
||||
var/rabid = FALSE // Will attack regardless of discipline.
|
||||
var/discipline = 0 // Beating slimes makes them less likely to lash out. In theory.
|
||||
var/resentment = 0 // 'Unjustified' beatings make this go up, and makes it more likely for abused slimes to go rabid.
|
||||
var/obedience = 0 // Conversely, 'justified' beatings make this go up, and makes discipline decay slower, potentially making it not decay at all.
|
||||
|
||||
var/always_stun = FALSE // If true, the slime will elect to attempt to permastun the target.
|
||||
|
||||
var/last_discipline_decay = null // Last world.time discipline was reduced from decay.
|
||||
var/discipline_decay_time = 5 SECONDS // Earliest that one discipline can decay.
|
||||
|
||||
/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 = 10
|
||||
obedience = 10
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/passive/New() // For Kendrick.
|
||||
..()
|
||||
pacify()
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/New()
|
||||
..()
|
||||
ASSERT(istype(holder, /mob/living/simple_mob/slime/xenobio))
|
||||
|
||||
// Checks if disciplining the slime would be 'justified' right now.
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/is_justified_to_discipline()
|
||||
if(rabid)
|
||||
return TRUE
|
||||
if(target)
|
||||
if(ishuman(target))
|
||||
var/mob/living/carbon/human/H = target
|
||||
if(istype(H.species, /datum/species/monkey))
|
||||
return FALSE // Attacking monkeys is okay.
|
||||
return TRUE // Otherwise attacking other things is bad.
|
||||
return FALSE // Not attacking anything.
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/can_command(mob/living/commander)
|
||||
if(rabid)
|
||||
return FALSE
|
||||
if(!hostile)
|
||||
return SLIME_COMMAND_OBEY
|
||||
// if(commander in friends)
|
||||
// return SLIME_COMMAND_FRIEND
|
||||
if(holder.IIsAlly(commander))
|
||||
return SLIME_COMMAND_FACTION
|
||||
if(discipline > resentment && obedience >= 5)
|
||||
return SLIME_COMMAND_OBEY
|
||||
return FALSE
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/adjust_discipline(amount, silent)
|
||||
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
|
||||
if(amount > 0)
|
||||
if(rabid)
|
||||
return
|
||||
var/justified = is_justified_to_discipline()
|
||||
lost_target() // Stop attacking.
|
||||
|
||||
if(justified)
|
||||
obedience++
|
||||
if(!silent)
|
||||
holder.say(pick("Fine...", "Okay...", "Sorry...", "I yield...", "Mercy..."))
|
||||
else
|
||||
if(prob(resentment * 20))
|
||||
enrage()
|
||||
holder.say(pick("Evil...", "Kill...", "Tyrant..."))
|
||||
else
|
||||
if(!silent)
|
||||
holder.say(pick("Why...?", "I don't understand...?", "Cruel...", "Stop...", "Nooo..."))
|
||||
resentment++ // Done after check so first time will never enrage.
|
||||
|
||||
discipline = between(0, discipline + amount, 10)
|
||||
my_slime.update_mood()
|
||||
|
||||
// This slime always enrages if disciplined.
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/red/adjust_discipline(amount, silent)
|
||||
if(amount > 0 && !rabid)
|
||||
holder.say("Grrr...")
|
||||
holder.add_modifier(/datum/modifier/berserk, 30 SECONDS)
|
||||
enrage()
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/handle_special_strategical()
|
||||
discipline_decay()
|
||||
|
||||
// Handles decay of discipline.
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/discipline_decay()
|
||||
if(discipline > 0 && last_discipline_decay + discipline_decay_time < world.time)
|
||||
if(!prob(75 + (obedience * 5)))
|
||||
adjust_discipline(-1)
|
||||
last_discipline_decay = world.time
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/handle_special_tactic()
|
||||
evolve_and_reproduce()
|
||||
|
||||
// Hit the correct verbs to keep the slime species going.
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/evolve_and_reproduce()
|
||||
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
|
||||
if(my_slime.amount_grown >= 10)
|
||||
// Press the correct verb when we can.
|
||||
if(my_slime.is_adult)
|
||||
my_slime.reproduce() // Splits into four new baby slimes.
|
||||
else
|
||||
my_slime.evolve() // Turns our holder into an adult slime.
|
||||
|
||||
|
||||
// Called when pushed too far (or a red slime core was used).
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/enrage()
|
||||
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
|
||||
if(my_slime.harmless)
|
||||
return
|
||||
rabid = TRUE
|
||||
my_slime.update_mood()
|
||||
my_slime.visible_message(span("danger", "\The [src] enrages!"))
|
||||
|
||||
// Called when using a pacification agent (or it's Kendrick being initalized).
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/proc/pacify()
|
||||
lost_target() // So it stops trying to kill them.
|
||||
rabid = FALSE
|
||||
hostile = FALSE
|
||||
retaliate = FALSE
|
||||
cooperative = FALSE
|
||||
|
||||
// The holder's attack changes based on intent. This lets the AI choose what effect is desired.
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/pre_melee_attack(atom/A)
|
||||
if(istype(A, /mob/living))
|
||||
var/mob/living/L = A
|
||||
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
|
||||
|
||||
if( (!L.lying && prob(30 + (my_slime.power_charge * 7) ) || (!L.lying && always_stun) ))
|
||||
my_slime.a_intent = I_DISARM // Stun them first.
|
||||
else if(my_slime.can_consume(L) && L.lying)
|
||||
my_slime.a_intent = I_GRAB // Then eat them.
|
||||
else
|
||||
my_slime.a_intent = I_HURT // Otherwise robust them.
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/closest_distance(atom/movable/AM)
|
||||
if(istype(AM, /mob/living))
|
||||
var/mob/living/L = AM
|
||||
if(ishuman(L))
|
||||
var/mob/living/carbon/human/H = L
|
||||
if(istype(H.species, /datum/species/monkey))
|
||||
return 1 // Otherwise ranged slimes will eat a lot less often.
|
||||
if(L.stat >= UNCONSCIOUS)
|
||||
return 1 // Melee (eat) the target if dead/dying, don't shoot it.
|
||||
return ..()
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/can_attack(atom/movable/AM)
|
||||
. = ..()
|
||||
if(.) // Do some additional checks because we have Special Code(tm).
|
||||
if(ishuman(AM))
|
||||
var/mob/living/carbon/human/H = AM
|
||||
if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too.
|
||||
return TRUE // Monkeys are always food (sorry Pun Pun).
|
||||
else if(H.species && H.species.name == SPECIES_PROMETHEAN)
|
||||
return FALSE // Prometheans are always our friends.
|
||||
if(discipline && !rabid)
|
||||
return FALSE // We're a good slime.
|
||||
|
||||
// Commands, reactions, etc
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/on_hear_say(mob/living/speaker, message)
|
||||
ai_log("xenobio_slime/on_hear_say([speaker], [message]) : Entered.", AI_LOG_DEBUG)
|
||||
var/mob/living/simple_mob/slime/xenobio/my_slime = holder
|
||||
|
||||
if((findtext(message, num2text(my_slime.number)) || findtext(message, my_slime.name) || findtext(message, "slimes"))) // Talking to us.
|
||||
|
||||
// First, make sure it's actually a player saying something and not an AI, or else we risk infinite loops.
|
||||
if(!speaker.client)
|
||||
return
|
||||
|
||||
// Are all slimes being referred to?
|
||||
// var/mass_order = FALSE
|
||||
// if(findtext(message, "slimes"))
|
||||
// mass_order = TRUE
|
||||
|
||||
// Say hello back.
|
||||
if(findtext(message, "hello") || findtext(message, "hi") || findtext(message, "greetings"))
|
||||
delayed_say(pick("Hello...", "Hi..."), speaker)
|
||||
|
||||
// Follow request.
|
||||
if(findtext(message, "follow") || findtext(message, "come with me"))
|
||||
if(!can_command(speaker))
|
||||
delayed_say(pick("No...", "I won't follow..."), speaker)
|
||||
return
|
||||
|
||||
delayed_say("Yes... I follow \the [speaker]...", speaker)
|
||||
set_follow(speaker)
|
||||
|
||||
// Squish request.
|
||||
if(findtext(message , "squish"))
|
||||
if(!can_command(speaker))
|
||||
delayed_say("No...", speaker)
|
||||
return
|
||||
|
||||
spawn(rand(1 SECOND, 2 SECONDS))
|
||||
if(!src || !holder || !can_act()) // We might've died/got deleted/etc in the meantime.
|
||||
return
|
||||
my_slime.squish()
|
||||
|
||||
|
||||
// Stop request.
|
||||
if(findtext(message, "stop") || findtext(message, "halt") || findtext(message, "cease"))
|
||||
if(my_slime.victim) // We're being asked to stop eatting someone.
|
||||
if(!can_command(speaker) || !is_justified_to_discipline())
|
||||
delayed_say("No...", speaker)
|
||||
return
|
||||
else
|
||||
delayed_say("Fine...", speaker)
|
||||
adjust_discipline(1, TRUE)
|
||||
my_slime.stop_consumption()
|
||||
|
||||
if(target) // We're being asked to stop chasing someone.
|
||||
if(!can_command(speaker) || !is_justified_to_discipline())
|
||||
delayed_say("No...", speaker)
|
||||
return
|
||||
else
|
||||
delayed_say("Fine...", speaker)
|
||||
adjust_discipline(1, TRUE) // This must come before losing the target or it will be unjustified.
|
||||
lost_target()
|
||||
|
||||
|
||||
if(leader) // We're being asked to stop following someone.
|
||||
if(can_command(speaker) == SLIME_COMMAND_FRIEND || leader == speaker)
|
||||
delayed_say("Yes... I'll stop...", speaker)
|
||||
lose_follow()
|
||||
else
|
||||
delayed_say("No... I'll keep following \the [leader]...", speaker)
|
||||
|
||||
/* // Commented out since its mostly useless now due to slimes refusing to attack if it would make them naughty.
|
||||
// Murder request
|
||||
if(findtext(message, "harm") || findtext(message, "attack") || findtext(message, "kill") || findtext(message, "murder") || findtext(message, "eat") || findtext(message, "consume") || findtext(message, "absorb"))
|
||||
if(can_command(speaker) < SLIME_COMMAND_FACTION)
|
||||
delayed_say("No...", speaker)
|
||||
return
|
||||
|
||||
for(var/mob/living/L in view(7, my_slime) - list(my_slime, speaker))
|
||||
if(L == src)
|
||||
continue // Don't target ourselves.
|
||||
var/list/valid_names = splittext(L.name, " ") // Should output list("John", "Doe") as an example.
|
||||
for(var/line in valid_names) // Check each part of someone's name.
|
||||
if(findtext(message, lowertext(line))) // If part of someone's name is in the command, the slime targets them if allowed to.
|
||||
if(!(mass_order && line == "slime")) //don't think random other slimes are target
|
||||
if(can_attack(L))
|
||||
delayed_say("Okay... I attack \the [L]...", speaker)
|
||||
give_target(L)
|
||||
return
|
||||
else
|
||||
delayed_say("No... I won't attack \the [L].", speaker)
|
||||
return
|
||||
|
||||
// If we're here, it couldn't find anyone with that name.
|
||||
delayed_say("No... I don't know who to attack...", speaker)
|
||||
*/
|
||||
ai_log("xenobio_slime/on_hear_say() : Exited.", AI_LOG_DEBUG)
|
||||
|
||||
/datum/ai_holder/simple_mob/xenobio_slime/can_violently_breakthrough()
|
||||
if(discipline && !rabid) // Good slimes don't shatter the windows because their buddy in an adjacent cell decided to piss off Slimesky.
|
||||
return FALSE
|
||||
return ..()
|
||||
239
code/modules/ai/ai_holder_targeting.dm
Normal file
239
code/modules/ai/ai_holder_targeting.dm
Normal file
@@ -0,0 +1,239 @@
|
||||
// Used for assigning a target for attacking.
|
||||
|
||||
/datum/ai_holder
|
||||
var/hostile = FALSE // Do we try to hurt others?
|
||||
var/retaliate = FALSE // Attacks whatever struck it first. Mobs will still attack back if this is false but hostile is true.
|
||||
|
||||
var/atom/movable/target = null // The thing (mob or object) we're trying to kill.
|
||||
var/atom/movable/preferred_target = null// If set, and if given the chance, we will always prefer to target this over other options.
|
||||
var/turf/target_last_seen_turf = null // Where the mob last observed the target being, used if the target disappears but the mob wants to keep fighting.
|
||||
|
||||
var/vision_range = 7 // How far the targeting system will look for things to kill. Note that values higher than 7 are 'offscreen' and might be unsporting.
|
||||
var/respect_alpha = TRUE // If true, mobs with a sufficently low alpha will be treated as invisible.
|
||||
var/alpha_vision_threshold = 127 // Targets with an alpha less or equal to this will be considered invisible. Requires above var to be true.
|
||||
|
||||
var/lose_target_time = 0 // world.time when a target was lost.
|
||||
var/lose_target_timeout = 5 SECONDS // How long until a mob 'times out' and stops trying to find the mob that disappeared.
|
||||
|
||||
var/list/attackers = list() // List of strings of names of people who attacked us before in our life.
|
||||
// This uses strings and not refs to allow for disguises, and to avoid needing to use weakrefs.
|
||||
var/destructive = FALSE // Will target 'neutral' structures/objects and not just 'hostile' ones.
|
||||
|
||||
// A lot of this is based off of /TG/'s AI code.
|
||||
|
||||
// Step 1, find out what we can see.
|
||||
/datum/ai_holder/proc/list_targets()
|
||||
. = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions.
|
||||
|
||||
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha))
|
||||
|
||||
for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines))
|
||||
if(can_see(holder, HM, vision_range))
|
||||
. += HM
|
||||
|
||||
// Step 2, filter down possible targets to things we actually care about.
|
||||
/datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE)
|
||||
if(!hostile) // So retaliating mobs only attack the thing that hit it.
|
||||
return null
|
||||
. = list()
|
||||
if(!has_targets_list)
|
||||
possible_targets = list_targets()
|
||||
for(var/possible_target in possible_targets)
|
||||
var/atom/A = possible_target
|
||||
if(found(A)) // In case people want to override this.
|
||||
. = list(A)
|
||||
break
|
||||
if(can_attack(A)) // Can we attack it?
|
||||
. += A
|
||||
continue
|
||||
|
||||
var/new_target = pick_target(.)
|
||||
give_target(new_target)
|
||||
return new_target
|
||||
|
||||
// Step 3, pick among the possible, attackable targets.
|
||||
/datum/ai_holder/proc/pick_target(list/targets)
|
||||
if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
|
||||
targets = target_filter_distance(targets)
|
||||
// for(var/possible_target in targets)
|
||||
// var/atom/A = possible_target
|
||||
// var/target_dist = get_dist(holder, target)
|
||||
// var/possible_target_distance = get_dist(holder, A)
|
||||
// if(target_dist < possible_target_distance)
|
||||
// targets -= A
|
||||
if(!targets.len) // We found nothing.
|
||||
return
|
||||
|
||||
var/chosen_target
|
||||
if(preferred_target && preferred_target in targets)
|
||||
chosen_target = preferred_target
|
||||
else
|
||||
chosen_target = pick(targets)
|
||||
return chosen_target
|
||||
|
||||
// Step 4, give us our selected target.
|
||||
/datum/ai_holder/proc/give_target(new_target)
|
||||
target = new_target
|
||||
if(target != null)
|
||||
if(should_threaten())
|
||||
set_stance(STANCE_ALERT)
|
||||
else
|
||||
set_stance(STANCE_APPROACH)
|
||||
return TRUE
|
||||
|
||||
// Filters return one or more 'preferred' targets.
|
||||
|
||||
// This one is for closest targets.
|
||||
/datum/ai_holder/proc/target_filter_distance(list/targets)
|
||||
for(var/possible_target in targets)
|
||||
var/atom/A = possible_target
|
||||
var/target_dist = get_dist(holder, target)
|
||||
var/possible_target_distance = get_dist(holder, A)
|
||||
if(target_dist < possible_target_distance)
|
||||
targets -= A
|
||||
return targets
|
||||
|
||||
/datum/ai_holder/proc/can_attack(atom/movable/the_target)
|
||||
if(!can_see_target(the_target))
|
||||
return FALSE
|
||||
|
||||
if(istype(the_target, /mob/zshadow))
|
||||
return FALSE // no
|
||||
|
||||
if(isliving(the_target))
|
||||
var/mob/living/L = the_target
|
||||
if(L.stat == DEAD)
|
||||
return FALSE
|
||||
if(holder.IIsAlly(L))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
if(istype(the_target, /obj/mecha))
|
||||
var/obj/mecha/M = the_target
|
||||
if(M.occupant)
|
||||
return can_attack(M.occupant)
|
||||
return destructive // Empty mechs are 'neutral'.
|
||||
|
||||
if(istype(the_target, /obj/machinery/porta_turret))
|
||||
var/obj/machinery/porta_turret/P = the_target
|
||||
if(P.stat & BROKEN)
|
||||
return FALSE // Already dead.
|
||||
if(P.faction == holder.faction)
|
||||
return FALSE // Don't shoot allied turrets.
|
||||
if(!P.raised && !P.raising)
|
||||
return FALSE // Turrets won't get hurt if they're still in their cover.
|
||||
return TRUE
|
||||
|
||||
return TRUE
|
||||
// return FALSE
|
||||
|
||||
// Override this for special targeting criteria.
|
||||
// If it returns true, the mob will always select it as the target.
|
||||
/datum/ai_holder/proc/found(atom/movable/the_target)
|
||||
return FALSE
|
||||
|
||||
//We can't see the target, go look or attack where they were last seen.
|
||||
/datum/ai_holder/proc/lose_target()
|
||||
if(target)
|
||||
target = null
|
||||
lose_target_time = world.time
|
||||
|
||||
give_up_movement()
|
||||
|
||||
|
||||
//Target is no longer valid (?)
|
||||
/datum/ai_holder/proc/lost_target()
|
||||
set_stance(STANCE_IDLE)
|
||||
lose_target_position()
|
||||
lose_target()
|
||||
|
||||
// Check if target is visible to us.
|
||||
/datum/ai_holder/proc/can_see_target(atom/movable/the_target, view_range = vision_range)
|
||||
ai_log("can_see_target() : Entering.", AI_LOG_TRACE)
|
||||
|
||||
if(!the_target) // Nothing to target.
|
||||
ai_log("can_see_target() : There is no target. Exiting.", AI_LOG_WARNING)
|
||||
return FALSE
|
||||
|
||||
if(holder.see_invisible < the_target.invisibility) // Real invis.
|
||||
ai_log("can_see_target() : Target ([the_target]) was invisible to holder. Exiting.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
|
||||
if(respect_alpha && the_target.alpha <= alpha_vision_threshold) // Fake invis.
|
||||
ai_log("can_see_target() : Target ([the_target]) was sufficently transparent to holder and is hidden. Exiting.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
|
||||
if(get_dist(holder, the_target) > view_range) // Too far away.
|
||||
ai_log("can_see_target() : Target ([the_target]) was too far from holder. Exiting.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
|
||||
if(!can_see(holder, the_target, view_range))
|
||||
ai_log("can_see_target() : Target ([the_target]) failed can_see(). Exiting.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
|
||||
ai_log("can_see_target() : Target ([the_target]) can be seen. Exiting.", AI_LOG_TRACE)
|
||||
return TRUE
|
||||
|
||||
// Updates the last known position of the target.
|
||||
/datum/ai_holder/proc/track_target_position()
|
||||
if(!target)
|
||||
lose_target_position()
|
||||
|
||||
if(last_turf_display && target_last_seen_turf)
|
||||
target_last_seen_turf.overlays -= last_turf_overlay
|
||||
|
||||
target_last_seen_turf = get_turf(target)
|
||||
|
||||
if(last_turf_display)
|
||||
target_last_seen_turf.overlays += last_turf_overlay
|
||||
|
||||
// Resets the last known position to null.
|
||||
/datum/ai_holder/proc/lose_target_position()
|
||||
if(last_turf_display && target_last_seen_turf)
|
||||
target_last_seen_turf.overlays -= last_turf_overlay
|
||||
ai_log("lose_target_position() : Last position is being reset.", AI_LOG_INFO)
|
||||
target_last_seen_turf = null
|
||||
|
||||
// Responds to a hostile action against its mob.
|
||||
/datum/ai_holder/proc/react_to_attack(atom/movable/attacker)
|
||||
if(holder.stat) // We're dead.
|
||||
ai_log("react_to_attack() : Was attacked by [attacker], but we are dead/unconscious.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
if(!hostile && !retaliate) // Not allowed to defend ourselves.
|
||||
ai_log("react_to_attack() : Was attacked by [attacker], but we are not allowed to attack back.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
if(holder.IIsAlly(attacker)) // I'll overlook it THIS time...
|
||||
ai_log("react_to_attack() : Was attacked by [attacker], but they were an ally.", AI_LOG_TRACE)
|
||||
return FALSE
|
||||
if(target) // Already fighting someone. Switching every time we get hit would impact our combat performance.
|
||||
ai_log("react_to_attack() : Was attacked by [attacker], but we already have a target.", AI_LOG_TRACE)
|
||||
on_attacked(attacker) // So we attack immediately and not threaten.
|
||||
return FALSE
|
||||
|
||||
if(stance == STANCE_SLEEP) // If we're asleep, try waking up if someone's wailing on us.
|
||||
ai_log("react_to_attack() : AI is asleep. Waking up.", AI_LOG_TRACE)
|
||||
go_wake()
|
||||
|
||||
ai_log("react_to_attack() : Was attacked by [attacker].", AI_LOG_INFO)
|
||||
on_attacked(attacker) // So we attack immediately and not threaten.
|
||||
return give_target(attacker) // Also handles setting the appropiate stance.
|
||||
|
||||
// Sets a few vars so mobs that threaten will react faster to an attacker or someone who attacked them before.
|
||||
/datum/ai_holder/proc/on_attacked(atom/movable/AM)
|
||||
last_conflict_time = world.time
|
||||
if(isliving(AM))
|
||||
var/mob/living/L = AM
|
||||
attackers |= L.name
|
||||
|
||||
// Causes targeting to prefer targeting the taunter if possible.
|
||||
// This generally occurs if more than one option is within striking distance, including the taunter.
|
||||
// Otherwise the default filter will prefer the closest target.
|
||||
/datum/ai_holder/proc/receive_taunt(atom/movable/taunter, force_target_switch = FALSE)
|
||||
ai_log("receive_taunt() : Was taunted by [taunter].", AI_LOG_INFO)
|
||||
preferred_target = taunter
|
||||
if(force_target_switch)
|
||||
give_target(taunter)
|
||||
|
||||
/datum/ai_holder/proc/lose_taunt()
|
||||
ai_log("lose_taunt() : Resetting preferred_target.", AI_LOG_INFO)
|
||||
preferred_target = null
|
||||
237
code/modules/mob/living/simple_mob/combat.dm
Normal file
237
code/modules/mob/living/simple_mob/combat.dm
Normal file
@@ -0,0 +1,237 @@
|
||||
// Does a melee attack.
|
||||
/mob/living/simple_mob/proc/attack_target(atom/A)
|
||||
set waitfor = FALSE // For attack animations. Don't want the AI processor to get held up.
|
||||
|
||||
if(!A.Adjacent(src))
|
||||
return FALSE
|
||||
var/turf/their_T = get_turf(A)
|
||||
|
||||
face_atom(A)
|
||||
|
||||
if(melee_attack_delay)
|
||||
// their_T.color = "#FF0000"
|
||||
melee_pre_animation(A)
|
||||
handle_attack_delay(A, melee_attack_delay) // This will sleep this proc for a bit, which is why waitfor is false.
|
||||
|
||||
// Cooldown testing is done at click code (for players) and interface code (for AI).
|
||||
setClickCooldown(get_attack_speed())
|
||||
|
||||
. = do_attack(A, their_T)
|
||||
|
||||
if(melee_attack_delay)
|
||||
melee_post_animation(A)
|
||||
// their_T.color = "#FFFFFF"
|
||||
|
||||
|
||||
|
||||
// This does the actual attack.
|
||||
// This is a seperate proc for the purposes of attack animations.
|
||||
// A is the thing getting attacked, T is the turf A is/was on when attack_target was called.
|
||||
/mob/living/simple_mob/proc/do_attack(atom/A, turf/T)
|
||||
face_atom(A)
|
||||
var/missed = FALSE
|
||||
if(!isturf(A) && !(A in T) ) // Turfs don't contain themselves so checking contents is pointless if we're targeting a turf.
|
||||
missed = TRUE
|
||||
else if(!T.AdjacentQuick(src))
|
||||
missed = TRUE
|
||||
|
||||
if(missed) // Most likely we have a slow attack and they dodged it or we somehow got moved.
|
||||
add_attack_logs(src, A, "Animal-attacked (dodged)", admin_notify = FALSE)
|
||||
playsound(src, 'sound/weapons/punchmiss.ogg', 75, 1)
|
||||
visible_message(span("warning", "\The [src] misses their attack."))
|
||||
return FALSE
|
||||
|
||||
var/damage_to_do = rand(melee_damage_lower, melee_damage_upper)
|
||||
|
||||
damage_to_do = apply_bonus_melee_damage(A, damage_to_do)
|
||||
|
||||
for(var/datum/modifier/M in modifiers)
|
||||
if(!isnull(M.outgoing_melee_damage_percent))
|
||||
damage_to_do *= M.outgoing_melee_damage_percent
|
||||
|
||||
if(isliving(A)) // Check defenses.
|
||||
var/mob/living/L = A
|
||||
|
||||
if(prob(melee_miss_chance))
|
||||
add_attack_logs(src, L, "Animal-attacked (miss)", admin_notify = FALSE)
|
||||
do_attack_animation(src)
|
||||
playsound(src, 'sound/weapons/punchmiss.ogg', 75, 1)
|
||||
return FALSE // We missed.
|
||||
|
||||
if(ishuman(L))
|
||||
var/mob/living/carbon/human/H = L
|
||||
if(H.check_shields(damage = damage_to_do, damage_source = src, attacker = src, def_zone = null, attack_text = "the attack"))
|
||||
return FALSE // We were blocked.
|
||||
|
||||
if(apply_attack(A, damage_to_do))
|
||||
apply_melee_effects(A)
|
||||
if(attack_sound)
|
||||
playsound(src, attack_sound, 75, 1)
|
||||
|
||||
return TRUE
|
||||
|
||||
// Generally used to do the regular attack.
|
||||
// Override for doing special stuff with the direct result of the attack.
|
||||
/mob/living/simple_mob/proc/apply_attack(atom/A, damage_to_do)
|
||||
return A.attack_generic(src, damage_to_do, pick(attacktext))
|
||||
|
||||
// Override for special effects after a successful attack, like injecting poison or stunning the target.
|
||||
/mob/living/simple_mob/proc/apply_melee_effects(atom/A)
|
||||
return
|
||||
|
||||
// Override to modify the amount of damage the mob does conditionally.
|
||||
// This must return the amount of outgoing damage.
|
||||
// Note that this is done before mob modifiers scale the damage.
|
||||
/mob/living/simple_mob/proc/apply_bonus_melee_damage(atom/A, damage_amount)
|
||||
return damage_amount
|
||||
|
||||
//The actual top-level ranged attack proc
|
||||
/mob/living/simple_mob/proc/shoot_target(atom/A)
|
||||
set waitfor = FALSE
|
||||
setClickCooldown(get_attack_speed())
|
||||
|
||||
face_atom(A)
|
||||
|
||||
if(ranged_attack_delay)
|
||||
ranged_pre_animation(A)
|
||||
handle_attack_delay(A, ranged_attack_delay) // This will sleep this proc for a bit, which is why waitfor is false.
|
||||
|
||||
if(needs_reload)
|
||||
if(reload_count >= reload_max)
|
||||
try_reload()
|
||||
return FALSE
|
||||
|
||||
visible_message("<span class='danger'><b>\The [src]</b> fires at \the [A]!</span>")
|
||||
shoot(A)
|
||||
if(casingtype)
|
||||
new casingtype(loc)
|
||||
|
||||
if(ranged_attack_delay)
|
||||
ranged_post_animation(A)
|
||||
|
||||
return TRUE
|
||||
|
||||
|
||||
// Shoot a bullet at something.
|
||||
/mob/living/simple_mob/proc/shoot(atom/A)
|
||||
if(A == get_turf(src))
|
||||
return
|
||||
|
||||
face_atom(A)
|
||||
|
||||
var/obj/item/projectile/P = new projectiletype(src.loc)
|
||||
if(!P)
|
||||
return
|
||||
|
||||
// If the projectile has its own sound, use it.
|
||||
// Otherwise default to the mob's firing sound.
|
||||
playsound(src, P.fire_sound ? P.fire_sound : projectilesound, 80, 1)
|
||||
|
||||
P.firer = src // So we can't shoot ourselves.
|
||||
P.launch(A)
|
||||
if(needs_reload)
|
||||
reload_count++
|
||||
|
||||
// if(distance >= special_attack_min_range && distance <= special_attack_max_range)
|
||||
// return TRUE
|
||||
|
||||
/mob/living/simple_mob/proc/try_reload()
|
||||
set waitfor = FALSE
|
||||
set_AI_busy(TRUE)
|
||||
|
||||
if(do_after(src, reload_time))
|
||||
if(reload_sound)
|
||||
playsound(src.loc, reload_sound, 50, 1)
|
||||
reload_count = 0
|
||||
. = TRUE
|
||||
else
|
||||
. = FALSE
|
||||
set_AI_busy(FALSE)
|
||||
|
||||
// Can we currently do a special attack?
|
||||
/mob/living/simple_mob/proc/can_special_attack(atom/A)
|
||||
// Validity check.
|
||||
if(!istype(A))
|
||||
return FALSE
|
||||
|
||||
// Ability check.
|
||||
if(isnull(special_attack_min_range) || isnull(special_attack_max_range))
|
||||
return FALSE
|
||||
|
||||
// Distance check.
|
||||
var/distance = get_dist(src, A)
|
||||
if(distance < special_attack_min_range || distance > special_attack_max_range)
|
||||
return FALSE
|
||||
|
||||
// Cooldown check.
|
||||
if(!isnull(special_attack_cooldown) && last_special_attack + special_attack_cooldown > world.time)
|
||||
return FALSE
|
||||
|
||||
// Charge check.
|
||||
if(!isnull(special_attack_charges) && special_attack_charges <= 0)
|
||||
return FALSE
|
||||
|
||||
return TRUE
|
||||
|
||||
// Should we do one? Used to make the AI not waste their special attacks. Only checked for AI. Players are free to screw up on their own.
|
||||
/mob/living/simple_mob/proc/should_special_attack(atom/A)
|
||||
return TRUE
|
||||
|
||||
// Special attacks, like grenades or blinding spit or whatever.
|
||||
// Don't override this, override do_special_attack() for your blinding spit/etc.
|
||||
/mob/living/simple_mob/proc/special_attack_target(atom/A)
|
||||
face_atom(A)
|
||||
|
||||
if(special_attack_delay)
|
||||
special_pre_animation(A)
|
||||
handle_attack_delay(A, special_attack_delay) // This will sleep this proc for a bit, which is why waitfor is false.
|
||||
|
||||
last_special_attack = world.time
|
||||
if(do_special_attack(A))
|
||||
if(special_attack_charges)
|
||||
special_attack_charges -= 1
|
||||
. = TRUE
|
||||
else
|
||||
. = FALSE
|
||||
|
||||
if(special_attack_delay)
|
||||
special_post_animation(A)
|
||||
|
||||
// Override this for the actual special attack.
|
||||
/mob/living/simple_mob/proc/do_special_attack(atom/A)
|
||||
return FALSE
|
||||
|
||||
// Sleeps the proc that called it for the correct amount of time.
|
||||
// Also makes sure the AI doesn't do anything stupid in the middle of the delay.
|
||||
/mob/living/simple_mob/proc/handle_attack_delay(atom/A, delay_amount)
|
||||
set_AI_busy(TRUE)
|
||||
|
||||
// Click delay modifiers also affect telegraphing time.
|
||||
// This means berserked enemies will leave less time to dodge.
|
||||
var/true_attack_delay = delay_amount
|
||||
for(var/datum/modifier/M in modifiers)
|
||||
if(!isnull(M.attack_speed_percent))
|
||||
true_attack_delay *= M.attack_speed_percent
|
||||
|
||||
setClickCooldown(true_attack_delay) // Insurance against a really long attack being longer than default click delay.
|
||||
|
||||
sleep(true_attack_delay)
|
||||
|
||||
set_AI_busy(FALSE)
|
||||
|
||||
|
||||
// Override these four for special custom animations (like the GOLEM).
|
||||
/mob/living/simple_mob/proc/melee_pre_animation(atom/A)
|
||||
do_windup_animation(A, melee_attack_delay)
|
||||
|
||||
/mob/living/simple_mob/proc/melee_post_animation(atom/A)
|
||||
|
||||
/mob/living/simple_mob/proc/ranged_pre_animation(atom/A)
|
||||
do_windup_animation(A, ranged_attack_delay) // Semi-placeholder.
|
||||
|
||||
/mob/living/simple_mob/proc/ranged_post_animation(atom/A)
|
||||
|
||||
/mob/living/simple_mob/proc/special_pre_animation(atom/A)
|
||||
do_windup_animation(A, special_attack_delay) // Semi-placeholder.
|
||||
|
||||
/mob/living/simple_mob/proc/special_post_animation(atom/A)
|
||||
@@ -0,0 +1,278 @@
|
||||
// These slimes have the mechanics xenobiologists care about, such as reproduction, mutating into new colors, and being able to submit through fear.
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio
|
||||
desc = "The most basic of slimes. The grey slime has no remarkable qualities, however it remains one of the most useful colors for scientists."
|
||||
layer = MOB_LAYER + 1 // Need them on top of other mobs or it looks weird when consuming something.
|
||||
ai_holder_type = /datum/ai_holder/simple_mob/xenobio_slime // This should never be changed for xenobio slimes.
|
||||
var/is_adult = FALSE // Slimes turn into adults when fed enough. Adult slimes are somewhat stronger, and can reproduce if fed enough.
|
||||
var/maxHealth_adult = 200
|
||||
var/power_charge = 0 // Disarm attacks can shock someone if high/lucky enough.
|
||||
var/mob/living/victim = null // the person the slime is currently feeding on
|
||||
var/rainbow_core_candidate = TRUE // If false, rainbow cores cannot make this type randomly.
|
||||
var/mutation_chance = 25 // Odds of spawning as a new color when reproducing. Can be modified by certain xenobio products. Carried across generations of slimes.
|
||||
var/list/slime_mutation = list(
|
||||
/mob/living/simple_mob/slime/xenobio/orange,
|
||||
/mob/living/simple_mob/slime/xenobio/metal,
|
||||
/mob/living/simple_mob/slime/xenobio/blue,
|
||||
/mob/living/simple_mob/slime/xenobio/purple
|
||||
)
|
||||
var/amount_grown = 0 // controls how long the slime has been overfed, if 10, grows or reproduces
|
||||
var/number = 0 // This is used to make the slime semi-unique for indentification.
|
||||
var/harmless = FALSE // Set to true when pacified. Makes the slime harmless, not get hungry, and not be able to grow/reproduce.
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/initialize(mapload, var/mob/living/simple_mob/slime/xenobio/my_predecessor)
|
||||
ASSERT(ispath(ai_holder_type, /datum/ai_holder/simple_mob/xenobio_slime))
|
||||
number = rand(1, 1000)
|
||||
update_name()
|
||||
|
||||
. = ..() // This will make the AI and do the other mob constructor things. It will also return the default hint at the end.
|
||||
|
||||
if(my_predecessor)
|
||||
inherit_information(my_predecessor)
|
||||
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/Destroy()
|
||||
if(victim)
|
||||
stop_consumption() // Unbuckle us from our victim.
|
||||
return ..()
|
||||
|
||||
// Called when a slime makes another slime by splitting. The predecessor slime will be deleted shortly afterwards.
|
||||
/mob/living/simple_mob/slime/xenobio/proc/inherit_information(var/mob/living/simple_mob/slime/xenobio/predecessor)
|
||||
if(!predecessor)
|
||||
return
|
||||
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/AI = ai_holder
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/previous_AI = predecessor.ai_holder
|
||||
ASSERT(istype(AI))
|
||||
ASSERT(istype(previous_AI))
|
||||
|
||||
// Now to transfer the information.
|
||||
// Newly made slimes are bit more rebellious than their predecessors, but they also somewhat forget the atrocities the xenobiologist may have done.
|
||||
AI.discipline = max(previous_AI.discipline - 1, 0)
|
||||
AI.obedience = max(previous_AI.obedience - 1, 0)
|
||||
AI.resentment = max(previous_AI.resentment - 1, 0)
|
||||
AI.rabid = previous_AI.rabid
|
||||
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/update_icon()
|
||||
icon_living = "[icon_state_override ? "[icon_state_override] slime" : "slime"] [is_adult ? "adult" : "baby"][victim ? " eating" : ""]"
|
||||
icon_dead = "[icon_state_override ? "[icon_state_override] slime" : "slime"] [is_adult ? "adult" : "baby"] dead"
|
||||
icon_rest = icon_dead
|
||||
..() // This will apply the correct icon_state and do the other overlay-related things.
|
||||
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/handle_special()
|
||||
if(stat != DEAD)
|
||||
handle_nutrition()
|
||||
|
||||
if(victim)
|
||||
handle_consumption()
|
||||
|
||||
handle_stuttering() // ??
|
||||
|
||||
..()
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/examine(mob/user)
|
||||
..()
|
||||
if(hat)
|
||||
to_chat(user, "It is wearing \a [hat].")
|
||||
|
||||
if(stat == DEAD)
|
||||
to_chat(user, "It appears to be dead.")
|
||||
else if(incapacitated(INCAPACITATION_DISABLED))
|
||||
to_chat(user, "It appears to be incapacitated.")
|
||||
else if(harmless)
|
||||
to_chat(user, "It appears to have been pacified.")
|
||||
else
|
||||
if(has_AI())
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/AI = ai_holder
|
||||
if(AI.rabid)
|
||||
to_chat(user, "It seems very, very angry and upset.")
|
||||
else if(AI.obedience >= 5)
|
||||
to_chat(user, "It looks rather obedient.")
|
||||
else if(AI.discipline)
|
||||
to_chat(user, "It has been subjugated by force, at least for now.")
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/proc/make_adult()
|
||||
if(is_adult)
|
||||
return
|
||||
|
||||
is_adult = TRUE
|
||||
melee_damage_lower = round(melee_damage_lower * 2) // 20
|
||||
melee_damage_upper = round(melee_damage_upper * 2) // 30
|
||||
maxHealth = maxHealth_adult
|
||||
amount_grown = 0
|
||||
update_icon()
|
||||
update_name()
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/proc/update_name()
|
||||
if(harmless) // Docile slimes are generally named, so we shouldn't mess with it.
|
||||
return
|
||||
name = "[slime_color] [is_adult ? "adult" : "baby"] [initial(name)] ([number])"
|
||||
real_name = name
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/update_mood()
|
||||
var/old_mood = mood
|
||||
if(incapacitated(INCAPACITATION_DISABLED))
|
||||
mood = "sad"
|
||||
else if(harmless)
|
||||
mood = ":33"
|
||||
else if(has_AI())
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/AI = ai_holder
|
||||
if(AI.rabid)
|
||||
mood = "angry"
|
||||
else if(AI.target)
|
||||
mood = "mischevous"
|
||||
else if(AI.discipline)
|
||||
mood = "pout"
|
||||
else
|
||||
mood = ":3"
|
||||
else
|
||||
mood = ":3"
|
||||
|
||||
if(old_mood != mood)
|
||||
update_icon()
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/proc/enrage()
|
||||
if(harmless)
|
||||
return
|
||||
if(has_AI())
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/AI = ai_holder
|
||||
AI.enrage()
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/proc/pacify()
|
||||
harmless = TRUE
|
||||
if(has_AI())
|
||||
var/datum/ai_holder/simple_mob/xenobio_slime/AI = ai_holder
|
||||
AI.pacify()
|
||||
|
||||
faction = "neutral"
|
||||
|
||||
// If for whatever reason the mob AI (or player) decides to try to attack something anyways.
|
||||
melee_damage_upper = 0
|
||||
melee_damage_lower = 0
|
||||
|
||||
update_mood()
|
||||
|
||||
|
||||
// These are verbs so that player slimes can evolve/split.
|
||||
/mob/living/simple_mob/slime/xenobio/verb/evolve()
|
||||
set category = "Slime"
|
||||
set desc = "This will let you evolve from baby to adult slime."
|
||||
|
||||
if(stat)
|
||||
to_chat(src, span("warning", "I must be conscious to do this..."))
|
||||
return
|
||||
|
||||
if(harmless)
|
||||
to_chat(src, span("warning", "I have been pacified. I cannot evolve..."))
|
||||
return
|
||||
|
||||
if(!is_adult)
|
||||
if(amount_grown >= 10)
|
||||
make_adult()
|
||||
else
|
||||
to_chat(src, span("warning", "I am not ready to evolve yet..."))
|
||||
else
|
||||
to_chat(src, span("warning", "I have already evolved..."))
|
||||
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/verb/reproduce()
|
||||
set category = "Slime"
|
||||
set desc = "This will make you split into four new slimes."
|
||||
|
||||
if(stat)
|
||||
to_chat(src, span("warning", "I must be conscious to do this..."))
|
||||
return
|
||||
|
||||
if(harmless)
|
||||
to_chat(src, span("warning", "I have been pacified. I cannot reproduce..."))
|
||||
return
|
||||
|
||||
if(is_adult)
|
||||
if(amount_grown >= 10)
|
||||
// Check if there's enough 'room' to split.
|
||||
var/list/nearby_things = orange(1, src)
|
||||
var/free_tiles = 0
|
||||
for(var/turf/T in nearby_things)
|
||||
var/free = TRUE
|
||||
if(T.density) // No walls.
|
||||
continue
|
||||
for(var/atom/movable/AM in T)
|
||||
if(AM.density)
|
||||
free = FALSE
|
||||
break
|
||||
|
||||
if(free)
|
||||
free_tiles++
|
||||
|
||||
if(free_tiles < 3) // Three free tiles are needed, as four slimes are made and the 4th tile is from the center tile that the current slime occupies.
|
||||
to_chat(src, span("warning", "It is too cramped here to reproduce..."))
|
||||
return
|
||||
|
||||
var/list/babies = list()
|
||||
for(var/i = 1 to 4)
|
||||
babies.Add(make_new_slime())
|
||||
|
||||
var/mob/living/simple_mob/slime/new_slime = pick(babies)
|
||||
new_slime.universal_speak = universal_speak
|
||||
if(src.mind)
|
||||
src.mind.transfer_to(new_slime)
|
||||
else
|
||||
new_slime.key = src.key
|
||||
qdel(src)
|
||||
else
|
||||
to_chat(src, span("warning", "I am not ready to reproduce yet..."))
|
||||
else
|
||||
to_chat(src, span("warning", "I have not evolved enough to reproduce yet..."))
|
||||
|
||||
// Used when reproducing or dying.
|
||||
/mob/living/simple_mob/slime/xenobio/proc/make_new_slime(var/desired_type)
|
||||
var/t = src.type
|
||||
if(desired_type)
|
||||
t = desired_type
|
||||
if(prob(mutation_chance / 10))
|
||||
t = /mob/living/simple_mob/slime/xenobio/rainbow
|
||||
else if(prob(mutation_chance) && slime_mutation.len)
|
||||
t = slime_mutation[rand(1, slime_mutation.len)]
|
||||
var/mob/living/simple_mob/slime/xenobio/baby = new t(loc, src)
|
||||
|
||||
// Handle 'inheriting' from parent slime.
|
||||
baby.mutation_chance = mutation_chance
|
||||
baby.power_charge = round(power_charge / 4)
|
||||
|
||||
if(!istype(baby, /mob/living/simple_mob/slime/xenobio/rainbow))
|
||||
baby.unity = unity
|
||||
baby.faction = faction
|
||||
baby.friends = friends.Copy()
|
||||
|
||||
step_away(baby, src)
|
||||
return baby
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/get_description_interaction()
|
||||
var/list/results = list()
|
||||
|
||||
if(!stat)
|
||||
results += "[desc_panel_image("slimebaton")]to stun the slime, if it's being bad."
|
||||
|
||||
results += ..()
|
||||
|
||||
return results
|
||||
|
||||
/mob/living/simple_mob/slime/xenobio/get_description_info()
|
||||
var/list/lines = list()
|
||||
var/intro_line = "Slimes are generally the test subjects of Xenobiology, with different colors having different properties. \
|
||||
They can be extremely dangerous if not handled properly."
|
||||
lines.Add(intro_line)
|
||||
lines.Add(null) // To pad the line breaks.
|
||||
|
||||
var/list/rewards = list()
|
||||
for(var/potential_color in slime_mutation)
|
||||
var/mob/living/simple_mob/slime/S = potential_color
|
||||
rewards.Add(initial(S.slime_color))
|
||||
var/reward_line = "This color of slime can mutate into [english_list(rewards)] colors, when it reproduces. It will do so when it has eatten enough."
|
||||
lines.Add(reward_line)
|
||||
lines.Add(null)
|
||||
|
||||
lines.Add(description_info)
|
||||
return lines.Join("\n")
|
||||
Reference in New Issue
Block a user